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",
},
}