import { asyncTransitions, useStatus, } from "@charcuterie/logic" import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import { CATEGORICAL_HUES, CATEGORICAL_INDEXES, getCategoricalIndex, } from "@charcuterie/tokens" import type { Meta, StoryObj } from "@storybook/react" import { categoricalArgType, intentArgType, } from "../argTypes.storyHelpers.ts" import { Button } from "../Button/Button.tsx" import { ContainerBoard, StoryCell, StoryGrid, StoryRow, StorySection, } from "../board.storyHelpers.tsx" import { DotIcon } from "../icons.storyHelpers.tsx" import { getAsyncIntent, getAsyncLabel, } from "../statusIntent.ts" import { Badge } from "./Badge.tsx" const INTENTS = [ "neutral", "accent", "success", "warning", "danger", "info", ] as const const meta = { title: "Components/Data/Badge", component: Badge, parameters: { layout: "padded" }, argTypes: { categorical: categoricalArgType, intent: intentArgType, }, // 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: { appearance: "soft", intent: "neutral", overflow: "truncate", size: "md", }, } satisfies Meta export default meta type Story = StoryObj /** * A badge has no role by design — it is a word about something * else, not a live region — so the handle an agent reads off the row * is its text. */ export const Playground: Story = { parameters: playgroundParameters, args: { children: "running", intent: "info" }, } export const AllVariants: Story = { args: { children: "running" }, // `categorical` is destructured out rather than spread: this board // is about intents, and the type refuses a badge holding both — a // badge is one colour. render: ({ categorical, ...badgeProps }) => ( {(["soft", "solid", "outline"] as const).flatMap( (appearance) => INTENTS.map((intent) => ( {intent} )), )} ), } export const AllStates: Story = { args: { children: "running" }, render: ({ categorical, ...badgeProps }) => ( } intent="success" > verified
Blade Runner (1982) completed
), } /** * The numbered family, which means nothing on purpose. * * Ten hues, contrast-gated in both schemes and both directions, so * a user picking a colour for a label cannot pick one that is * unreadable. That is the whole difference between this and * `Swatch`: `Swatch` takes a colour arriving from the world — a * physical sticker, an accent pulled off an album cover — and can * make no promises about it at all. * * Flip the scheme in the toolbar. Every pill here holds its ratio * on both sides, and the ring stays separable on both. */ export const Categorical: Story = { args: { children: "label" }, render: ({ intent, ...badgeProps }) => ( {(["soft", "solid", "outline"] as const).flatMap( (appearance) => CATEGORICAL_INDEXES.map((index) => ( {CATEGORICAL_HUES[index].label} )), )} ), } /** * Invented labels, coloured by `getCategoricalIndex(name)` — the * deterministic fallback for the rows that existed before anybody * thought about colour. * * The point of the row is what it looks like *as a row*: this is * how a task list reads when a dozen labels are on screen at once, * which is the only place a categorical palette is really tested. * Nobody compares two swatches in a specimen board; they scan a * list for the green one. */ export const CategoricalLabels: Story = { args: { children: "label" }, render: () => (
{[ "Homelab", "Errands", "Anime backlog", "Garage", "Kitchen", "Reading", "Music library", "Firmware", "Bikes", "Prints", ].map((label) => ( {label} ))}
), } /** * A label longer than the space it is given, which is the case that * shipped broken: `shrink-0` plus `whitespace-nowrap` and no cap let * the pill paint straight across the next column. * * `truncate` keeps the row height fixed and paints an ellipsis — * and because that ellipsis is painted rather than inserted, a * triple-click still selects the whole string and a screen reader * still reads it. Hovering a clipped pill shows the rest. * * `wrap` is the answer where hover does not exist. The kiosk is a * touch context, so a pill whose full text matters wraps there * rather than hiding half of itself behind an affordance nobody can * reach. */ export const Responsive: Story = { args: { children: "quarantined — checksum mismatch on title 4", intent: "warning", }, render: (badgeProps) => (
), } const AsyncStatusBadge = () => { const { reset, status, transitionTo } = useStatus({ initialState: "idle", transitions: asyncTransitions, }) return ( {getAsyncLabel(status)} ) } /** * The M2 join, live: one `asyncTransitions` machine, one * `getAsyncIntent`, and a badge that cannot render a state the * machine does not have. * * Press *Succeed* before *Start* and the badge does not move: * `idle → success` is not a legal edge, so the core throws (loudly, * in the console) rather than painting a green pill over a job that * never ran. That is the difference between this and mux-magic's * `Record`, which renders whatever string it is * handed and colours an unknown one as nothing at all. */ export const Interactive: Story = { args: { children: "running" }, render: () => , }