import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import { controlSizeArgType } from "../argTypes.storyHelpers.ts" import { ContainerBoard, StoryCell, StoryGrid, StorySection, } from "../board.storyHelpers.tsx" import { ImageIcon, InboxIcon, PlayIcon, SearchIcon, SettingsIcon, } from "../icons.storyHelpers.tsx" import { RadioGroup } from "../RadioGroup/RadioGroup.tsx" import type { ActionTileItem } from "./ActionTiles.tsx" import { ActionTiles } from "./ActionTiles.tsx" /** * The shape this component was built for: a first step that opens * the next step. Nothing stays selected, because nothing was chosen * — the page moved on. */ const QUEUE_TYPE_ITEMS: ActionTileItem[] = [ { hint: "Choose titles yourself, then arrange them in priority and random lanes.", label: "Picks", value: "picks", }, { hint: "Set eligibility filters and let the app select matching titles.", label: "Rules", value: "rules", }, ] /** A tile may lead with an icon. The library ships none. */ const START_ITEMS: ActionTileItem[] = [ { hint: "Everything that arrived since the last run.", icon: , label: "New arrivals", value: "new", }, { hint: "Whatever the saved query matches today.", icon: , label: "Saved search", value: "search", }, { hint: "Pick up where the last session stopped.", icon: , label: "Continue", value: "continue", }, ] /** * Enough tiles to see the palette walk. Ten hues in * `CATEGORICAL_SEQUENCE` order — every neighbouring pair a third of * the wheel apart — and not one colour prop between them. */ const LIBRARY_ITEMS: ActionTileItem[] = [ { hint: "Feature films.", icon: , label: "Movies", value: "movies", }, { hint: "Episodic television.", icon: , label: "Shows", value: "shows", }, { hint: "Albums and tracks.", icon: , label: "Music", value: "music", }, { hint: "Scanned pages by series.", icon: , label: "Comics", value: "comics", }, { hint: "Cores and ROMs.", icon: , label: "Games", value: "games", }, ] /** * A tile that carries an `href` is a real ``, so * middle-click, ctrl-click and "copy link address" all work. The * same set may mix links and buttons. */ const TOOL_ITEMS: ActionTileItem[] = [ { hint: "Read a container's tracks and say what would change.", href: "/tools/inspect", label: "Inspect", value: "inspect", }, { hint: "Rewrite the container without touching a stream.", href: "/tools/remux", label: "Remux", value: "remux", }, { hint: "The reference, on the upstream site.", href: "https://example.com/docs", isExternal: true, label: "Read the docs", value: "docs", }, ] const meta = { title: "Components/Actions/ActionTiles", component: ActionTiles, parameters: { layout: "padded" }, argTypes: { size: controlSizeArgType }, // The component's own defaults, restated — Storybook does not seed // `args` from docgen, so an unstated default shows in the props // table with nothing selected in its control. args: { accent: "auto", minTileInlineSize: 200, size: "md", }, } satisfies Meta export default meta type Story = StoryObj export const Playground: Story = { parameters: playgroundParameters, args: { items: QUEUE_TYPE_ITEMS, label: "Queue type" }, } export const AllVariants: Story = { args: { items: QUEUE_TYPE_ITEMS, label: "Queue type" }, render: (controlProps) => ( {(["sm", "md", "lg"] as const).map((size) => ( ))} ), } /** * A tile may lead with an icon. The library ships none — these are * story-only SVGs, exactly what an app passes — and the icon is * `aria-hidden` by construction, because the name it sits above is * inside the same tile. */ export const WithIcons: Story = { args: { items: START_ITEMS, label: "Where to start" }, } /** * `href` makes the tile an anchor rather than a button, routed * through the injected `RouterLink` when the destination is the * router's. An `isExternal` tile falls back to a plain `` and says so to a screen reader. */ export const Links: Story = { args: { items: TOOL_ITEMS, label: "Pick a tool" }, } export const AllStates: Story = { args: { items: QUEUE_TYPE_ITEMS, label: "Queue type" }, render: (controlProps) => ( ), } /** * The two tile components, side by side, which is the comparison a * reader choosing between them actually needs. * * The box is the **same box** — `tileStyles.ts` holds the padding, * the border, the radius, the surface, the type ramp and the grid, * and both components read it. What differs is everything the box is * not: one announces "1 of 2" and keeps a tile selected, the other * presses and is done. */ export const BesideTheRadioTile: Story = { args: { items: QUEUE_TYPE_ITEMS, label: "Queue type" }, render: (controlProps) => ( ({ hint, label, value, }), )} label="Queue type, as radios" size={controlProps.size} /> ), } /** * Column count comes from the **container**, never the window — the * standing rule for any list of cards, and the reason this board * varies a wrapper's width rather than the viewport's. * * At the 200px default floor: one track at 15rem, one at 24rem, two * at 34rem. `auto-fill` and deliberately not `auto-fit`, so the * tiles keep their size in a container with room to spare instead of * stretching to fill it. Same formula as the `RadioGroup` tile, from * the same constant. */ export const Responsive: Story = { args: { items: START_ITEMS, label: "Where to start" }, render: (controlProps) => ( {(width) => ( )} ), } /** * The complete keyboard path, and it is the platform's: Tab reaches * **each** tile, Enter and Space press a button one, Enter follows a * link one. There is no roving tabindex here — a radio group borrows * one tab stop because exactly one of its options is true at a time, * and nothing here is true. */ export const Interactive: Story = { args: { items: QUEUE_TYPE_ITEMS, label: "Queue type" }, } /** * The default: the set walks the ten-hue categorical palette in * order, so ten tiles get ten colours and the call site passes no * colour at all. * * The bar is `Card`'s accent-edge pseudo-element, which is why a * tile and a card on one page are the same bar rather than two that * nearly match, and why it follows the tile's corner instead of * running past it and stopping square. */ export const AutoHues: Story = { args: { items: LIBRARY_ITEMS, label: "Pick a library" }, } /** * A tile that names its own `categorical` keeps that hue, and the * rest go on walking the palette from their own positions — so * naming one does not force naming all of them. * * Worth doing when the colour belongs to the THING rather than to * the list: a destination that is hue 7 everywhere else in the app * should not become hue 2 because a tile was inserted above it. */ export const NamedHues: Story = { args: { items: [ { ...(LIBRARY_ITEMS[0] as ActionTileItem) }, { ...(LIBRARY_ITEMS[1] as ActionTileItem), categorical: 7, }, { ...(LIBRARY_ITEMS[2] as ActionTileItem) }, ], label: "Pick a library, with Shows pinned to hue 7", }, } /** * `accent="none"` is the paint this component shipped with, kept as * an opt-out rather than deleted. * * Reach for it when the tiles sit inside something already carrying * a colour of its own — a coloured panel, a lane header — where a * second palette is noise rather than information. Everything else * about the tile is unchanged. */ export const Neutral: Story = { args: { accent: "none", items: START_ITEMS, label: "Where to start", }, }