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 { InboxIcon, PlayIcon, SearchIcon, } from "../icons.storyHelpers.tsx" import type { RadioItem } from "./RadioGroup.tsx" import { RadioGroup } from "./RadioGroup.tsx" /** * Options carrying a real sentence each — the case an in-line * `SegmentedControl` strip would wrap, and the reason to stack. */ const NAMING_ITEMS: RadioItem[] = [ { label: "Match the Plex agent", value: "plex" }, { label: "Match AniDB titles", value: "anidb" }, { label: "Keep the original filenames", value: "original", }, { label: "Use a custom pattern", value: "custom" }, ] /** * The same control with a name and a line of help per option — the * shape four apps in the fleet had each hand-painted, and the reason * none of them reached for this component. */ const IMPORT_ITEMS: RadioItem[] = [ { hint: "Nothing is left behind at the source.", label: "Move the files", value: "move", }, { hint: "Twice the space, and the source is untouched.", label: "Copy the files", value: "copy", }, { hint: "One copy on disk, two names for it. Same volume only.", label: "Hard link", value: "link", }, { hint: "Catalogue them where they are.", label: "Leave in place", value: "leave", }, ] const ICON_ITEMS: RadioItem[] = [ { 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", }, ] const meta = { title: "Components/Controls/RadioGroup", component: RadioGroup, 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: { isReadOnly: false, itemShape: "row", minTileInlineSize: 200, size: "md", }, } satisfies Meta export default meta type Story = StoryObj export const Playground: Story = { parameters: playgroundParameters, args: { items: NAMING_ITEMS, label: "Naming scheme" }, } export const AllVariants: Story = { args: { items: NAMING_ITEMS, label: "Naming scheme" }, render: (controlProps) => ( {(["sm", "md", "lg"] as const).map((size) => ( ))} ), } /** * `itemShape="tile"` is the same `radiogroup` in a box: one choice * out of several, exclusive, announced as "3 of 6", drawn as cards * in a grid that gains columns with its **container**. * * The selected tile is an accent **edge** and a lifted surface, not * a fill — the radio dot inside it is still the thing a screen * reader reads, and a state told in colour alone is the one state a * monochrome ePaper build cannot show. */ export const Tiles: Story = { args: { items: IMPORT_ITEMS, itemShape: "tile", label: "On import", }, } /** * The tile at all three sizes. * * `AllVariants` above covers the three heights of a **row**, and * every tile story took the default `md` — so the largest tile had * no snapshot at all, and `TILE_PADDING_CLASS.lg` could be changed * without a single pixel moving in the report. It was, on * 2026-09-02, when `lg` grew to a landing-page tile's padding. * * The padding is shared with `ActionTiles` through `tileStyles.ts`, * which is the promise this board is here to photograph. */ export const TileSizes: Story = { args: { items: IMPORT_ITEMS, itemShape: "tile", label: "On import", }, 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 * in the same button. */ export const TilesWithIcons: Story = { args: { items: ICON_ITEMS, itemShape: "tile", label: "Where to start", }, } const STATE_ITEMS: RadioItem[] = [ { label: "Match the Plex agent", value: "plex" }, { label: "Match AniDB titles", value: "anidb" }, { isDisabled: true, label: "Keep the original filenames", value: "original", }, { label: "Use a custom pattern", value: "custom" }, ] /** * A disabled option is out of the *focus* group and still one of the * *options* — it can even be the one a consumer names as the initial * `selectedValue`, which is what a saved preference pointing at a * scheme the source no longer supports looks like. */ export const AllStates: Story = { args: { items: STATE_ITEMS, label: "Naming scheme" }, render: (controlProps) => ( ), } /** * 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. */ export const Responsive: Story = { args: { items: IMPORT_ITEMS, itemShape: "tile", label: "On import", }, render: (controlProps) => ( {(width) => ( )} ), } /** * The complete keyboard path. Tab enters the group **once** — the * roving-tabindex rule — then the arrow keys move and check * together, the same activation model as `SegmentedControl`. */ export const Interactive: Story = { args: { items: NAMING_ITEMS, label: "Naming scheme" }, }