import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import type { ReactNode } from "react" import { useState } from "react" import { controlSizeArgType } from "../argTypes.storyHelpers.ts" import { ContainerBoard, StoryCell, StoryGrid, StorySection, } from "../board.storyHelpers.tsx" import { Field } from "../Field/Field.tsx" import { TimecodeInput } from "./TimecodeInput.tsx" import type { TimecodeRange } from "./timecode.ts" import { formatTimecode } from "./timecode.ts" /** * A made-up 45 minute 12 second file, so every demo reads the same * numbers and a screenshot of one is the same picture next week. */ const DURATION_MS = 2_712_000 /** * The demo readout prints a committed value the way the field * writes it — `millisecondDigits: "auto"` — so one position is one * spelling everywhere on the page. `formatTimecode`'s own default is * still the full `hh:mm:ss.mmm`, and the docs page shows both. */ const describeValue = ( value: null | number | TimecodeRange, ) => { if (value === null) { return "—" } if (typeof value === "number") { return formatTimecode(value, { millisecondDigits: "auto", }) } // A section with neither end set is the same absence `onChange` // reports as `null`, so the demo reads it the same way. if (value.end === null && value.start === null) { return "—" } return `${value.start === null ? "the beginning" : formatTimecode(value.start, { millisecondDigits: "auto" })} to ${value.end === null ? "the end" : formatTimecode(value.end, { millisecondDigits: "auto" })}` } const meta = { title: "Components/Controls/TimecodeInput", component: TimecodeInput, args: { durationMs: DURATION_MS, isDisabled: false, isRange: false, label: "Start at", size: "md", stepMs: 1_000, }, argTypes: { size: controlSizeArgType, }, parameters: { layout: "padded" }, } satisfies Meta export default meta type Story = StoryObj const TimecodeHarness = ({ ...inputProps }: { durationMs?: number isDisabled?: boolean isRange?: boolean label: string maxValueMs?: number minValueMs?: number stepMs?: number valueMs?: number | TimecodeRange }): ReactNode => { const [chosen, setChosen] = useState< null | number | TimecodeRange >(inputProps.valueMs ?? null) return (

Chosen: {describeValue(chosen)}

) } /** * The field, empty. Type a position and the line underneath resolves * it before anything commits — `90` is ninety seconds, `1:30` is the * same position spelled the other way, `1:02:03.500` is the full * form. Enter or blur commits; Escape puts the last value back. * * The line is a reading of the field, so it leaves when the focus * does. A refusal stays up, because a control that is still marked * invalid needs the sentence that says why. * * Milliseconds are optional both ways: `1:01:00` commits as * `01:01:00`, not as `01:01:00.000`. */ export const Playground: Story = { parameters: playgroundParameters, args: { label: "Start at" }, render: () => , } /** * Single and section, a bounded window, a coarse step, and the field * inside a `Field`. * * `isRange` is a **mode**, not a sibling component — the same call * `DatePicker` made, and for the same reason: a section has no ARIA * role of its own to be named after. */ export const AllVariants: Story = { args: { label: "Start at" }, render: () => ( ), } /** * Empty, filled, disabled — and the four states a section really * has. Both ends are independently optional, so "from here to the * end" and "from the beginning, stopping here" are values rather * than half-finished input. */ export const AllStates: Story = { args: { label: "Start at" }, render: () => ( ), } /** * The field at three container widths, which is the only honest way * to story a container query — the window never moves. * * Below `--cq-sm` the section becomes the **Narrow View**: the two * fields stack, and the word `to` is replaced by a "Start" caption * over the first and an "End" caption over the second. Two * `hh:mm:ss.mmm` fields and a word between them fill a phone-width * modal edge to edge, which is where this was reported from. */ export const Responsive: Story = { args: { label: "Start at" }, render: () => ( {(width) => ( )} ), } /** * The complete keyboard path. * * Tab to the field and type — the line underneath resolves what you * typed, and nothing has committed. Enter or Tab commits. Escape * puts the last committed value back. ArrowUp and ArrowDown step by * `stepMs`, and Shift multiplies that by ten. * * In the section below, type an end that sits before the start and * the pair **swaps**; type an end equal to the start and it is * **refused**, because a section with no length plays nothing. The * refusal is readable after the field has been left, which is when * a commit-time complaint is written. */ export const Interactive: Story = { args: { label: "Start at" }, render: () => (
), }