import type { Meta, StoryObj } from '@storybook/react-vite' import { IconSquareRounded } from '@tabler/icons-react' import { MenuPanel } from './Menu' import { PreviewCard } from './PreviewCard' import { SkeletonBar } from './Skeleton' /** * More of a thing, on hover. * * A list shows one line per thing because a list has to. This is where the * rest of that thing goes: a card that opens beside the row when the pointer * rests on it, so the reader can decide whether to open the thing without * opening it. * * The canvas draws the surface with `MenuPanel`, since a live card portals and * places itself; **`OnARow`** and **`Scrolling`** are the live ones (D25). */ const meta = { title: 'Overlays/PreviewCard', component: PreviewCard, decorators: [(Story) =>
], args: { content: null, children: null }, argTypes: { content: { control: false }, children: { control: false } }, } satisfies Meta export default meta type Story = StoryObj /** A heading, a few named values and a paragraph — the shape most previews * take. What goes in is entirely the caller's. */ function Detail() { return ( <> Item one
{[ ['Label', 'Value'], ['Label', 'Another value'], ['Label', 'A third value'], ].map(([k, v], i) => (
{k} {v}
))}
This is the card. It opened because the pointer came to rest on the row and stayed there for 350ms, and it will close 200ms after the pointer leaves — long enough to move into the card without losing it. ) } function Row({ label, note }: { label: string; note?: string }) { return (
{label} {note && {note}}
) } /** The card at rest, so its anatomy can be read without hovering anything. */ export const Default: Story = { parameters: { controls: { disable: true } }, render: () => ( ), } /** * While the content is still arriving. * * A skeleton rather than a spinner, on purpose: a preview has a known shape, * and drawing that shape says "this is what is coming" where a spinner only * says "wait". It also keeps the card the size it is about to be, so nothing * jumps when the content lands. */ export const Loading: Story = { parameters: { controls: { disable: true } }, render: () => ( /* The skeleton is the shape of `Default`, line for line: a heading, three label/value rows, and two lines of detail. That is what makes it a skeleton rather than a placeholder — the card is the size it is about to be, so nothing moves when the content lands. */
{['w-16', 'w-24', 'w-20'].map((w, i) => (
))}
), } /** Live. Rest the pointer on the row and read what the card says. */ export const OnARow: Story = { // axe color-contrast is off here until PLAN.md stage 0.10 is ruled: // the row's note is muted caption text, 3.93:1 on --bg-base in signal (AA 4.5:1). parameters: { controls: { disable: true }, a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } } }, render: () => ( } wrapperClassName="w-[320px]"> ), } /** * A card taller than its cap scrolls, **and you can reach the scrollbar** — * which is the whole difference between this and a tooltip. Rest the pointer * on the row, move into the card, and scroll it. */ export const Scrolling: Story = { parameters: { controls: { disable: true } }, render: () => ( Item with more than fits There is more here than the card's 300px cap allows, so it scrolls. You can reach that scrollbar because the pointer can enter this card. A tooltip cannot be entered, so a tooltip that scrolled would be a tooltip nobody could read to the end. {Array.from({ length: 14 }, (_, i) => (
Label Value {i + 1}
))} } >
), } /** * The side is a preference, not a promise: it flips when the side you asked * for has no room. This one asks for the right in a row pinned to the right * edge, so it opens on the left. */ export const FlippedAtAnEdge: Story = { parameters: { controls: { disable: true }, layout: 'fullscreen' }, render: () => (
} wrapperClassName="w-[320px]">
), }