[
  {
    "docPath": "FORMS.md",
    "example": "import { inertiaUpload } from \"@godxjp/ui/inertia\";\nconst upload = inertiaUpload((file, callbacks) => router.post(endpoint, { file }, callbacks), t(\"Upload failed\"));\n<Upload onUpload={upload} />;",
    "group": "data-entry",
    "importPath": "@godxjp/ui/inertia",
    "name": "inertiaUpload",
    "props": [
      {
        "description": "Start a router.post visit with the supplied callbacks and file.",
        "name": "send",
        "type": "(file: File, callbacks: InertiaUploadCallbacks) => void"
      },
      {
        "description": "Localized fallback for network or HTTP failures.",
        "name": "errorMessage",
        "type": "string"
      }
    ],
    "related": [
      "Upload",
      "FormRoot"
    ],
    "rules": [
      1
    ],
    "tagline": "Bridge Inertia multipart visits to Upload progress, cancellation, retry and error recovery.",
    "usage": [
      "Pass the returned callback to Upload.onUpload. Spread callbacks into router.post options; they enable multipart and independent async requests.",
      "Keep failed items in controlled value so the user can retry; remove only done items when server props provide the saved list."
    ]
  },
  {
    "docPath": "docs/data-entry/form-root.tsx",
    "example": "import { FormRoot, FormFieldControl, useZodForm } from \"@godxjp/ui/form\";\nimport { Input } from \"@godxjp/ui/data-entry\";\nimport { z } from \"zod\";\nconst schema = z.object({ email: z.string().email() });\n// Inside your component:\nconst form = useZodForm(schema, { defaultValues: { email: \"\" } });\n<FormRoot form={form} onSubmit={save} layout=\"horizontal\"><FormFieldControl name=\"email\" label=\"Email\">{field => <Input {...field} value={String(field.value ?? \"\")} />}</FormFieldControl></FormRoot>;",
    "group": "data-entry",
    "importPath": "@godxjp/ui/form",
    "name": "FormRoot",
    "props": [
      {
        "description": "RHF form returned by useZodForm; use either form or adapter.",
        "name": "form",
        "type": "UseFormReturn<T>"
      },
      {
        "description": "External store with getValue/setValue/getError/getValues/isSubmitting and optional reset.",
        "name": "adapter",
        "type": "FormStateAdapter"
      },
      {
        "description": "Validated submit callback.",
        "name": "onSubmit",
        "type": "(values: T) => void | Promise<void>"
      },
      {
        "description": "Validation failure callback.",
        "name": "onSubmitFailed",
        "type": "(errors: FieldErrors<T>) => void"
      },
      {
        "description": "Handle rejected submission.",
        "name": "onSubmitError",
        "type": "(error: unknown) => void"
      },
      {
        "description": "Banner shown when onSubmit rejects. Default: localized dataEntry.form.submitFailed text; a node replaces it; false never shows it. Skipped by default for a validation rejection (400/422) once the `errors` bag holds a message.",
        "name": "submitFailedMessage",
        "type": "ReactNode | false"
      },
      {
        "description": "Called after values reset.",
        "name": "onReset",
        "type": "() => void"
      },
      {
        "description": "Scroll the first invalid field into view.",
        "name": "scrollToFirstError",
        "type": "boolean"
      },
      {
        "description": "Disable field mutations.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Shared Form layout.",
        "name": "layout",
        "type": "FormLayoutProp"
      },
      {
        "description": "Shared label width.",
        "name": "labelWidth",
        "type": "WidthProp"
      },
      {
        "description": "Shared control width.",
        "name": "controlWidth",
        "type": "WidthProp"
      },
      {
        "description": "Shared label alignment.",
        "name": "labelAlign",
        "type": "\"start\" | \"end\""
      },
      {
        "description": "Responsive stacking breakpoint.",
        "name": "collapseBelow",
        "type": "BreakpointProp | false"
      },
      {
        "description": "Control density.",
        "name": "density",
        "type": "DensityProp"
      },
      {
        "description": "Server error bag.",
        "name": "errors",
        "type": "ErrorBagProp"
      },
      {
        "description": "Required/optional marking policy.",
        "name": "requiredMark",
        "type": "boolean | \"optional\""
      },
      {
        "description": "Responsive field grid rendered inside the form element.",
        "name": "columns",
        "type": "ResponsiveGridColumnsProp"
      }
    ],
    "related": [
      "Form",
      "FormField",
      "FormRoot",
      "FormFieldControl"
    ],
    "rules": [
      23,
      31
    ],
    "storyPath": "data-entry/form-root.tsx",
    "tagline": "Client or server-adapted forms with shared layout, validation, submission, reset and error recovery.",
    "usage": [
      "Use inside the documented form composition; do not nest native form elements.",
      "Use godx-ui controls and preserve field names, errors and disabled state.",
      "SUBMIT-FAILED BANNER: when `onSubmit` rejects, FormRoot shows a destructive banner (`submitFailedMessage`, default the localized `dataEntry.form.submitFailed`). It is SKIPPED for a validation rejection (`classifyQueryError(error).category === \"validation\"`: 400/422) once the `errors` bag holds at least one message — the fields / `<FormErrors />` show it, as antd shows field errors instead of a form banner — and stays skipped for that failure if the app later clears the bag. A validation rejection with an empty or message-less bag, and every 5xx / network / unknown rejection, still shows it, so a failure is never silently swallowed. Errors mapped with react-hook-form `setError` instead of `errors` are not detected: pass `submitFailedMessage={false}` there. `submitFailedMessage={false}` never shows the banner; a node replaces its text.",
      "CANONICAL SERVER-VALIDATION FORM (gh#698) — a 422 renders EXACTLY ONCE: `const m = useMutation({ mutationFn: save }); <FormRoot form={form} onSubmit={(v) => m.mutateAsync(v)} errors={serverErrors(m.error)}><FormErrors /><AlertMutationFeedback mutation={m} /><FormFieldControl name=\"code\" label=\"Code\">{(field) => <Input {...field} value={String(field.value ?? \"\")} />}</FormFieldControl></FormRoot>` (`serverErrors` = your API client's mapper from its error to the Laravel-style `{ key: string[] }` bag). Each message appears once: under its field (claimed key) or in `<FormErrors />` (unclaimed key); `AlertMutationFeedback` skips the validation error (gh#690) and FormRoot shows no `submitFailed` banner. A 5xx / network rejection still shows both the FormRoot banner and the AlertMutationFeedback alert — pass `submitFailedMessage={false}` to keep only the latter.",
      "DON'T hand-guard the banner or wrap `mutateAsync` in try/catch just to hide a 422 — pass the bag to `errors` and the form renders it once."
    ],
    "useCases": [
      "Validated settings forms",
      "Nested repeating data entry"
    ]
  },
  {
    "docPath": "docs/data-entry/form-root.tsx",
    "example": "import { FormFieldControl } from \"@godxjp/ui/form\";\nimport { Input } from \"@godxjp/ui/data-entry\";\n<FormFieldControl name=\"email\" label=\"Email\" required>{field => <Input {...field} value={String(field.value ?? \"\")} />}</FormFieldControl>;",
    "group": "data-entry",
    "importPath": "@godxjp/ui/form",
    "name": "FormFieldControl",
    "props": [
      {
        "description": "Typed field path, including nested list rows.",
        "name": "name",
        "type": "FieldPath<T>"
      },
      {
        "description": "Visible field label. Optional (antd): omitted, the field renders NO label row and reserves no space for one — the boolean-field shape, where the control carries its own inline label.",
        "name": "label",
        "type": "React.ReactNode"
      },
      {
        "defaultValue": "\"value\"",
        "description": "antd `valuePropName`. `\"checked\"` hands the render prop `checked` / `onCheckedChange` instead of `value` / `onChange` (the render-prop type narrows) and stores a boolean, so `<Checkbox {...field}>label</Checkbox>` / `<Switch {...field} />` need no wiring.",
        "name": "valuePropName",
        "type": "\"value\" | \"checked\""
      },
      {
        "description": "Dependent fields that trigger revalidation.",
        "name": "dependencies",
        "type": "FieldPath<T>[]"
      },
      {
        "description": "Extract the control value.",
        "name": "getValueFromEvent",
        "type": "(...args: unknown[]) => unknown"
      },
      {
        "description": "Normalize before storage.",
        "name": "normalize",
        "type": "(value: unknown, previous: unknown) => unknown"
      },
      {
        "description": "Keep value after unmount, default true.",
        "name": "preserve",
        "type": "boolean"
      },
      {
        "description": "Override validation error text.",
        "name": "help",
        "type": "React.ReactNode"
      },
      {
        "description": "Field-level disabled state.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Validation feedback state.",
        "name": "validateStatus",
        "type": "\"success\" | \"warning\" | \"error\" | \"validating\""
      },
      {
        "description": "Show accessible validation feedback.",
        "name": "hasFeedback",
        "type": "boolean"
      },
      {
        "description": "Custom feedback icon/content.",
        "name": "feedback",
        "type": "React.ReactNode"
      },
      {
        "description": "Render a godx-ui control bound to the field.",
        "name": "children",
        "type": "(field) => React.ReactNode"
      },
      {
        "description": "Optional DOM identity; defaults to a unique id even across sibling forms.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Per-field layout override.",
        "name": "layout",
        "type": "FormLayoutProp"
      },
      {
        "description": "Per-field label width.",
        "name": "labelWidth",
        "type": "WidthProp"
      },
      {
        "description": "Per-field control width.",
        "name": "controlWidth",
        "type": "WidthProp"
      },
      {
        "description": "Label help or action. In a horizontal/inline layout it wraps under the label inside the label column when it does not fit beside it.",
        "name": "labelAddon",
        "type": "React.ReactNode"
      },
      {
        "description": "Grid column span.",
        "name": "colSpan",
        "type": "number"
      }
    ],
    "related": [
      "Form",
      "FormField",
      "FormRoot",
      "FormFieldControl"
    ],
    "rules": [
      23,
      31
    ],
    "tagline": "Bind a typed field to RHF or a server adapter, with shared FormField presentation and validation.",
    "usage": [
      "Use inside the documented form composition; do not nest native form elements.",
      "Use godx-ui controls and preserve field names, errors and disabled state.",
      "RENDER-PROP BAG: `{ id, name, value, onChange, onValueChange, onBlur, ref, disabled? }`. `ref` is `RefCallback<HTMLElement>` (gh#698), so `{...field}` spreads onto Input, Textarea, Select, NumberInput and DatePicker with NO cast — never cast or drop the ref (react-hook-form focuses the first invalid field through it). `value` is `unknown`: narrow it per control (`String(field.value ?? \"\")`, `typeof field.value === \"number\" ? field.value : null`).",
      "SERVER VALIDATION: a `FormField name` under `FormRoot errors={…}` claims its bag key, so the 422 message renders once under the field — see FormRoot's canonical server-validation composition (FormRoot + FormErrors + AlertMutationFeedback renders a 422 exactly once).",
      "CANONICAL BOOLEAN FIELD (gh#709, antd `Form.Item valuePropName=\"checked\"`): `<FormFieldControl name=\"is_shared\" valuePropName=\"checked\">{(field) => <Checkbox {...field}>プロジェクトに共有する</Checkbox>}</FormFieldControl>` — box and label on ONE line, the label text toggles the box and names it, no label row above it, and a validation / 422 error still lands on the checkbox (`aria-invalid` + `aria-describedby`). The bag is `{ id, name, checked, onCheckedChange, onBlur, ref, disabled? }`; the stored value is always a boolean.",
      "DON'T put a boolean field's label ABOVE its checkbox — `<FormFieldControl name=\"is_shared\" label=\"プロジェクトに共有する\">{(field) => <Checkbox checked={field.value} onCheckedChange={(c) => field.onChange(c === true)} />}</FormFieldControl>` renders the label on one row and the box far below it. Use `valuePropName=\"checked\"` with the label as the Checkbox's children."
    ],
    "useCases": [
      "Validated settings forms",
      "Nested repeating data entry"
    ]
  },
  {
    "docPath": "docs/FORMS.md",
    "example": "import { FormFieldArray, FormFieldControl } from \"@godxjp/ui/form\";\nimport { Input } from \"@godxjp/ui/data-entry\";\n<FormFieldArray name=\"contacts\">{({fields}) => fields.map(row => <FormFieldControl key={row.key} name={`${row.name}.email`} label=\"Email\">{field => <Input {...field} value={String(field.value ?? \"\")} />}</FormFieldControl>)}</FormFieldArray>;",
    "group": "data-entry",
    "importPath": "@godxjp/ui/form",
    "name": "FormFieldArray",
    "props": [
      {
        "description": "Array field path in the surrounding RHF FormRoot.",
        "name": "name",
        "type": "FieldArrayPath<T>"
      },
      {
        "description": "Receives fields with stable key/name/index, append/prepend/insert/remove/move/swap/replace, array error and disabled state.",
        "name": "children",
        "type": "(collection) => React.ReactNode"
      }
    ],
    "related": [
      "Form",
      "FormField",
      "FormRoot",
      "FormFieldControl"
    ],
    "rules": [
      23,
      31
    ],
    "tagline": "Dynamic typed field collections with stable keys, nested validation and append/remove/reorder operations.",
    "usage": [
      "Use inside the documented form composition; do not nest native form elements.",
      "Use godx-ui controls and preserve field names, errors and disabled state."
    ],
    "useCases": [
      "Validated settings forms",
      "Nested repeating data entry"
    ]
  },
  {
    "docPath": "docs/FORMS.md",
    "example": "import { useZodForm } from \"@godxjp/ui/form\";\nimport { z } from \"zod\";\nconst form = useZodForm(z.object({email: z.string().email()}), {defaultValues: {email: \"\"}, mode: \"onBlur\"});",
    "group": "data-entry",
    "importPath": "@godxjp/ui/form",
    "name": "useZodForm",
    "props": [
      {
        "description": "Zod schema, including async refinements.",
        "name": "schema",
        "type": "z.ZodType<T>"
      },
      {
        "description": "RHF options such as defaultValues, mode, reValidateMode, criteriaMode and shouldUnregister.",
        "name": "options",
        "type": "UseZodFormOptionsProp<T>"
      }
    ],
    "related": [
      "Form",
      "FormField",
      "FormRoot",
      "FormFieldControl"
    ],
    "rules": [
      23,
      31
    ],
    "tagline": "Create a typed React Hook Form instance with Zod validation; reuse reset, setValue, trigger, formState and field errors.",
    "usage": [
      "Use inside the documented form composition; do not nest native form elements.",
      "Use godx-ui controls and preserve field names, errors and disabled state."
    ],
    "useCases": [
      "Validated settings forms",
      "Nested repeating data entry"
    ]
  },
  {
    "docPath": "docs/data-display/card/index.tsx",
    "example": "<Card><CardBar pad={{block:2,inline:3}} border=\"block-start\">Tools</CardBar></Card>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "CardBar",
    "props": [
      {
        "description": "Instance padding on the token scale.",
        "name": "pad",
        "type": "PadProp"
      },
      {
        "description": "Measured padding escape.",
        "name": "padRaw",
        "type": "PadRawProp"
      },
      {
        "description": "Spacing between main and extra slots.",
        "name": "gap",
        "type": "GapProp"
      },
      {
        "description": "Optional muted ground.",
        "name": "surface",
        "type": "\"muted\""
      },
      {
        "description": "Override positional divider edges for stacked bars.",
        "name": "border",
        "type": "\"none\" | \"block-start\" | \"block-end\" | \"both\""
      }
    ],
    "related": [
      "Card",
      "Flex",
      "Toolbar — the list-page filter strip (alias FilterBar); it wraps, so filters go there rather than in a CardBar."
    ],
    "rules": [
      9
    ],
    "tagline": "Inline card toolbar with scoped inset and divider edges.",
    "usage": [
      "Compose inside Card. Unset border follows its position; explicit border prevents double rules in stacked bars.",
      "DON'T put list filters (search, filter selects, filter chips) in a CardBar: its main slot scrolls sideways when crowded, and a filter strip wraps (cardinal rule #51). Filters are a Toolbar/FilterBar above the card."
    ],
    "useCases": [
      "Composer tool strips and view tabs."
    ]
  },
  {
    "docPath": "docs/data-entry/time-range-picker.tsx",
    "example": "import { TimeRangePicker } from \"@godxjp/ui/data-entry\";\n<TimeRangePicker aria-label=\"Shift\" defaultValue={[\"09:00\", \"18:00\"]} />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "TimeRangePicker",
    "props": [
      {
        "description": "Controlled canonical times.",
        "name": "value",
        "type": "[string,string]"
      },
      {
        "description": "Uncontrolled initial range.",
        "name": "defaultValue",
        "type": "[string,string]"
      },
      {
        "description": "Reports edited range.",
        "name": "onValueChange",
        "type": "(value: [string,string]) => void"
      },
      {
        "description": "Sort times automatically; false allows overnight ranges.",
        "name": "order",
        "type": "boolean"
      },
      {
        "description": "Permitted empty endpoints, default [true,true].",
        "name": "allowEmpty",
        "type": "[boolean,boolean]"
      },
      {
        "description": "A PAIR, one per endpoint — TimePicker's single-string `placeholder` is omitted from this type on purpose, because a range has two empty fields and one string would label both of them the same. Route both through t().",
        "name": "placeholder",
        "type": "[string,string]"
      },
      {
        "description": "Native names are name_from and name_to.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "TimePicker display format.",
        "name": "format",
        "type": "string"
      },
      {
        "description": "Shared time constraints.",
        "name": "disabledTime",
        "type": "TimePickerDisabledTimeProp"
      },
      {
        "description": "Enable second precision.",
        "name": "showSeconds",
        "type": "boolean"
      },
      {
        "description": "Minute column step.",
        "name": "minuteStep",
        "type": "number"
      },
      {
        "description": "Hour column step.",
        "name": "hourStep",
        "type": "number"
      },
      {
        "description": "Second column step.",
        "name": "secondStep",
        "type": "number"
      },
      {
        "description": "Permit clear only for an endpoint also allowed empty.",
        "name": "allowClear",
        "type": "boolean"
      }
    ],
    "related": [
      "TimePicker",
      "DatePicker"
    ],
    "rules": [
      9
    ],
    "storyPath": "data-entry/time-range-picker.tsx",
    "tagline": "Ordered time range with optional endpoints and canonical native fields.",
    "usage": [
      "Use for a start/end time pair. Set order=false for overnight intervals.",
      "Each endpoint follows the one-trailing-icon rule; allowEmpty=false suppresses its clear action."
    ],
    "useCases": [
      "Shift scheduling and reception hours."
    ]
  },
  {
    "docPath": "docs/general/typography.tsx",
    "example": "<VisuallyHidden>Unread</VisuallyHidden>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "VisuallyHidden",
    "props": [
      {
        "description": "Text available to assistive technology.",
        "name": "children",
        "type": "ReactNode"
      }
    ],
    "rules": [],
    "storyPath": "general/typography.tsx",
    "tagline": "Accessible supporting text without a visible layout box.",
    "usage": [
      "Use for supplementary accessible labels. Do not hide controls that remain focusable."
    ]
  },
  {
    "docPath": "docs/data-display/timeline.tsx",
    "example": "<RangeTimeline label=\"Schedule\" columns={[{ label: \"Week\", units: 7 }]} rows={[{ id: \"task\", label: \"Task\", start: 0, end: 6, startLabel: \"Start: day 1\", endLabel: \"End: day 7\" }]} />",
    "group": "data-display",
    "importPath": "@godxjp/ui/lab",
    "name": "RangeTimeline",
    "props": [
      {
        "description": "Localized accessible name for the scrollable schedule.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "description": "Positive unit counts determine proportional column widths. Column count determines the minimum canvas width, so coarse grouping zooms out. `muted: true` tints that column down the whole body (a weekend, a holiday, a closed period) with `--range-timeline-muted-column-background` (default `hsl(var(--muted))`, the header surface; body text keeps 14.25:1 / 12.4:1 and `--muted-foreground` 5.2:1 / 5.47:1 on it).",
        "name": "columns",
        "required": true,
        "type": "{ label: string; units: number; muted?: boolean }[]"
      },
      {
        "description": "Optional grouped axis labels above the ticks, for example months above days. Counts use the same units as columns and cover the same range.",
        "name": "bands",
        "type": "{ label: string; units: number }[]"
      },
      {
        "description": "Each row has id, label, inclusive start/end unit offsets (`null` for either = the row has no dates: no bar, no handles, a muted \"no dates\" hint, gh#1189), optional `color` (hex from data, e.g. a status colour; grip ink flips black/white by luminance; non-hex keeps the default ink), `muted` (a done row recedes, --range-timeline-bar-muted-alpha), `overdue` (destructive inline-end edge that keeps the colour, plus a localized sr-only \"overdue\"), `tooltip` (ReactNode on hover and keyboard focus of a focusable bar layer named by the row label, beside the grips, never around them), `plan` ({ start, end } | null — the PLANNED range 予定, a dashed ghost bar behind the solid ACTUAL bar 実績; past the planned end a destructive overrun segment with a localized \"+N日\" label (override with `varianceLabel`, `false` hides it); ending before it a small success early marker; both also spoken; the ghost is never draggable), `planNote` (muted text after the bar for a row WITHOUT a plan, e.g. \"baseline後に追加\"), `editable` (true | false | { start?, end? }: which grips a row offers when onRangeChange is set; a started-but-open issue whose end is \"today\" passes { end: false }), `emptyHint` (what an undated row says: default the localized \"no dates\"; `false` = nothing, a pure context row), FINISH-ONLY rows (`start: null`, `end` set: a finish tick at end, no bar, spoken `startUnknownLabel` ?? \"start unknown\"), `openEnd` (started, completion not recorded: the bar runs to end with a faded inline end, no end grip, spoken `openEndLabel` ?? \"completion not recorded\"), `shape: \"milestone\"` (a diamond at end, start ignored; its plan a hollow diamond at plan.end; overrun / early and their labels as for a bar; ONE grip reported as onRangeChange(id, \"end\", delta); tooltip focusable), a ONE-SIDED plan (only plan.start or plan.end known: a dashed due tick at the known end, or start; a known plan.end still measures overrun / early), `planLabel` (what the ghost means for the row, e.g. \"ベースライン\", spoken as \"ベースラインより3日遅れ\"; overrides the timeline planLabel), `earlyLabel` (visible after the early marker; default the localized \"-N日\"; `false` hides it), `cancelled` (a hatched bar distinct from muted, spoken 中止, no grips), `emphasis` (\"warning\" | \"critical\": an inline-start accent on the row from the mark tier plus a spoken label, for \"at risk\" and the critical path), and localized startLabel/endLabel including current values. Optional `depth` (0 = top level) nests rows: pass them flat and depth-first (a parent, then its descendants); a row is a parent when the row after it is deeper. The component indents the label cell by `--range-timeline-indent-width` per level (logical padding, so RTL indents from the right; the label column keeps its width) and gives each parent a disclosure button. With no `depth > 0` anywhere the markup is unchanged.",
        "name": "rows",
        "required": true,
        "type": "RangeTimelineRow[]"
      },
      {
        "description": "Optional current unit marker.",
        "name": "today",
        "type": "number | null"
      },
      {
        "description": "Committed endpoint movement in units. Omit for read-only. Dates remain consumer data; do not replace true anchors with clipped positions.",
        "name": "onRangeChange",
        "type": "(id: string, edge: \"start\" | \"end\", delta: number) => void"
      },
      {
        "defaultValue": "true",
        "description": "Rule the body as a Gantt grid: a line between every row (label column and track), a line under the header and between band and tick rows, and a vertical line per column down the whole body, exactly under its header column edge for unequal `units` too. ON BY DEFAULT, like `Calendar bordered`. The lines are one decorative layer (aria-hidden, pointer-events none) behind the bars. TWO TIERS, and they are different knobs: the INSIDE grid is `--range-timeline-grid-color`, default `hsl(var(--border))` (L* 93.80 / 1.149:1 on the card in light, L* 20.76 / 1.270:1 in dark) at `--range-timeline-grid-width` (hairline); the OUTER FRAME and the label-column divider are `--range-timeline-border-color`, default `var(--border)` — the same tier, so the grid can never read heavier than the box around it or than a DataTable row rule. It was `hsl(var(--input) / 0.5)` (L* 78.67 / 1.738:1) through 27.6.0 and was reported darker than every card and table in the system (gh#730); set `--range-timeline-grid-color: hsl(var(--input) / 0.5)` to get that weight back. `bordered={false}` restores header-only ruling; muted columns still paint.",
        "name": "bordered",
        "type": "boolean"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Width of ONE axis unit — the canonical density vocabulary, the same three steps `DataTable density` takes. `default` is the shipped 56px/day, so no existing Gantt moves; `compact` is 42px/day (a 31-day axis needs 1558px instead of 1992px — measured, the widest two-digit day label across ja/en/ar/fa/hi/th/bn numbering systems is 19.86px in a 24px content box, so the tick still fits); `comfortable` is 70px/day. It moves the COLUMN WIDTH ONLY — row height, bar height and the label column are identical at every step, so a compact Gantt is the same schedule with more days on screen, not a smaller one. Re-points `--range-timeline-unit-width` on the element; retune a step with `--range-timeline-unit-width-{compact,default,comfortable}`. Unrelated to `PageContainer density` / `AppProvider density`, which rescale the whole page through `--scaling`.",
        "name": "density",
        "type": "\"compact\" | \"default\" | \"comfortable\""
      },
      {
        "description": "Controlled ids of the EXPANDED parent rows (same spelling as `Tree expandedValues`). A folded parent removes every descendant row — label AND bar — from the DOM and the accessibility tree; the body grid, row rules and today marker stay aligned.",
        "name": "expandedValues",
        "type": "readonly string[]"
      },
      {
        "description": "Uncontrolled initial expanded parents. Omitted: every parent starts expanded (unlike `Tree`, whose branches start closed), so adding `depth` never hides a row.",
        "name": "defaultExpandedValues",
        "type": "readonly string[]"
      },
      {
        "description": "Fires with the next expanded parent ids when a disclosure is toggled, for controlled and uncontrolled timelines alike.",
        "name": "onExpandedValuesChange",
        "type": "(values: string[]) => void"
      },
      {
        "defaultValue": "false",
        "description": "antd Table `sticky` — keep the axis header (bands + ticks) on screen while the PAGE scrolls a long schedule. `offsetHeader` is px from the top of the scrolling viewport (the height of a fixed app topbar). The header moves out of the horizontal scroller and follows its scrollLeft; the section becomes `overflow: clip` and the body scroller takes the keyboard tab stop. Like antd, it needs no clipping ancestor between the timeline and the page scroller: `Card` is `overflow: hidden`, so a sticky timeline goes in the page as its own section, not inside a Card. antd's `offsetScroll` / `getContainer` (sticky horizontal scrollbar) are not ported.",
        "name": "sticky",
        "type": "boolean | { offsetHeader?: number }"
      },
      {
        "defaultValue": "false",
        "description": "Let the user resize the label column by dragging a divider on its inline-end edge in the header (gh#1189): role=separator, aria-orientation vertical, aria-valuenow/min/max, ArrowLeft/Right (Shift = 32px step, else 8px), Home/End to min/max, RTL-aware, pointer drag. `true` = min 160 / max 640 px. The width lands on --range-timeline-label-width, so the token stays the default until the user drags.",
        "name": "resizableLabel",
        "type": "boolean | { min?: number; max?: number }"
      },
      {
        "description": "Controlled label-column width in px. Persist it per user from onLabelWidthChange.",
        "name": "labelWidth",
        "type": "number"
      },
      {
        "description": "Uncontrolled initial label-column width in px. Omitted: the token's width.",
        "name": "defaultLabelWidth",
        "type": "number"
      },
      {
        "description": "Fires with the next label-column width (px, clamped) while dragging and on every key step.",
        "name": "onLabelWidthChange",
        "type": "(width: number) => void"
      },
      {
        "defaultValue": "false",
        "description": "A new range is loading while the previous rows stay on screen: dims the body (--range-timeline-busy-body-alpha), draws an indeterminate line under the header (static under prefers-reduced-motion) and sets aria-busy.",
        "name": "busy",
        "type": "boolean"
      },
      {
        "description": "What the plan ghost means on every row, e.g. \"予定\" (the live plan) or \"ベースライン\" (a frozen baseline), used in the spoken overrun / early text (\"ベースラインより3日遅れ\"). A row's own planLabel wins. Unset: the plain \"over / ahead of the plan\".",
        "name": "planLabel",
        "type": "string"
      },
      {
        "description": "Dependency connectors, drawn as elbow lines in one decorative SVG over the body. FS (default) runs from the predecessor's finish to the successor's start; SS start to start, FF finish to finish, SF start to finish; a milestone's endpoint is its diamond. Only visible rows dated at the needed endpoint are connected; a folded endpoint hides the link, an off-range one is clamped to the edge. Row centres are measured after layout, so a wrapped label does not shear a line. `violated` turns the connector destructive and puts a focusable 24px marker on its head (named \"Dependency violated: {label}\", with a tooltip), and lists the link for screen readers. Links are data: there is no drawing gesture.",
        "name": "links",
        "type": "{ id: string; from: string; to: string; type?: \"FS\" | \"SS\" | \"FF\" | \"SF\"; violated?: boolean; label?: string }[]"
      }
    ],
    "rules": [],
    "storyPath": "data-display/timeline.tsx",
    "tagline": "Horizontal intervals with token-owned geometry and optional endpoint editing.",
    "tier": "lab",
    "usage": [
      "Provide a precise non-drag editor in each row label when enabling changes. Clipped endpoints and short intervals omit grips; labels and their editors remain available.",
      "An interval wholly outside the columns shows a localized direction indicator at that edge of its row: a chevron plus \"before\" at the inline-start edge, \"after\" plus a chevron at the inline-end edge; the chevrons mirror under RTL.",
      "Mark non-working columns with `columns[].muted` rather than tinting cells yourself; retint the grid through `--range-timeline-grid-color` / `--range-timeline-muted-column-background`, never page CSS.",
      "Need more days on one screen? Pass `density=\"compact\"` — do NOT declare `--range-timeline-unit-width` in an app stylesheet. Tick and band labels are CENTRED on their column by the component; a band clipped by the range (a month the axis starts inside) centres its label on the VISIBLE part, since that is the box the band owns.",
      "Use TimelineGrid for time-of-day columns; RangeTimeline is a horizontal range axis.",
      "Nested Gantt (parent/child work items): pass the server's depth-first rows with `depth` on each (`{ id, label, start, end, startLabel, endLabel, depth }`), and fold with `expandedValues` / `onExpandedValuesChange` (or `defaultExpandedValues`). Never fake an indent inside `label` — the component owns the indent, the disclosure button (named from `rangeTimeline.childRows` + the row label, `aria-expanded`) and the `list` / `listitem` + `aria-level` / `aria-setsize` / `aria-posinset` structure."
    ]
  },
  {
    "example": "import { PageContainer, Flex } from \"@godxjp/ui/layout\";\nimport { Button } from \"@godxjp/ui/general\";\n\nexport default function OrdersPage() {\n  return (\n    <PageContainer\n      title=\"注文一覧\"\n      subtitle=\"直近30日間の受注データ\"\n      breadcrumb={[{ label: \"ホーム\", to: \"/\" }, { label: \"注文一覧\" }]}\n      extra={<Button>新規注文</Button>}\n    >\n      <Flex direction=\"col\" gap=\"lg\">{/* page content */}</Flex>\n    </PageContainer>\n  );\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "PageContainer",
    "props": [
      {
        "description": "Instance footer inset; omitted preserves the theme.",
        "name": "footerPad",
        "type": "PadProp"
      },
      {
        "description": "Instance toolbar inset; omitted preserves the theme.",
        "name": "toolbarPad",
        "type": "PadProp"
      },
      {
        "description": "Tên khả truy cập của landmark <nav> breadcrumb. Mặc định là chuỗi Breadcrumb đã dịch.",
        "name": "breadcrumbLabel",
        "type": "string"
      },
      {
        "description": "Page heading rendered as <h1>. OPTIONAL when the content draws its own heading (a document reader, LegalDocumentShell): omitted, no <h1> is rendered and a header with no other slot is dropped. The page must then still have exactly one <h1> from its content; `titleRef` has nothing to attach to.",
        "name": "title",
        "type": "string"
      },
      {
        "description": "Secondary line beneath the title.",
        "name": "subtitle",
        "type": "string"
      },
      {
        "description": "Status/meta band beside the title (StatusBadge, environment tag, \"updated …\" meta). Sits on the title line at the token-owned --page-header-status-gap and wraps UNDER the title on compact viewports. Part of the canonical page-header contract — never hand-lay a badge next to the <h1>.",
        "name": "status",
        "type": "ReactNode"
      },
      {
        "description": "Action buttons / controls rendered at the END of the title row. A bare node is the whole slot and stays the shape every page already passes — it lands in `start`. `{ start, end }` splits it in two so a page can put something AFTER the actions: the record-screen convention is identity → actions → PAGER LAST, and with one slot the pager had to live in a second header band inside the body, so an app carried two header shapes (gh#734). Both sub-slots are direct children of the one header-extra box in source order, so DOM order IS reading order and the accessibility tree follows; unused, nothing renders at all. The union is `TabsExtraProp`'s on purpose — `Tabs.extra` already accepts exactly this for exactly this reason, and the names are logical (`start`/`end`, never left/right), so they swap sides under dir=\"rtl\".",
        "name": "extra",
        "type": "ReactNode | { start?: ReactNode; end?: ReactNode }"
      },
      {
        "description": "FIXED chrome band between the page header and the body — a filter strip, a status bar, a \"channel workflow\" rail. It is a SIBLING of the body, not content inside it: with `fill` the body is the scroll viewport, so the band is `flex: none` OUTSIDE the scroller and never scrolls away or gets slid under. Shares the page gutters and the `measure` cap with the header and the body (the three bands line up on both edges), goes full-bleed under variant=\"flush\" (wrap padded strips in PageContainer.Inset), and renders NOTHING when omitted — no wrapper, no gap. Its BOX sits FLUSH against the header above and the body below (chrome is attached, not a third page section floating between two voids, so the band cancels the container gap from itself), and by default its inset gives the band's CONTENT that gap back: a plain filter strip (a Segmented, a row of filters) sits one band gap from the header and from the body (gh#1323 — it used to touch both, 0px). A theme that paints or rules the band sets a tighter inset with `--page-toolbar-pad-block`.",
        "name": "toolbar",
        "type": "ReactNode"
      },
      {
        "description": "Content area pinned below the page body. The BAND (its top rule and, with `stickyFooter`, its background) always spans the page; its CONTENT is laid out in the same column as the header and body (gh#682). With a cap — `measure=\"narrow\" | \"medium\"` or a service-wide `--app-shell-page-max-width` inside AppShell — an end-aligned Save/Cancel bar ends on the body's end edge and a full-row composer (`<Flex grow>`) spans exactly the body's content column; with no cap it is unchanged. The footer's children stay its direct children (no wrapper element), and an instance `footerPad` end inset composes with the cap. Never add a call-site `max-w-*` to line it up.",
        "name": "footer",
        "type": "ReactNode"
      },
      {
        "description": "The page sections. Every direct child of the body is spaced from the previous one by --page-body-gap (the section step): drop your Cards straight in — do NOT wrap them in a Flex to space them, do NOT add gap-*/mt-*. Group items INSIDE a section with <Flex direction=\"col\" gap> or <ResponsiveGrid>.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Ordered trail of { label, to?, ellipsis? } segments above the title. `ellipsis: true` on a segment keeps the trail on one line and cuts that label with an ellipsis, the full label in a tooltip on hover and on keyboard focus of the crumb link — same contract as `Breadcrumb` items.",
        "name": "breadcrumb",
        "type": "BreadcrumbItemProp[]"
      },
      {
        "description": "Override the breadcrumb nav landmark's accessible name (defaults to a localized \"Breadcrumb\"). Required when more than one PageContainer (each with its own breadcrumb) renders on the same page/view — two nav landmarks sharing one name/role fail landmark-unique.",
        "name": "breadcrumbAriaLabel",
        "type": "string"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Page shell layout; flush removes padding for full-bleed content.",
        "name": "variant",
        "type": "\"default\" | \"narrow\" | \"flush\" | \"ghost\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Spacing density across the page subtree.",
        "name": "density",
        "type": "\"compact\" | \"default\" | \"comfortable\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Whole-page semantic composition. \"admin-collection\" owns header-to-toolbar rhythm, collection search measure, control height and table density for the subtree through themeable tokens.",
        "name": "preset",
        "type": "\"default\" | \"admin-collection\""
      },
      {
        "defaultValue": "\"stack\"",
        "description": "How the title band and `extra` share the header row BELOW the 640px step. \"stack\" (default) drops `extra` onto its own full-width line under the subtitle. \"responsive-inline\" keeps it beside the title at the token-owned --page-header-extra-measure (11rem) and lets the title/subtitle wrap — use it for ONE compact control (a search field, a single primary action) that must stay on the title row at 390px. At >=640px the two arrangements are identical.",
        "name": "headerLayout",
        "type": "\"stack\" | \"responsive-inline\""
      },
      {
        "defaultValue": "\"document\"",
        "description": "What the page's top row IS, which decides the `<h1>`'s type step. \"document\" (default) = the row is the page's TITLE (a record, a form, a collection, a report): --page-title-font-size (h1, 20px) with the existing responsive step down at 720px; no attribute is emitted, so an existing page is byte-identical. \"chrome\" = the row is the surface's own furniture — a chat channel name, a mail subject line, an IDE tab — naming the thing the user is already INSIDE instead of announcing a document; the h1 takes --page-title-font-size-chrome (--heading-h3 = the 14px body step) at EVERY width, including below 720px where the document-scale step would otherwise pull it back UP. The heading stays an `<h1>` either way — this moves the type step only, never the element, so the screen-reader outline is untouched.",
        "name": "headerScale",
        "type": "\"document\" | \"chrome\" | \"display\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Bounded page MEASURE shared by the header AND the body — a third axis, ORTHOGONAL to `variant` (chrome) and `headerLayout` (arrangement). \"narrow\" (--page-measure-narrow, 42rem outer → 624px visible surface) and \"medium\" (--page-measure-medium, 48rem outer → 720px visible surface) cap BOTH bands, so a header `extra` action ends flush with the body surface instead of stranded at the page edge — unlike variant=\"narrow\", which caps only the body. The package-owned page gutters sit INSIDE the cap, and it is a max, so a 390px viewport stays fluid (358px surface at the 16px compact gutter). The footer BAND is intentionally not capped (its border/background is page chrome when `stickyFooter` pins it), but its CONTENT follows the same measure (gh#682): an end-aligned footer action ends flush with the body too, instead of stranded at the band edge. Inside AppShell the page is fluid by default; a service-wide `--app-shell-page-max-width` caps the same header/toolbar/body bands and the footer CONTENT (never the footer band), and `measure` overrides it on the page that sets it.",
        "name": "measure",
        "type": "\"default\" | \"narrow\" | \"medium\""
      },
      {
        "defaultValue": "false",
        "description": "Pin footer to viewport bottom on scroll — pairs with variant=\"narrow\". While the pinned footer is VISIBLE (always, or once revealed under `footerReveal=\"onScroll\"`), it publishes its block size as `--toast-sticky-bar-height` on `<html>` and the `Toaster` stack rises above it, so a toast never covers the Save/Submit button that raised it (gh#1271). Cleared on unmount or while a reveal footer is parked below the fold.",
        "name": "stickyFooter",
        "type": "boolean"
      },
      {
        "defaultValue": "\"always\"",
        "description": "When the footer is sticky, control WHEN it shows. \"always\" keeps it pinned the whole time; \"onScroll\" hides it until the header scrolls out of view then slides it up — the standard edit/create save bar. Stays mounted (no reflow → no jitter).",
        "name": "footerReveal",
        "type": "\"always\" | \"onScroll\""
      },
      {
        "defaultValue": "false",
        "description": "Grow the body to fill the remaining shell height. Default false = top-packed, content-height (short pages leave no stretched void). Enable for a full-height DataTable, SplitPane, or a chat surface.",
        "name": "fill",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Skeletonise the TITLE BAND only while the page's record resolves (title/subtitle placeholders + aria-busy; the <h1> stays in the outline with an sr-only accessible name). Breadcrumbs and `extra` stay live — they come from the route, not the record. This is not a page-wide loading flag; use DataState for the body.",
        "name": "headerLoading",
        "type": "boolean"
      },
      {
        "description": "The page's icon (an emoji or glyph) drawn large above the title, riding over a banner `cover`'s bottom edge — Notion's page icon (gh#1160). Add aria-hidden unless it is itself a control.",
        "name": "icon",
        "type": "ReactNode"
      },
      {
        "description": "A PageCover: the banner variant spans the page edge to edge above the header; the eyecatch variant sits in the reading column above the title (gh#1160).",
        "name": "cover",
        "type": "ReactNode (a PageCover)"
      },
      {
        "description": "Ref to the page's <h1>; passing it also makes the heading programmatically focusable (tabIndex -1, outside the tab order, no focus ring) so a router can focus the new page title after client-side navigation (gh#1135).",
        "name": "titleRef",
        "type": "React.Ref<HTMLHeadingElement>"
      },
      {
        "description": "Link component used for breadcrumb / header links (e.g. an Inertia or React Router `Link`). Defaults to a native `<a>`.",
        "name": "linkComponent",
        "type": "React.ElementType"
      }
    ],
    "related": [
      "PageContainer.Inset — use INSIDE a `variant='flush'` PageContainer to re-introduce horizontal padding for strips like Toolbar or intro text that should align with the page header, while the surrounding DataTable stays full-bleed. Not a standalone page shell.",
      "PageContainer — always use PageContainer for new pages; it supports `children`, `toolbar`, `footer`, `variant`, `density`, `stickyFooter`, and `fill`. Legacy code using the old prop names (`description` → `subtitle`, `actions` → `extra`) should be migrated to PageContainer.",
      "AppShell — the outer shell that owns the sidebar/topbar layout grid; PageContainer lives inside AppShell's `children` slot. Do not put AppShell inside PageContainer — the nesting order is AppShell → PageContainer.",
      "SplitPane — use instead of PageContainer when the page body needs a fixed-width aside panel alongside main content (e.g. a detail drawer next to a list). PageContainer has no aside slot; SplitPane fills that gap and can itself be placed inside PageContainer's children."
    ],
    "rules": [
      23
    ],
    "storyPath": "layout/page-container.tsx",
    "tagline": "Mandatory page shell — EVERY page wraps its content in PageContainer (title/subtitle/extra/footer/breadcrumb).",
    "usage": [
      "DO keep at most 2 direct actions plus ONE overflow `DropdownMenu` in `extra` when the page is used on a phone (ADR C06, compact = below 40rem): `extra={<><Button variant=\"outline\">Share</Button><Button>Edit</Button><DropdownMenu><DropdownMenuTrigger asChild><Button variant=\"outline\" size=\"icon\" aria-label={t(\"more\")}><MoreHorizontal /></Button></DropdownMenuTrigger>…</DropdownMenu></>}`. A favourite toggle, a copy-link and the rest go in the menu. The header wraps `extra` under the title but never prioritises it for you.",
      "DO: Always wrap every page's content in PageContainer — it is the mandatory page shell. Pass `title` (required, rendered as `<h1>`) for every page; omitting it leaves the page without an accessible heading.",
      "CANONICAL PAGE-HEADER CONTRACT: PageContainer's embedded header IS the DXS `PageHeader` — there is deliberately NO separate PageHeader export, so the page header cannot be re-created or nested. It owns breadcrumbs (`breadcrumb`), title (`title`), subtitle/description (`subtitle`), status/meta (`status`), actions (`extra`) and responsive overflow (`headerLayout` + `measure`). Loading/error/denied are compositions of siblings, never hand-rolls: skeleton `title`/`subtitle` content or `SkeletonDetail` body while a detail loads; `ErrorSurface` (from @godxjp/ui/layout) REPLACES the page for denied (403) / not-found (404) / failed (5xx) whole-page states; `Alert.QueryError` / `DataState` own an in-body query failure.",
      "DO: Use the `extra` prop (not a sibling div, not a wrapper) for action buttons or controls that sit right of the title row — e.g. `extra={<Button>新規作成</Button>}`. Use the `footer` prop for a pinned action bar below the body (e.g. Save/Cancel on a form page); combine with `stickyFooter` to pin it to the viewport bottom on scroll.",
      "DO: Use `toolbar` for any FIXED strip that belongs between the page header and the page content — a filter/segment bar, a status or connection band, a list's bulk-action rail. DON'T put it in `children` (with `fill` the body is the scroller, so it scrolls out of sight) and NEVER hand-lay it with `position: sticky` / a `top-0 z-10` wrapper at the call site: page chrome is the shell's job, and a sticky strip still lets content flow underneath it — the half-sliced row every hand-rolled version produces. It stays out of the scroll viewport, inherits the page gutters and the `measure` cap so it lines up with the title and the body, and is entirely absent from the DOM when the prop is omitted.",
      "DO: Know the `toolbar` band draws NO bottom rule by default — `--page-toolbar-divider` is unset and falls back to `--page-header-divider` (itself `none`), so a service that opts into the page-header divider gets a consistent band rule in ONE declaration, and `--page-toolbar-divider: none` silences just the band. `variant='ghost'` keeps both quiet unless a theme opts one in explicitly, which it then lets through.",
      "DO: Give the `toolbar` band its own SURFACE from the theme when the design separates it from the page ground — `--page-toolbar-background: hsl(var(--card));` declared once (`:root` or a scoped `[data-tenant]`) paints the whole band, page gutters and `measure` cap included, and stays re-themeable per tenant. It is the `background` shorthand, so a gradient works too. The default is `transparent`, i.e. the band looks exactly as it did before the knob existed (rule #44).",
      "DO: Set `--page-toolbar-pad-block` in the SAME theme declaration that paints or rules the band. The band's box sits FLUSH against the header and the body (chrome is attached — a ruled, painted band adrift in two 16px voids divides nothing); the inset is its breathing room. Unset, it defaults to the page's band gap, so a transparent filter strip sits exactly one band gap from the header and from the body, like any other band (gh#1323). Once the band is painted or ruled it has an inside of its own, and `--page-toolbar-pad-block: var(--space-2)` is where that tighter inset belongs; `0px` restores a strip flush to both edges. The CALL SITE never sets it — a strip padded at the call site pads only the strip, not the band.",
      "DO: Silence the `footer` band's top rule with `--page-footer-divider: none` when the footer content already carries its own frame — a chat composer is a bordered Card, and the shell's full-width rule then lands directly above it as a SECOND line (a pixel diff against a consumer chat design caught a 100%-wide rule at y=701 the design does not have). This is the ONE page-chrome divider whose default is a RULE rather than silence, deliberately: `footer` is the shared slot a form's Save/Cancel bar lands in, where that line separates the actions from the content. Unset is byte-identical to the literal the rule used to hard-code. All three page bands are now one contract: `--page-header-divider` / `--page-toolbar-divider` / `--page-footer-divider`, each read at the CALL SITE with a fallback, none of them a `border-*` utility at the call site.",
      "DON'T: Style the `toolbar` band from the call site. `toolbar={<div className='bg-card py-1.5'>…</div>}` is hand-laid page chrome: the utility paints the STRIP, not the band, so it stops at the content box instead of running the full page width (and full-bleed under `variant='flush'`); it is invisible to per-tenant theming; and it puts geometry the shell owns back into the app. The band's ground, inset and rule are `--page-toolbar-background` / `--page-toolbar-pad-block` / `--page-toolbar-divider` — three theme declarations, zero call-site classes.",
      "DO: Set `headerScale='chrome'` when the page's top row is CHROME rather than a document title — a chat channel header, a mail thread's subject line, an IDE tab, a conversation view. The `<h1>` drops to the body type step (--page-title-font-size-chrome) at every width, so the header band stops eating the height the content needs: a consumer chat page measured a 61px band with a 24px channel name where the design asked for ~40px at the `sm` step. Pair it with `variant='ghost'` for the full quiet chrome header — ghost drops the header's bottom pad and lets no divider inherit in — and with `fill` + `toolbar` + `footer`/`stickyFooter` for the canonical chat surface. The heading stays an `<h1>`: this is a type step, never a heading-level downgrade. The same attribute also drops the page's top padding to `--page-pad-block-start-chrome` (0), so the band sits ON the frame instead of floating in a document's top margin — four consequences of ONE fact (this row is furniture), not four props a call site has to keep in lockstep: the subtitle drops to `--page-subtitle-font-size-chrome` (~11px) so the caption under a channel name stops matching the name's own size, and the `extra` cluster centres on the bar instead of top-packing against a heading that is no longer tall. If a design wants its chrome inset or a different caption step, the theme retunes those tokens once; never pad, negative-margin or `self-center` the page at the call site.",
      "DON'T: Reach for `headerScale='chrome'` just because a title \"looks too big\" on an ordinary document page (a record detail, a form, a collection, a report) — the page title is the document's headline and the h1 step is the system's answer for it; shrinking it there only breaks the type rhythm the rest of the page is measured against. And NEVER override `--page-title-font-size` (or put a `text-sm` / `text-base` utility on the title) at the call site to fake it: that re-themes every page in the subtree, is invisible to the 720px responsive step, and puts page-chrome geometry back in the app. If a service wants a different chrome step, it retunes `--page-title-font-size-chrome` (or `--page-subtitle-font-size-chrome`) once in its theme. Same for the header actions: never hang `self-center` / `items-center` on the node you pass to `extra` to fix an off-centre icon row — that aligns one call site's box while every other chrome page keeps the document's top-packed row.",
      "DO: Use `variant='flush'` when the page body contains a full-bleed component like DataTable. Inside a flush container, wrap any padded strips (Toolbar, intro text) in `<PageContainer.Inset>` to align them with the header. Never add manual `px-*` or `p-*` padding to compensate — use PageContainer.Inset.",
      "DO: Pass `breadcrumb` as an ordered array of `{ label, to? }` objects from root to current page. The last item is automatically rendered without a link and receives `aria-current='page'`; earlier items with `to` become router `<Link>` elements. Never hand-roll a breadcrumb nav inside a PageContainer.",
      "DON'T: Use `density` to change individual control sizes — it cascades spacing across the entire page subtree. Set it once per page (e.g. `density='compact'` for data-dense list pages) and let all child components inherit it. Do not apply density classes manually.",
      "DO: Use `preset='admin-collection'` for canonical Admin list pages. It owns the toolbar/search/control/table composition once at PageContainer level; do not repeat widths, heights, cell padding or media queries on child fields and rows.",
      "DO: Use `subtitle` (not `description`) and `extra` (not `actions`) — those are the canonical page-header names. If you see `description` / `actions` in old code, migrate them.",
      "DO: Pass `extra={{ start: <actions/>, end: <pager/> }}` when the header convention is identity → action cluster → PAGER LAST. The two sub-slots render in that order inside the one header-extra box, so the record-pager band that used to live inside the body moves up and the app stops carrying two header shapes (gh#734). The shape is `Tabs.extra`'s, logical (`start`/`end`), and a bare node still means exactly what it always did.",
      "DON'T: Fake \"after the actions\" by putting the pager in a second header strip inside `children`, or by appending it to the same node you pass to `extra` and spacing it with a utility — the first gives the page two header bands to keep in sync, the second hand-lays the gap the header-extra box already owns.",
      "DO: Leave `fill` off (the default) for ordinary pages — the body is content-height and top-packed, so a short page on a tall viewport leaves no stretched empty void below the content (the page background simply spans the shell). Only set `fill` when the body itself should occupy the full remaining height: a full-height DataTable, a SplitPane, or a chat surface whose message list scrolls and whose composer is pinned to the bottom via `footer` + `stickyFooter`; or a page whose ENTIRE body is a `variant='page'` EmptyState, which then takes that height and centres in it (a zero-state that is the whole page is the one short page that must NOT top-pack — see EmptyState). DON'T add a manual `min-h-screen` / `flex-1` wrapper or a spacer div to fight or fake this.",
      "DO: Reach for `headerLayout=\"responsive-inline\"` when a SINGLE compact header control (a member search, one primary action) must stay beside the title at 390px instead of wrapping under the subtitle. Its measure is the token `--page-header-extra-measure` (11rem) — never a consumer `w-[176px]` or a media query in app CSS. Keep the default `stack` when `extra` holds a toolbar of several buttons; squeezing those into the compact measure only makes them wrap in a narrower box.",
      "DO: Know the header draws NO bottom divider by default — it is governed by the semantic token `--page-header-divider` (default `none`). A service theme opts in once, globally, with `--page-header-divider: 1px solid hsl(var(--border));` in its theme CSS. Never re-create the divider with a `border-b` utility on the header or a `<Separator>` under the title. `variant='ghost'` does NOT overrule the token: it blocks a divider from INHERITING in (so an unset token stays silent) but an explicit `--page-header-divider` still draws on a ghost page — the same shape as `--page-toolbar-divider` on the band. Ghost's real quiet half is the header's bottom pad, which it drops.",
      "DO: Bound a readable/feed page with `measure=\"medium\"` (720px visible surface) or `measure=\"narrow\"` (624px) — NEVER a page-local `max-w-[720px]`, a wrapper div, or a consumer CSS variable override. `measure` caps the HEADER and the BODY together, which is the whole point: with `variant=\"narrow\"` the header action stays out at the page edge while the body is 624px, so the action and the card do not share an end edge. Retune the presets once in a service theme via `--page-measure-narrow` / `--page-measure-medium`.",
      "DO: Compose the axes — `variant=\"ghost\" measure=\"medium\" headerLayout=\"responsive-inline\"` is the canonical quiet notification/inbox feed: ghost owns the quiet header rhythm (no divider, no header bottom pad, tighter title→body gap), `measure` owns the shared 720px measure, `headerLayout` keeps one compact control on the title row at 390px. They are independent props precisely so chrome and measure are no longer one variant axis. DON'T stack `variant=\"narrow\"` on top of `measure` — the measure rule simply wins on the body (verified in Chromium: variant=\"narrow\" + measure=\"medium\" resolves the body to 768px, not the intersection), so the `variant=\"narrow\"` is dead weight that only misleads the next reader. `variant=\"narrow\"` is the legacy body-only cap; `measure=\"narrow\"` is the same 624px surface with the header included."
    ],
    "useCases": [
      "A master list page (e.g. invoices, journal entries, customers) where the header holds the page title, a 'New Invoice' button in `extra`, a breadcrumb trail, and a full-bleed DataTable as the body — use `variant='flush'` + `<PageContainer.Inset>` for the Toolbar above the table.",
      "A detail / edit form page where the footer holds Save and Cancel buttons — use `footer={<Flex direction='row' justify='between' fill><Button variant='outline'>削除</Button><Button>保存</Button></Flex>}` with `stickyFooter` + `footerReveal='onScroll'` so the save bar slides up only once the header (and its actions) scroll out of view — the canonical edit/create pattern.",
      "A settings or narrow-form page (e.g. account profile, entity configuration) where `variant='narrow'` constrains content to a readable column width and `stickyFooter` pins the submit bar.",
      "A dashboard page with KPI cards and chart sections — use `variant='default'` with `children={<Flex direction='col' gap='lg'>…</Flex>}` to vertically stack multiple Card/StatCard sections beneath the page title.",
      "Any deep-nav page in a multi-level admin (e.g. Accounting > Ledger > Journal Entry #42) where a 3-segment breadcrumb trail provides back-navigation without browser history dependence.",
      "A high-density data reconciliation page where an analyst needs to see maximum rows — use `density='compact'` to tighten all spacing across the DataTable, Toolbar, and controls in a single prop.",
      "A chat / messaging detail page where the message list should scroll inside the page and the composer stays pinned at the bottom — use `fill` so the body occupies the full shell height, with `footer={<ChatComposer />}` (@godxjp/chat) + `stickyFooter`. Without `fill` the page would top-pack and the composer would float mid-screen on a tall viewport.",
      "A Slack-like chat channel, a mail thread, or an IDE-style tab view whose top row is the SURFACE's name rather than a document title — `headerScale='chrome'` (usually with `variant='ghost'`) puts the `<h1>` on the body type step so the header reads as a channel label and the band collapses to roughly the height of one control row, leaving the vertical space to the conversation.",
      "A chat channel page where a fixed band (channel workflow / pinned-message / connection status) must sit between the page header and the scrolling transcript — `toolbar={<Toolbar>…</Toolbar>}` with `fill` + `footer={<ChatComposer />}` (@godxjp/chat) + `stickyFooter`. The band is outside the scroller, so the transcript never travels under it and the composer stays pinned; a collection page uses the same slot for its filter strip above a full-bleed DataTable (`variant='flush'`)."
    ]
  },
  {
    "docPath": "docs/layout/page-cover.tsx",
    "example": "import { PageContainer } from \"@godxjp/ui/layout\";\nimport { PageCover } from \"@godxjp/ui/lab\";\nimport { Button } from \"@godxjp/ui/general\";\n\n<PageContainer\n  title={page.title}\n  headerScale=\"display\"\n  icon={<span aria-hidden=\"true\">{page.icon}</span>}\n  cover={\n    <PageCover\n      src={page.cover}\n      alt=\"\"\n      positionY={y}\n      onPositionChange={setY}\n      repositioning={moving}\n      onRepositioningChange={setMoving}\n      actions={<Button size=\"sm\" variant=\"secondary\" onClick={() => setMoving(true)}>Reposition</Button>}\n    />\n  }\n>\n  {body}\n</PageContainer>",
    "group": "layout",
    "importPath": "@godxjp/ui/lab",
    "name": "PageCover",
    "props": [
      {
        "description": "Image URL.",
        "name": "src",
        "required": true,
        "type": "string"
      },
      {
        "description": "Alternative text; \"\" for a decorative cover.",
        "name": "alt",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "50",
        "description": "Vertical focal point, 0 top – 100 bottom.",
        "name": "positionY",
        "type": "number"
      },
      {
        "description": "New focal point while repositioning.",
        "name": "onPositionChange",
        "type": "(positionY: number) => void"
      },
      {
        "description": "Reposition mode (controlled): the image becomes a slider.",
        "name": "repositioning",
        "type": "boolean"
      },
      {
        "description": "Enter / Escape ended the mode.",
        "name": "onRepositioningChange",
        "type": "(repositioning: boolean) => void"
      },
      {
        "defaultValue": "\"banner\"",
        "description": "Banner above the header, or eyecatch in the reading column.",
        "name": "variant",
        "type": "\"banner\" | \"eyecatch\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Banner height step; token-owned, scales phone → desktop.",
        "name": "height",
        "type": "\"md\" | \"lg\""
      },
      {
        "description": "Change / reposition / remove controls: over the bottom edge on a wide screen, BELOW the image on a phone (never on the focal area).",
        "name": "actions",
        "type": "ReactNode"
      },
      {
        "description": "Id on the cover frame.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "PageContainer — `cover`, `icon`, headerScale=\"display\" for an article title.",
      "Image — an image that opens a preview."
    ],
    "rules": [],
    "storyPath": "layout/page-cover.tsx",
    "tagline": "A page's cover image (gh#1160): Notion's banner across the top, or note.com's eyecatch (~1.91:1, rounded) in the reading column. Put it in PageContainer `cover`. The focal point is positionY (object-position); while `repositioning` the image IS a vertical WAI-ARIA slider — drag or arrows / PageUp / PageDown / Home / End, Enter / Escape to finish.",
    "tier": "lab",
    "usage": [
      "DO pass it as PageContainer `cover` with the page `icon` — the container places it and overlaps the icon.",
      "DO wire a Reposition button in `actions` that sets `repositioning`, and persist positionY from onPositionChange."
    ]
  },
  {
    "absorbed": [
      "SpaceCompact"
    ],
    "example": "import { Flex } from \"@godxjp/ui/layout\";\nimport { Button } from \"@godxjp/ui/general\";\n\n<Flex direction=\"row\" gap=\"sm\" align=\"center\" justify=\"between\" wrap>\n  <SearchSummary />\n  <Flex direction=\"row\" gap=\"xs\" align=\"center\" wrap>\n    <Button variant=\"outline\">リセット</Button>\n    <Button>適用</Button>\n  </Flex>\n</Flex>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "Flex",
    "props": [
      {
        "defaultValue": "false",
        "description": "Weld the children into one control — antd Space.Compact (v32 folded `SpaceCompact` in here). Each child gets its own flex-item wrapper, inner corner radii are zeroed and the shared border collapses to one hairline; each child keeps its own focus ring, name and `size`. `direction` picks the axis (\"row\" default, \"col\"); `gap`/`wrap`/`pad` and the other layout knobs do not apply (a seam has no gap). Named (aria-label/-labelledby, e.g. by a wrapping FormField) it becomes role=\"group\".",
        "name": "attached",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "`attached` only. The joined row fills its parent's inline size (antd `block`, renamed to match Button.fullWidth). Field children grow to fill it; a Button keeps its content width (gh#1062).",
        "name": "fullWidth",
        "type": "boolean"
      },
      {
        "description": "`attached` only. Scoped control density for the whole joined row (antd Space.Compact `size`). Each child still carries its OWN `size` prop.",
        "name": "density",
        "type": "\"compact\" | \"default\" | \"comfortable\""
      },
      {
        "description": "Floating child actions shown on parent hover/focus-within; always visible on touch.",
        "name": "reveal",
        "type": "\"hover\""
      },
      {
        "description": "Negative inline inset on the token scale.",
        "name": "bleed",
        "type": "GapProp"
      },
      {
        "description": "Lightweight surface, without Card structure.",
        "name": "surface",
        "type": "\"muted\" | \"popover\" | \"warning\""
      },
      {
        "description": "false prevents shrinking.",
        "name": "shrink",
        "type": "boolean"
      },
      {
        "description": "Grow into remaining flex space.",
        "name": "grow",
        "type": "boolean"
      },
      {
        "description": "Semantic element. A list keeps its marker and indentation unless marker=\"none\" says otherwise.",
        "name": "as",
        "type": "\"div\" | \"span\" | \"ul\" | \"ol\" | \"li\""
      },
      {
        "defaultValue": "as=\"ul\" → \"disc\", as=\"ol\" → \"decimal\"",
        "description": "Marker for a LIST element; ignored by every other tag. marker=\"none\" emits NO data-list, so the list keeps its <ul>/<ol>, its <li> semantics and its gap token, and loses the bullet AND the --space-5 indent — that is the container a short entity list needs: <Flex as=\"ul\" marker=\"none\" direction=\"col\" gap=\"none\"> with <ListRow as=\"li\"> rows. Before it, the bullet was unconditional and .ui-flex[data-list] > li { display: list-item } outranked [data-slot=\"list-row\"] { display: flex }, so rows fell out of flex layout (94.58px tall instead of 69.98px) and the only move left was a raw <ul>, which carries no gap token (gh#714). disc/decimal stay available so an <ol> can be bulleted, or a <ul> numbered, without a list-style-type in a className.",
        "name": "marker",
        "type": "\"disc\" | \"decimal\" | \"none\""
      },
      {
        "description": "Responsive axis; omitted steps inherit.",
        "name": "direction",
        "type": "\"row\" | \"col\" | {base?: \"row\" | \"col\"; sm?: \"row\" | \"col\"; md?: \"row\" | \"col\"; lg?: \"row\" | \"col\"; xl?: \"row\" | \"col\"}"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Token gap between children, shared with other layout primitives. \"none\" is a DELIBERATE zero for two lines that read as one block — a name over its role, a weekday over its date, a tab bar with no seam — not a way to opt out of the token scale.",
        "name": "gap",
        "type": "\"none\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\""
      },
      {
        "description": "ESCAPE HATCH: a gap in pixels, off every step of the scale. Real designs land on 2px, 5px, 6px, 10px, 14px — rounding to the nearest named step drifts the layout, and writing the literal in a className is blocked by ui-audit. Spend the ten named steps FIRST: `gap={3}` is 12px and follows the user's --scaling, gapRaw does not. It wins over `gap` (which then emits no class, so the two cannot fight over specificity) and leaves data-gap-raw on the DOM, so every escape stays countable.",
        "name": "gapRaw",
        "type": "number"
      },
      {
        "description": "Inner padding on the token scale — one step for all four sides, or an object keyed by LOGICAL side. Exists because a missing padding prop produced 42 of 51 ui-audit errors in one real consumer (gh#408): `<Flex className=\"p-3\">` was the only move left.",
        "name": "pad",
        "type": "number | { inline?, block?, inlineStart?, inlineEnd?, blockStart?, blockEnd? }"
      },
      {
        "description": "Raw-pixel padding for values off the scale, same contract and same price as gapRaw: it leaves data-pad-raw on the DOM so each escape is countable. Overrides `pad` PER SIDE, not as a whole.",
        "name": "padRaw",
        "type": "number | { inline?, block?, inlineStart?, inlineEnd?, blockStart?, blockEnd? }"
      },
      {
        "defaultValue": "false",
        "description": "Take the space the siblings leave — the ELASTIC column of a `fixed | elastic | fixed` row (name · meter · figures). Also sets min-inline-size: 0, which is what lets a truncating child ellipse instead of pushing the row wider. Without this axis the only move was className=\"flex-1 min-w-0\", which ui-audit blocks (gh#405 §2).",
        "name": "fill",
        "type": "boolean"
      },
      {
        "description": "A FIXED column: number = px, string = any CSS length. Comes with flex: none — a width a sibling can still squeeze is a suggestion, and stacked rows would start at different x offsets. Leaves data-width-raw on the DOM so the measurement stays countable, like gapRaw/padRaw.",
        "name": "width",
        "type": "number | string"
      },
      {
        "description": "CENTRE this box and CAP it at a page measure (--page-measure-narrow 42rem / -medium 48rem / -wide 72rem): margin-inline:auto + inline-size:100% + max-inline-size. THE public answer to \"full-bleed outside, measured column inside\" — a marketing <section> paints edge to edge and its inner Flex measure=\"wide\" holds the content on the page measure. Deliberately NOT on PageContainer: that owns page padding and a header/toolbar/footer scaffold, so a full-bleed page cannot consume it, and the refusal left the centred column owned by nobody — three showcases each hand-wrote the same four declarations (gh#839). It adds NO gutter: the page inset is pad={{ inline: 6 }}, and one band with two padding owners drifts. A MAX, so nothing binds below the cap at 390px. An explicit `width` wins. Do not give a READING column \"wide\" — prose stays narrow/medium, which bracket Bringhurst's 45-75 characters. Omitted emits no attribute at all.",
        "name": "measure",
        "type": "\"narrow\" | \"medium\" | \"wide\""
      },
      {
        "description": "Cross-axis alignment, emitted as a data attribute for the layout CSS.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\""
      },
      {
        "description": "Main-axis distribution, emitted as a data attribute for the layout CSS.",
        "name": "justify",
        "type": "\"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\""
      },
      {
        "defaultValue": "false",
        "description": "Allows children to wrap onto additional flex lines.",
        "name": "wrap",
        "type": "boolean"
      },
      {
        "description": "Drop this region below a canonical breakpoint step (sm 40rem, md 48rem, lg 64rem, xl 80rem). THE public way to make a region responsive without a page-local media query — a public header hides its anchor navigation with hideBelow=\"md\". Omitted by default, so no attribute is emitted and no rule matches. display:none also removes it from the a11y tree, so keep its destinations reachable elsewhere (a footer nav) at that width.",
        "name": "hideBelow",
        "type": "\"sm\" | \"md\" | \"lg\" | \"xl\""
      },
      {
        "description": "Inverse of hideBelow — drop the region FROM that step upwards, i.e. keep it only on the narrow side (a compact-only affordance).",
        "name": "hideFrom",
        "type": "\"sm\" | \"md\" | \"lg\" | \"xl\""
      },
      {
        "description": "ESCAPE HATCH: the same drop, at a width in PIXELS off every step of the scale — the gapRaw/padRaw contract on the breakpoint axis (gh#528). An off-scale breakpoint is a LOCAL EXCEPTION, not a new tier: sm/md/lg/xl are shared with collapseBelow and --master-detail-collapse-below, so two regions asked to fold at the same step really do fold together, while a raw width folds one region only. Spend the four steps first; reach here when the canonical design specifies a width the scale has not got (a nav that becomes a hamburger at 900px). It leaves data-hide-below-raw on the DOM so every escape stays countable, and prints one deduped media rule per distinct width (a media query cannot read a var()). It wins over hideBelow, which then emits nothing.",
        "name": "hideBelowRaw",
        "type": "number"
      },
      {
        "description": "Inverse of hideBelowRaw, same contract and same price (data-hide-from-raw). THE SEAM: hideBelowRaw hides while width < N and hideFromRaw hides while width >= N — the same comparisons as the token steps, so the pair is exact complements and at exactly N the hideBelowRaw region is the visible one. Never hand-roll this with an inclusive max-width: `<= N` paired with `>= N` hides BOTH at exactly N, the 1px hole a consumer measured on a hand-rolled max-[900px]: pair.",
        "name": "hideFromRaw",
        "type": "number"
      }
    ],
    "related": [
      "Flex `direction='col'` — the standard pattern for ordinary vertical block spacing; use explicit `align`, `justify`, or `wrap` props when you need more control.",
      "Flex `direction='row'` — the standard pattern for simple horizontal groups; add `wrap` and `align='center'` for the typical row with wrapped centered items.",
      "ResponsiveGrid — use for equal-width, multi-column tile layouts. Flex arranges children on one flex axis and does not provide column-count behavior.",
      "PageContainer — page scaffold and padding context. Flex is an inner layout primitive used inside page sections, cards, dialogs, and toolbars. PageContainer `measure` bounds an APP page (header and body together, narrow | medium); Flex `measure` bounds ONE centred column inside a full-bleed band, and is the only one that reaches the marketing `wide` step."
    ],
    "rules": [
      2,
      40
    ],
    "storyPath": "layout/flex.tsx",
    "tagline": "Token-spaced flex primitive with explicit direction, alignment, justification, and wrapping controls — and, with `attached`, a row of controls welded into ONE visual unit (antd Space.Compact).",
    "usage": [
      "DO import from `@godxjp/ui/layout` and reach for Flex when the axis, alignment, justification, or wrap behavior is part of the component contract: `import { Flex } from \"@godxjp/ui/layout\"`.",
      "DO keep spacing on the `gap` prop instead of raw `gap-*`, `space-*`, or padding utilities. Flex uses the same token scale as other layout primitives, so spacing remains tied to the design system.",
      "DO use `direction=\"row\"` with `wrap` for responsive control rows, chip clusters, and action groups that need more control than simple row composition.",
      "DO use `direction=\"col\"` for vertical groupings that need explicit `align` or `justify` behavior. For pure vertical stacking without alignment control, `direction=\"col\"` is sufficient.",
      "DON'T override the axis with `className` after choosing a direction prop. Keep the layout intent in props so catalog guidance and data attributes stay accurate.",
      "FULL-BLEED BAND: a marketing/landing section is a `<section>` carrying the edge-to-edge paint (background, border, halo) around ONE `<Flex direction=\"col\" measure=\"wide\" pad={{ inline: 6, block: 20 }}>`. The measure centres and caps the column, `pad` owns the gutter and the band rhythm (`block: 20` = 80px, `block: 24` = 96px — the two band steps). Never a page-local `max-width: 1200px` + `margin-inline: auto` class, and never an inline style saying the same thing: that four-declaration idiom is what this prop replaced (gh#839).",
      "Flex is a plain div with React.HTMLAttributes<HTMLDivElement>; pass `id`, `role`, `aria-*`, `data-*`, and structural className values as needed, but do not use it as a semantic form or button wrapper. When the parent only accepts phrasing content — a TabsTrigger, PopoverTrigger or Button, all of which render a <button> — pass `as=\"span\"` rather than reaching for a raw `<span className=\"flex …\">`.",
      "SEMANTIC LIST: a list of rows is `<Flex as=\"ul\" marker=\"none\" direction=\"col\" gap=\"none\">` with `<ListRow as=\"li\">` children — never a raw `<ul>` (no gap token), never `<div role=\"list\">` + `<div role=\"listitem\">` (ARIA re-describing markup HTML already has), and never a wrapper around each row: the divider is `:not(:last-child)` among SIBLINGS, so a row alone in its own wrapper loses it silently. `ui-audit` flags all three as `no-hand-rolled-list`.",
      "NAMED FLEX = GROUP: a role-less div may not carry a naming attribute (axe aria-allowed-attr), so a Flex given `aria-label`/`aria-labelledby` — e.g. by FormField wrapping a composite range/年月 field — automatically renders `role='group'`, folds `aria-errormessage` into `aria-describedby`, and drops the widget-only `aria-required`/`aria-invalid`. Passing an explicit `role` opts out of all of this and the caller owns the attribute set.",
      "ATTACHED: DO reach for it when two or more controls must read as ONE field — a number + a unit (\"毎 [2] [週 ▾] ごと\"), a currency Select welded to an amount Input, a search field with an attached filter Select. `ResponsiveGrid columns={2}` puts them in separate cells (which collapses to ONE column on a narrow form card); `Flex attached` keeps them fused on one line regardless of the grid.",
      "ATTACHED: ONE label for the pair vs per-control: wrap the WHOLE `Flex attached` in a single `FormField label=\"…\"` when the pair reads as one field (\"繰り返し間隔\" over a number+unit row) — a role-less `<div>` cannot carry a cloned `aria-label`/`aria-labelledby` (axe `aria-allowed-attr`), so a NAMED `Flex attached` with no explicit `role` promotes itself to `role=\"group\"` automatically, the same contract `Flex` already honours for a range/年月 pair. Give each CHILD its own `aria-label` instead only when the two controls are independently meaningful outside the row (rare — prefer the one-label pattern for a welded control).",
      "ATTACHED: DON'T reach for `className=\"rounded-none\"` / `[&>*:not(:first-child)]` utilities to join controls yourself — `ui-audit`'s `no-utility-layout` rule blocks exactly that, and it is precisely the corner-radius seam this component owns.",
      "ATTACHED: DO set `size` on each CHILD individually, not on `Flex attached` — the row has no size prop of its own by design (see `density`); every control already owns its own `size` axis (`xs|sm|md|lg`).",
      "ATTACHED: DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
      "ATTACHED: `fullWidth` is antd `Space.Compact block` (gh#1062): in a row, a FIELD child (Input and its affix/Password/Search forms, NumberInput, Textarea, TagInput, the Date/Time pickers, and the Select/Cascader/TreeSelect/SearchSelect trigger) grows to fill the row, and every other child — a Button, an icon button — keeps its content width. So `[Input][+ Add]` is a full-width field with its submit at content width, and two fields (NumberInput + Select) share the row evenly between them. Deviation from antd, written down: antd grows only its Input family (`width: 100%`); Select/InputNumber/DatePicker keep their own width there. Here every field grows, so two fields never leave an empty tail. `direction=\"col\"` + `fullWidth` stacks full-width children as before.",
      "ATTACHED: DON'T expect corner-radius welding on `direction=\"col\"` yet — the shared border still collapses, but each child keeps all four of its own corners rounded until a block-axis radius knob exists on the Input/trigger families (documented gap, not a silent one)."
    ],
    "useCases": [
      "Toolbar internals where controls should sit in a row, wrap on narrow widths, and stay vertically centered.",
      "Card headers that need title content on the left and actions on the right via `justify='between'` without hand-rolling flex utility classes.",
      "Empty-state or loading blocks that center content on both axes using `align='center'` and `justify='center'`.",
      "Form sub-sections where a vertical group needs stretched children or centered helper content beyond what a plain column Flex provides.",
      "Badge, chip, or tag clusters where wrapping is required but the caller also needs explicit gap control.",
      "Low-level layout composition inside custom components where raw flex classes would duplicate the primitive.",
      "ATTACHED: a recurring-issue interval row, \"毎 [NumberInput] [Select 週/月/日 ▾] ごと\", fused on one line inside a FormField, inside a 2-column ResponsiveGrid form card that collapses to one column.",
      "ATTACHED: A currency amount: [Select 通貨 ▾][NumberInput 金額] welded so the currency reads as part of the amount field, not a separate control beside it.",
      "ATTACHED: A filtered search bar: [Select scope][SearchInput query] as one visual field, `fullWidth` inside a page toolbar."
    ]
  },
  {
    "example": "import { ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { StatCard } from \"@godxjp/ui/data-display\";\n\n<ResponsiveGrid columns={4}>\n  <StatCard label=\"総会員数\" value=\"12,400\" />\n  <StatCard label=\"公開中クーポン\" value=\"8\" />\n  <StatCard label=\"月間利用数\" value=\"3,210\" />\n  <StatCard label=\"割引総額\" value=\"¥480,000\" />\n</ResponsiveGrid>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "ResponsiveGrid",
    "props": [
      {
        "description": "Measured inset escape; stamped data-pad-raw.",
        "name": "padRaw",
        "type": "PadRawProp"
      },
      {
        "description": "Token inset, scalar or logical sides.",
        "name": "pad",
        "type": "PadProp"
      },
      {
        "defaultValue": "\"rows\"",
        "description": "Rows adapt to container width. Columns keep a horizontal collection in order with token-owned column width; compose inside ScrollArea orientation=\"horizontal\". Column flow ignores columns and preset geometry.",
        "name": "flow",
        "type": "\"rows\" | \"columns\""
      },
      {
        "description": "antd `Row align`, spelled with Flex's FlexAlignProp values (antd `top` → \"start\", `middle` → \"center\"). \"center\" puts cells of different heights on one centre line (a condition row's number and ＋/− buttons beside taller controls). \"stretch\" makes every cell as tall as the tallest one. Omitted, flow=\"columns\" keeps \"start\" and flow=\"rows\" keeps the grid's natural stretch.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"stretch\""
      },
      {
        "defaultValue": "4",
        "description": "Numeric desktop target, or container column counts. Base defaults to 1; omitted object steps inherit upward. Use { base: 2, sm: 4 } for compact metric tiles.",
        "name": "columns",
        "type": "number | { base?: number; sm?: number; md?: number; lg?: number }"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Token gap between cells, the same steps as Flex. \"none\" is a DELIBERATE zero for tiles that must read as one continuous surface (a segmented bar, a seamless tile strip) — not a way to opt out of the token scale.",
        "name": "gap",
        "type": "\"none\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\""
      },
      {
        "defaultValue": "false",
        "description": "Dense packing (grid-auto-flow: row dense): later small cells back-fill the holes a wide or tall cell leaves — a widget board of 1×1 / 2×1 / 1×2 cells (gh#1175). Visual order only; DOM and focus order stay as written, so use it for independent widgets/tiles, not reading order. Ignored under flow=\"columns\".",
        "name": "dense",
        "type": "boolean"
      },
      {
        "description": "Grid items — typically Card or StatCard.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      }
    ],
    "related": [
      "Flex — use Flex (direction col or row) for sequential blocks of mixed-width content (forms, description lists, button rows). Use ResponsiveGrid only when you want equal-width, auto-reflowing tile columns.",
      "SplitPane — a main column beside a FIXED-rem aside that splits at the pane's own 48rem and stacks below it (a user-draggable ratio is ResizablePanel). Use ResponsiveGrid when you want automatic column count collapse on narrow screens.",
      "StatCard — the canonical direct child of ResponsiveGrid for KPI tiles. StatCard is self-contained (draws its own bordered card); never wrap it in Card/CardContent when placing it inside ResponsiveGrid.",
      "SkeletonStat — the loading-state sibling of StatCard, used as a drop-in placeholder child of ResponsiveGrid with the same columns count while KPI data is in flight."
    ],
    "rules": [
      24,
      40
    ],
    "storyPath": "layout/responsive-grid.tsx",
    "tagline": "Container-responsive card grid with configurable base, sm, md and lg columns.",
    "usage": [
      "ResponsiveGrid.Item span={2} owns a responsive column span; an object {base:1,lg:2} sets explicit steps, clamped to the parent columns.",
      "ResponsiveGrid.Item rowSpan={2} spans two rows — the tall (1×2) widget (gh#1175). Same shape as span: a number applies from sm up and is 1 at base (the single stacked column), an object sets each step. Pair with `dense` so the cells after it fill the space beside it.",
      "KANBAN LANES: <ScrollArea orientation=\"horizontal\"><ResponsiveGrid flow=\"columns\" align=\"stretch\">{lanes.map(lane => <Card onDragOver onDrop>…<Card draggable>…</Card></Card>)}</ResponsiveGrid></ScrollArea>. align=\"stretch\" keeps an empty lane as tall as the fullest one, so it stays a full-height drop target; a Select inside a draggable card does not enlarge the drag image.",
      "DO place StatCard tiles directly as immediate children — StatCard IS already a bordered card; never wrap it in an extra <Card><CardContent>. The canonical pattern is <ResponsiveGrid columns={4}><StatCard .../><StatCard .../></ResponsiveGrid>.",
      "DO use columns={2|3|4} to declare the target desktop column count — the grid collapses automatically to 1 column on narrow containers (mobile-first via CSS container queries), via 2-column intermediate at ≥640px, then full target count at ≥1024px. Use columns={{ base: 2, sm: 4 }} for two mobile columns and four wider-container columns; no consumer CSS is needed.",
      "DO NOT place a DataTable inside a ResponsiveGrid column beside a card or chart. DataTable must occupy its own full-width row in a Card with CardContent flush. Nesting a multi-column table in a grid column squeezes CJK text to one character per line (see rule 37).",
      "DO use ResponsiveGrid for page-level spacing — it applies the correct gap token (--space-stack-md) automatically. Never add raw gap-* / p-* / space-* utilities to the page layout around tiles; compose spacing through this component instead (rule 40).",
      "DO render SkeletonStat children in place of StatCard tiles while KPIs are loading — same columns prop, same count as the real tiles. Switch to real StatCard once data resolves.",
      "The grid uses CSS container queries, not viewport media queries — it responds to its containing block width, not the window. Ensure the container is not artificially constrained (e.g. inside a narrow SplitPane column) or column expansion will never trigger.",
      "THE STEP NAMES ARE CONTAINER WIDTHS, AND THEY DO NOT LINE UP WITH VIEWPORT NAMES — `sm` is 40rem, `md` 48rem, `lg` 64rem OF THE GRID'S OWN BOX. A page gutter eats the difference: inside a 1280px shell with 32px gutters a 768px tablet hands the grid 704px, so `columns={{ base: 2, md: 3 }}` stays 2-up there — measured. Key the tablet column to `sm` instead (`{ base: 2, sm: 3, lg: 4 }`) and verify by measuring the GRID, not the window. Never reach for a viewport media query to 'correct' it.",
      "An UNNAMED `@container` in your own CSS resolves against the nearest ANCESTOR container, which inside a grid is the grid — not the card. A rule meant to fire per card measured against the 358px grid instead of the 163px cell. Either make the card its own container (`container-type: inline-size`) or use a plain media query when the viewport is genuinely what you meant."
    ],
    "useCases": [
      "Dashboard KPI row: rendering 3–4 StatCard tiles (revenue, member count, active invoices, overdue amount) that reflow to a 2-column stacked grid on tablet and a single column on mobile.",
      "Summary header above a list page: a 2-column grid of two StatCard totals (e.g. total payable vs total paid) sitting above a Toolbar and DataTable.",
      "Accounting period overview: 4 StatCard tiles (opening balance, total debits, total credits, closing balance) that collapse gracefully on narrow viewports without any custom CSS.",
      "Loading state for a KPI row: identical <ResponsiveGrid columns={4}> wrapping four <SkeletonStat /> placeholders rendered while async data is in flight, swapped for real StatCard tiles once resolved.",
      "Settings or profile summary cards: 2- or 3-column grid of Card+CardContent blocks (not StatCard) showing categorized read-only data groups before a detail form below.",
      "Entity comparison panel: a columns={3} grid comparing three legal entities side-by-side with a Card+CardContent per entity, which collapses to 2-up on tablet and stacks on mobile."
    ]
  },
  {
    "example": "import { AppShell, Sidebar } from \"@godxjp/ui/layout\";\nimport { LayoutDashboard, Users } from \"lucide-react\";\nimport { router } from \"@inertiajs/react\";\n\nconst sidebar = (\n  <Sidebar\n    activeId=\"/dashboard\"\n    onSelect={(id) => router.visit(id)}\n    sections={[{ items: [\n      { id: \"/dashboard\", label: \"ダッシュボード\", icon: LayoutDashboard },\n      { id: \"/users\", label: \"ユーザー\", icon: Users },\n    ] }]}\n    product={{ name: \"JOVY CRM\", role: \"本部\", color: \"var(--color-primary)\" }}\n  />\n);\n\nexport function CrmLayout({ children }: { children: React.ReactNode }) {\n  return <AppShell sidebar={sidebar}>{children}</AppShell>;\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AppShell",
    "props": [
      {
        "description": "Optional sidebar node, typically a Sidebar. Omit or pass null/false to remove its landmark and grid track. Logo remains in the topbar; navRail is independent.",
        "name": "sidebar",
        "type": "ReactNode"
      },
      {
        "description": "Main page content rendered in <main>.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Full topbar override; else a rail is built from topbarLeft/topbarRight/logo. Omit ALL FOUR bar slots (topbar, topbarLeft, topbarRight, logo) and there is NO `<header class=\"app-topbar\">` in the DOM at all: the shell publishes `data-topbar=\"none\"` on its root and the bar's grid row collapses to zero, for a shell whose PAGE owns the top row. The one exception is the AppShell-owned drawer trigger — at or below 900px the header comes back carrying the hamburger alone, so navigation is never unreachable.",
        "name": "topbar",
        "type": "ReactNode"
      },
      {
        "description": "Right slot of the auto-built topbar rail (user menu, switcher).",
        "name": "topbarRight",
        "type": "ReactNode"
      },
      {
        "description": "Left slot of the auto-built topbar rail.",
        "name": "topbarLeft",
        "type": "ReactNode"
      },
      {
        "description": "The shell's brand lockup. ALWAYS rendered — unlike topbarLeft/topbarRight it survives a custom `topbar`, because identity belongs to the frame, not to the bar's contents. WHERE it lands follows `topbarSpan`, the axis that already says who owns the top-left corner: `content` puts it at the sidebar's head, aligned to that track; `full` puts it in the bar beside the space-level chrome. Do not place it yourself in `Sidebar.brand` or a `Topbar` slot — that pins it to one arrangement while the axis moves the rest of the shell.",
        "name": "logo",
        "type": "ReactNode"
      },
      {
        "description": "The brand node a NARROW bar gets instead of `logo` — a mark without the wordmark, a shorter lockup, a different viewBox (gh#728). The brand cell already shrinks without it: below the sm step it is capped at --app-shell-brand-compact-max-inline-size (the bar's own height) and anything past the cap is cropped from the inline-end, which is all a stylesheet can do to artwork it did not author — viewBox is an ATTRIBUTE, which is why consumers were re-cropping their own SVG through a media-query hook. Pass a node here to CHOOSE what the narrow bar shows. Both nodes are rendered and the breakpoint drops one with display:none, so exactly one brand is ever in the accessibility tree. Applies to the brand IN THE BAR (topbarSpan=\"full\", or a shell with no sidebar); under the default content span the brand sits at the rail's head, which the drawer has already replaced at that width.",
        "name": "logoCompact",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"sm\"",
        "description": "The step at which `logoCompact` takes over — Flex hideBelow's vocabulary on the same canonical scale (sm 40rem · md 48rem · lg 64rem · xl 80rem). Ignored without `logoCompact` (gh#728).",
        "name": "logoCompactBelow",
        "type": "BreakpointProp"
      },
      {
        "defaultValue": "false",
        "description": "Collapse the sidebar to icon-only mode.",
        "name": "sidebarCollapsed",
        "type": "boolean"
      },
      {
        "defaultValue": "\"drawer\"",
        "description": "Navigation strategy below the canonical 900px breakpoint. drawer exposes the accessible mobile Sheet; docked retains the token-sized sidebar grid track and suppresses the redundant drawer trigger.",
        "name": "responsiveNavigation",
        "type": "\"drawer\" | \"docked\""
      },
      {
        "defaultValue": "\"content\"",
        "description": "Which columns the topbar spans. content starts it beside the sidebar, so the rail runs the full window height and the bar sits over the content only. full runs the bar edge to edge with the rail beneath it, for a bar carrying space-level chrome (global search, account, notifications) that outranks the current section. full also renders the header before the aside so keyboard order follows the visual order.",
        "name": "topbarSpan",
        "type": "\"content\" | \"full\""
      },
      {
        "description": "A SECOND navigation column, narrower than `sidebar` and placed before it — the workspace/organization switcher shape (Slack, Teams, Discord): rail → sidebar → content. THE THREE COLUMNS ARE THREE SCOPES, and that is what decides where a control goes. The rail is PLATFORM scope: what is true across every app in the organization — which organization, which app, notifications, messages, events, organization settings, cross-app shortcuts. The sidebar is APP scope: this app's own sections, channels, routes. The topbar is PAGE scope: where you are and what you can do here. App navigation never goes in the rail and a platform switch never goes in the sidebar; a destination that would fit both belongs to the rail, because it survives changing apps. A rail repeating the sidebar's own entries is a second chrome band carrying the first one's rank, just vertical. Passing a node adds the grid track and publishes `data-nav-rail` on the root; omitting it leaves the two-column shell unchanged. ONE THICKNESS, BOTH AXES: `--app-shell-nav-rail-width` (2.5rem) and `--app-shell-nav-rail-height`, which resolves to it, so the rail is the same measure on every edge and a service retunes both with one line. Deliberately not the collapsed sidebar's 4rem — at equal widths the two nav tracks fuse into one block when the sidebar collapses. THE RAIL SIZES ITS OWN CELLS through `--app-shell-nav-rail-item-size` (2.25rem): a control carries its own band token, so narrowing the TRACK alone does not narrow it, it CLIPS it (the rail clips) — the same relationship the bar has with TopbarItem, which stretches to the bar rather than the other way round. `@media (pointer: coarse)` lifts the cell to the 44px tap floor of rule #24 and the track with it. Fully orthogonal to `topbarSpan` — the rail says how many navigation COLUMNS exist, `topbarSpan` says how far the BAR reaches, and all four combinations are supported. `sidebarCollapsed` folds the sidebar track only; the rail keeps its width (the Slack behaviour). Rendered as its own `complementary` landmark, and its content is added to the mobile drawer automatically.",
        "name": "navRail",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"start\"",
        "description": "WHICH EDGE the rail sits on. `start` (default) and `end` are the INLINE edges — logical, so an RTL document mirrors them with no `[dir]` rule, because grid columns lay out in the inline direction. `top` and `bottom` are the BLOCK edges: the rail becomes a full-measure horizontal strip and the shell grows a ROW instead of a column, which is the phone tab-bar shape at the bottom and a platform band above the app's own bar at the top. THE SCOPE CONTRACT DOES NOT MOVE WITH IT — wherever it sits, the rail is platform scope and the sidebar is app scope; the edge is a presentation choice, not a re-ranking. Thickness follows the orientation (`--app-shell-nav-rail-width` as a column, `--app-shell-nav-rail-height` as a strip), and `sidebarCollapsed` folds the sidebar track only at every position.",
        "name": "navRailPosition",
        "type": "\"start\" | \"end\" | \"top\" | \"bottom\""
      },
      {
        "description": "Rail content pinned to its FAR end — the counterpart of Sidebar's `footer`, and the tray end of a taskbar: appearance, settings, the account glyph. It follows the orientation, so it is the bottom of a column and the inline-end of a strip, and it stays put while `navRail` scrolls. A SLOT rather than \"whatever you put last\", because pinning needs an auto margin on whichever axis the rail currently runs — geometry that would otherwise land in consumer CSS, which this library does not accept. It reaches the mobile drawer with the rest of the rail; a control that exists on only some viewports is a trap, not a control. Ignored without `navRail`: there is no rail to pin it to.",
        "name": "navRailEnd",
        "type": "ReactNode"
      },
      {
        "description": "Accessible name for the `navRail` landmark. Defaults to the localized 'Workspaces'. The rail and the sidebar are two `complementary` landmarks on one page, so ARIA requires distinct names; the shell supplies both defaults so the two columns of equal rank behave the same way.",
        "name": "navRailLabel",
        "type": "string"
      },
      {
        "description": "App-level footer outside the main content area.",
        "name": "footer",
        "type": "ReactNode"
      },
      {
        "description": "Breadcrumb trail rendered in the topbar header for back-navigation.",
        "name": "breadcrumb",
        "type": "BreadcrumbProp"
      },
      {
        "description": "Navigation shown in the AppShell-owned mobile drawer at or below 900px (where the docked sidebar is hidden). Defaults to the `sidebar` node; pass a tailored menu, or null to opt out.",
        "name": "mobileNav",
        "type": "ReactNode"
      },
      {
        "description": "Accessible title for the mobile navigation drawer. Defaults to localized 'Menu'.",
        "name": "mobileNavLabel",
        "type": "string"
      },
      {
        "description": "Visible text beside the drawer trigger's glyph (e.g. 'Pages'); it becomes the trigger's accessible name instead of the localized 'Open navigation'. The drawer closes on a link, a Sidebar row or a menu item, or an explicit close (Esc, overlay, close button, SheetClose) — other buttons such as a tree's expand toggle leave it open (gh#1133).",
        "name": "mobileNavTriggerLabel",
        "type": "string"
      },
      {
        "description": "SheetContent.onCloseAutoFocus for the built-in drawer: fires on every close before focus returns to the trigger. Call event.preventDefault() and focus your own target (e.g. the new page's heading via PageContainer titleRef) to send focus there instead (gh#1140).",
        "name": "mobileNavOnCloseAutoFocus",
        "type": "(event: Event) => void"
      },
      {
        "description": "Controlled open state of the mobile drawer. Omit for AppShell-owned state.",
        "name": "mobileNavOpen",
        "type": "boolean"
      },
      {
        "description": "Change handler for the mobile drawer open state.",
        "name": "onMobileNavOpenChange",
        "type": "(open: boolean) => void"
      }
    ],
    "related": [
      "Sidebar — the canonical node to pass as AppShell's `sidebar` prop; owns activeId, collapsible submenu groups, collapsed icon-only mode, and section labels. Never hand-roll a nav list inside the sidebar slot.",
      "Topbar — the bar to pass to AppShell's `topbar` prop when it needs live controls: compose a switcher, search, notifications or the user avatar into its `start` / `center` / `end` slots. A custom `topbar` replaces the DEFAULT bar layout, so `topbarLeft`/`topbarRight` are ignored — but `logo` is not: the shell always renders it, and `topbarSpan` decides whether it lands in the bar or at the sidebar's head.",
      "PageContainer — the mandatory direct child inside AppShell's `children` for every page; provides title, subtitle, extra actions, breadcrumb, footer, variant (flush/narrow/ghost), and density. Never render raw content directly as AppShell's child without a PageContainer wrapper."
    ],
    "rules": [
      23
    ],
    "storyPath": "layout/app-shell.tsx",
    "tagline": "Root application shell — composes sidebar, topbar rail, main content area, and optional footer. Keyboard focus in clipped chrome stays inset; the main scroll region shows an inset brand ring by default (--region-focus-ring-width).",
    "usage": [
      "DO put a sidebar collapse toggle IN the rail, not floating in the bar (ADR C10): the shell does not render one, so pass it through `Sidebar action={(collapsed) => <Button variant=\"ghost\" size=\"icon\" aria-label={t(\"toggleSidebar\")} aria-expanded={!collapsed} …/>}` (or the `footer` render-prop), which keeps it attached to the rail in both states. A toggle in `Topbar start` under `topbarSpan=\"full\"` lands beside the brand, detached from the rail it controls. The shell has no edge-mounted toggle (a control straddling the rail border); `Sidebar action` keeps it inside the rail head, which is the supported placement. Below the navigation boundary the drawer has its own trigger; render no collapse toggle there.",
      "useAppShellNavigationMode() returns drawer/docked at the same 56.25rem boundary as AppShell. The root also publishes data-navigation. Use this instead of a consumer media query.",
      "DO pass a <Sidebar> node to `sidebar` (required) and page content to `children` (required) — these are the only two required props. Everything else is optional and omitting optional slots simply removes that zone from the rendered DOM.",
      "DO rely on AppShell's OWNED mobile drawer at or below 900px (NOT the Tailwind `lg` 1024px step — the shipped media query is `width <= 56.25rem`): it renders a hamburger trigger in the topbar and a focus-trapped Sheet (Esc + overlay close, focus returns to the trigger). `mobileNav` defaults to the `sidebar` node, so the same nav is reachable on mobile with no wiring — never hide the sidebar without providing this. Pass a tailored `mobileNav`, or `mobileNav={null}` only when navigation lives elsewhere (e.g. a bottom bar).",
      "DO set `responsiveNavigation=\"docked\"` only when the approved product contract retains its sidebar below 900px. AppShell keeps the same sidebar/footer/active navigation in a token-sized grid track and removes the redundant drawer trigger; never reproduce this with consumer media queries.",
      "DO let the drawer nav own its own inset: AppShell renders `mobileNav` in a Sheet body whose inline padding is the `--app-shell-mobile-nav-inset` token (near-zero by default) instead of the generic 24px sheet chrome inset, so a <Sidebar> in the drawer is not double-padded (its own --sidebar-nav-scroll-padding already insets each row). If a custom `mobileNav` node needs the full chrome inset, set `--app-shell-mobile-nav-inset: var(--space-6)` in the service theme — never patch the drawer with a `[data-slot='sheet-body']` selector in app CSS.",
      "DO use the auto-built topbar rail (logo / topbarLeft / topbarRight) for simple shells. Pass a fully configured <Topbar start={…} center={…} end={…}> to the `topbar` prop when you need live controls (an entity switcher, search, notifications, the user avatar) — Topbar has those three slots and no per-control props — `topbarLeft`/`topbarRight` are then ignored (they are slots of the DEFAULT bar layout, and a custom `topbar` IS replacing that layout; a dev-mode warning names them). `logo` is NOT one of them: it is always rendered, because brand identity belongs to the frame rather than to the bar's contents. Two repos passed both for months and got no logo at all — no error, no warning, and a bar that still looked right because it had other content.",
      "DO pass `logo` and let the shell place it — do NOT put the lockup in `Sidebar`'s `brand` slot or hand-position it in a `Topbar` slot. THE TOP-LEFT CORNER BELONGS TO WHOEVER `topbarSpan` SAYS: under `content` the rail runs the full window height and the brand sits at the sidebar's head, aligned to that track; under `full` the bar runs edge to edge and the brand goes in the bar with the rest of the space-level chrome. Placing it yourself pins it to one of those answers, and the axis then moves the rest of the shell out from under it (measured on a shipped consumer: the logo floating in the content column at x=280, indented 24px past the rail it should have sat above).",
      "DO give a wide brand lockup a narrow-bar answer with `logoCompact` instead of cropping your own artwork in the app: `<AppShell topbarSpan=\"full\" logo={<FullLockup />} logoCompact={<MarkOnly />} />` swaps the NODE below the `sm` step, which is the only way to change a `viewBox` (no stylesheet can set an attribute). Without it the brand cell still gives room back — it is capped at --app-shell-brand-compact-max-inline-size below that step and cropped from the inline-end — where before it kept its intrinsic width at every viewport: measured with a 6:1 lockup at 390px, brand 143.7px, `.ui-topbar` 150.3px, `.ui-topbar-start` width 0 and the end cluster's last cell painting at x=384.8 on a 390px viewport (gh#728).",
      "DO build a chat / mail / IDE shell by omitting ALL FOUR bar slots (`topbar`, `topbarLeft`, `topbarRight`, `logo`) — a shell whose PAGE owns the top row. AppShell then renders no `<header class='app-topbar'>` and marks its root `data-topbar='none'`, so the bar's grid row collapses to zero and the page header IS the first row of chrome. Keeping an empty bar instead costs a fixed `--app-shell-bar-height` band plus its border and card background, stacking a second row of chrome (~48px + the page header) over exactly the region a transcript or an editor needs most. The mobile drawer survives: at or below 900px the header returns carrying the hamburger alone.",
      "DO NOT fake the bar-less shell with `topbar={<></>}` (or `topbarLeft={<div />}`, `logo={null}`) — any defined slot counts as bar content, so the `<header>` is still rendered, still paints its border and background, and still eats the grid row. The trigger is the slot being UNDEFINED; pass nothing at all (a conditional slot must resolve to `undefined`, not to an empty node).",
      "DO wire a single `sidebarCollapsed` boolean between AppShell's `sidebarCollapsed` prop and Sidebar's `collapsed` prop — AppShell sets `data-collapsed='true'` on the root div (which CSS reads for width transitions) but does NOT own the collapsed state itself; lift the state and pass it down to both.",
      "DO place breadcrumb content in AppShell's `breadcrumb` prop (renders in the `app-breadcrumb` div inside `<main>` ABOVE children) — do NOT hand-roll a breadcrumb bar as the first child of children, and do NOT put breadcrumbs inside <Sidebar>.",
      "DO build a three-column shell (a narrow workspace/org rail, then the channel or section sidebar, then content) by passing `navRail` — NEVER by putting two columns inside the single `sidebar` slot. Hand-rolling it hits two measured traps: `Sidebar` renders `.sb-root { display: contents }`, so two Sidebars dropped side by side dissolve into one flex row and both collapse to zero unless each is separately wrapped in its own flex-column box; and sizing the one available track for two columns means overriding `--app-shell-sidebar-width`, which is how a shipped consumer moved its content edge 64px between routes. `navRail` owns the track, so neither is needed.",
      "DO leave `sidebarCollapsed` wired to the sidebar alone when a `navRail` is present — collapse folds the sidebar track (16rem → 4rem) and the rail keeps its width, so the rail's destinations stay reachable while collapsed. That is the Slack behaviour and it is the shell's, not something to reproduce with consumer CSS.",
      "DO NOT pass `mobileNav` just to re-add the rail on mobile — when `navRail` is present the drawer already defaults to the rail followed by the sidebar, because BOTH docked columns are hidden below 900px and a `sidebar`-only default would silently delete every app-level destination the rail carries.",
      "DO NOT nest a second AppShell or AppShell inside AppShell's children — AppShell renders the root `app-root` div; nesting shells breaks the CSS grid layout.",
      "DO NOT add padding directly to children expecting it to reach the viewport edge — AppShell's `<main>` is a scroll container; use <PageContainer> (or <PageContainer.Inset> inside a flush PageContainer) inside children to get standard page padding.",
      "DO let the page be FLUID: inside AppShell a <PageContainer> spans the whole main column at every sidebar state (gh#672 — the old default 80rem cap left a 168px dead strip at a 1512px viewport once the sidebar collapsed, and cut a sticky footer short). A service that wants ONE bounded column sets `--app-shell-page-max-width` once in its theme (default `none`); it caps the page header, toolbar and body — never the `.ui-page-footer` band, which keeps spanning `.app-main` while its CONTENT ends on the end edge of the body (gh#682) — and a page-level `measure` overrides it on that page. Bound a single readable page with `measure=\"narrow\" | \"medium\"`, never a page-local max-width or a wrapper div."
    ],
    "useCases": [
      "Full admin SPA shell: AppShell wraps a <Sidebar> nav rail and a <Topbar> (with productMenu entity-switcher, onSearchOpen, onNotificationsOpen, user avatar) and every Inertia page renders as children inside a <PageContainer>.",
      "Collapsible-sidebar layout: maintain a `collapsed` boolean in a persistent Inertia layout component, pass it to both AppShell's `sidebarCollapsed` and Sidebar's `collapsed`, wire Topbar's `onToggleCollapsed` to flip it — AppShell handles the CSS transition automatically.",
      "Multi-tenant accounting app: pass a <Topbar start={<DropdownMenu>…</DropdownMenu>}> to AppShell's `topbar` slot so the legal-entity chip opens an inline switcher without a modal.",
      "App-level footer (e.g. version/build info, compliance notice): pass a <footer> node to AppShell's `footer` prop — it renders outside `<main>` so it stays pinned below the scroll area.",
      "Rapid prototype or internal tool where you want a branded shell with minimal topbar: skip the `topbar` prop entirely and use `logo`, `topbarLeft`, `topbarRight` to build the rail declaratively without instantiating <Topbar>.",
      "Breadcrumb-aware shell: pass a <Breadcrumb items={…}> node to AppShell's `breadcrumb` prop so the breadcrumb strip appears above all page content without each page having to render it separately.",
      "Chat / mail / IDE shell: a channel rail in `sidebar`, no bar slots at all, and a <PageContainer fill toolbar={…} footer={<ChatComposer />} stickyFooter> (ChatComposer from @godxjp/chat) as children — the page's own header is the top row and the shell reserves no band above it, while the 900px drawer still reaches the channel list."
    ]
  },
  {
    "absorbed": [
      "AuthShell",
      "OrganizationChoiceList"
    ],
    "example": "import { CenteredShell, Topbar, Flex } from \"@godxjp/ui/layout\";\nimport { AppSettingPicker } from \"@godxjp/ui/navigation\";\nimport { Avatar, AvatarFallback, Card, CardContent, CardHeader, CardTitle } from \"@godxjp/ui/data-display\";\nimport { Button, Text } from \"@godxjp/ui/general\";\n\nexport function MyPage() {\n  return (\n    <CenteredShell\n      width=\"md\"\n      topbar={\n        <Topbar\n          start={\n            <Avatar className=\"rounded-md\">\n              <AvatarFallback className=\"bg-primary text-primary-foreground font-bold\">G</AvatarFallback>\n            </Avatar>\n          }\n          end={\n            <>\n              <AppSettingPicker kind=\"locale\" />\n              <Button variant=\"ghost\" size=\"sm\">田中 太郎</Button>\n            </>\n          }\n        />\n      }\n      footer={<Text size=\"xs\" tone=\"muted\">© 2026 GodX</Text>}\n    >\n      <Flex direction=\"col\" gap=\"lg\">\n        <Card>\n          <CardHeader><CardTitle level={1}>マイページ</CardTitle></CardHeader>\n          <CardContent>アカウントの概要。</CardContent>\n        </Card>\n      </Flex>\n    </CenteredShell>\n  );\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "CenteredShell",
    "props": [
      {
        "defaultValue": "\"page\"",
        "description": "Which shape. \"page\": the authenticated centred column (topbar/width/align start|center/preset default|public-landing). \"auth\": the unauthenticated auth shell (brand/actions/footer, a vertically centred ~24rem card at the comfortable 44px control tier, preset = a hosted-identity flow, align anchored|center, measure, density). \"auth-canonical\": the auth shell on the shared DXS compact geometry (36px controls, 22.5rem card, responsive insets). The variant decides which props type-check. v32 folded the former `AuthShell` in here (`AuthShell` → `variant=\"auth\"`, `AuthShell variant=\"canonical\"` → `variant=\"auth-canonical\"`).",
        "name": "variant",
        "type": "\"page\" | \"auth\" | \"auth-canonical\""
      },
      {
        "description": "Centred column content — page sections (identity hero, org picker, service grid, team list). Top-aligned and scrolls; NOT vertically centred like the auth CenteredShell's card.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Top bar slot (banner) — a <Topbar> with brand + real actions (AppSettingPicker, user menu, sign-out). Wrapped in the SAME padded chrome as AppShell's topbar (inline padding, border, backdrop) WITHOUT a sidebar; omit → no banner. Never hand-roll a bar (the bare Topbar ships no padding — the .ui-topbar zero-inset footgun).",
        "name": "topbar",
        "type": "ReactNode"
      },
      {
        "description": "Footer slot (contentinfo) pinned to the bottom (legal links, locale switch, support). Omit → no footer.",
        "name": "footer",
        "type": "ReactNode"
      },
      {
        "description": "Max-width of the centred column: sm ~32rem, md (default) ~46rem, lg ~64rem — all wider than the auth CenteredShell's 24rem auth card. A service retunes each tier via --centered-shell-width-*.",
        "name": "width",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "AUTH: Block-axis placement of the auth column — ORTHOGONAL to `preset` the way `variant` is: the preset owns the page MEASURE (card width, inline gutters, section rhythm), `align` owns where that column sits vertically. Omit it to keep the preset's own choice: \"login\" and \"registration\" anchor so a requester/identity line that wraps to two lines cannot move the card (gh#237), every other preset centres. Pass \"center\" for a vertically centred column — the block-start inset collapses to the preset's block-end one on desktop AND mobile, so the padding is symmetric and justify-content has nothing to fight — or \"anchored\" for a top-anchored one. This is what replaces re-declaring a preset's offset tokens from consumer CSS, the page-local-vertical-offset anti-pattern presets exist to remove. HAZARD on a tall flow: a vertically centred tall card overflows ABOVE the scroll origin on a short viewport, putting its first field out of reach — which is why \"registration\" anchors by default; \"center\" is legal there but is the caller's judgement. PAGE: Block alignment of the centred column inside the 100dvh shell. \"start\" (default) keeps the top-aligned flowing/scrolling page shape. Overflowing content still scrolls from the top, so a long localized message is never clipped.",
        "name": "align",
        "type": "\"start\" | \"center\" (page) | \"anchored\" | \"center\" (auth)"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Whole-page shell contract. \"default\" emits no attribute and keeps the exact box. \"public-landing\" owns the PUBLIC landing geometry: ONE content measure shared by the header bar, the centred column and the footer (--centered-shell-landing-max-width, 67.5rem), the section rhythm between page sections, the flat elevation-free card chrome (--centered-shell-landing-card-shadow: none) and the hero h1 tier — plus the compact step at 40rem. A landing composition therefore needs no page-local CSS, no max-width wrapper and no descendant selector against shell internals. AUTH: Named flow GEOMETRY — the package-owned layout contract for a canonical hosted-identity flow. \"device-authorization\" = a 380px card at 1440/1024 with a 5px inline page gutter at 390, AND the code field itself: the preset hands --otp-slot-{inline,block}-size the canonical 27.5x52 device-grant slot, so two 4-slot grouped boxes measure 112x54 instead of the 146x38 the square --control-height tier produced. \"registration\" = the 360px sign-up measure with a 15px inline gutter at 390 (the same page rhythm as \"login\", so sign-in to sign-up never jumps on a phone). START-aligned like login: a sign-up card is the tallest surface in the set (name/email/password/confirm/strength/consent/submit/providers) and a vertically centred tall card overflows ABOVE the scroll origin on a short viewport, putting its first field out of reach.",
        "name": "preset",
        "type": "\"default\" | \"public-landing\" (page) | \"login\" | \"registration\" | \"device-authorization\" | \"context-selection\" | \"account-recovery\" (auth)"
      },
      {
        "description": "the auth shape only. Brand bar slot pinned to the top (e.g. a <Logo> / product mark).",
        "name": "brand",
        "type": "ReactNode"
      },
      {
        "description": "the auth shape only. Page-level controls pinned to the TOP-RIGHT of the same banner row as `brand` — a locale <Select>/<AppSettingPicker>, a theme <ToggleGroup>, a \"need help?\" link. They belong to the PAGE, not the auth form, so they sit in the bar, not in the card. The banner renders as soon as `brand` OR `actions` is present.",
        "name": "actions",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"default\"",
        "description": "the auth shape only. Inline MEASURE of the shell content slot. \"default\" is the single auth card (24rem, or 22.5rem under variant=\"canonical\"). \"wide\" opens the slot to --auth-shell-wide-card-max-width (64rem) for a SPLIT login: a brand/marketing panel beside the auth card, laid out with <ResponsiveGrid columns={{ sm: 1, lg: 2 }}>. The wide slot centres with auto margins, so a tall two-column layout starts at the top instead of overflowing above the scroll origin. Ignored under a `preset` — a preset already owns its flow geometry.",
        "name": "measure",
        "type": "\"default\" | \"wide\""
      },
      {
        "description": "the auth shape only. Vertical density scoped to auth-card descendants. Defaults to \"compact\" under variant=\"canonical\" and \"comfortable\" otherwise.",
        "name": "density",
        "type": "\"comfortable\" | \"compact\""
      }
    ],
    "related": [
      "AppShell — the shell for authenticated app pages WITH a sidebar nav rail. CenteredShell is its no-sidebar sibling (same padded topbar chrome, a centred column instead of a full-bleed main).",
      "Topbar — compose it into `topbar`; CenteredShell supplies the padded chrome the bare Topbar lacks.",
      "PageContainer — for a titled section INSIDE the column; or compose <Card>/<ResponsiveGrid> sections directly.",
      "Reveal — wrap the auth <Card> in <Reveal> for the entrance animation; the auth CenteredShell delegates motion (and prefers-reduced-motion handling) to it rather than baking an animation in.",
      "Card — the canonical container for the auth form; place the form inside <CardContent>. the auth CenteredShell centres and width-constrains it.",
      "EmptyState — with `tone=\"success\"` for a confirmation card inside the shell (e.g. device approved).",
      "AuthIdentity / AuthFooter / AuthStack / AuthDivider — the auth composites that fill the shell: the GoDX identity mark + heading + requesting-client line, the mono legal footer (its `locale` slot takes an <AppSettingPicker kind=\"locale\" appearance=\"labeled\" compact />), the 12px section rhythm, and the labelled \"or\" rule.",
      "ListRow — compose the organisation choice list for `preset=\"context-selection\"` as <Card><CardContent flush><Flex as=\"ul\" marker=\"none\"> of <ListRow as=\"li\">: shared row dividers, no per-row card outline. There is no separate OrganizationChoiceList component — that is a composition pattern (rule #46), not a framework component."
    ],
    "rules": [
      23
    ],
    "storyPath": "layout/centered-shell.tsx",
    "tagline": "The centred root shell, two shapes: `variant=\"page\"` (default) — authenticated, no-sidebar, centred-column page (hosted-ID My Page / account / standalone settings) with a padded topbar; `variant=\"auth\"` / `\"auth-canonical\"` — the unauthenticated login/mfa/reset shell (brand bar + vertically centred auth card + footer). Zero custom CSS either way.",
    "usage": [
      "DO use CenteredShell for an AUTHENTICATED page that has a topbar with actions but NO sidebar — the hosted-ID 'My Page', an account / self-service surface, a standalone settings page. It is the third shell: AppShell (needs a sidebar) · the auth CenteredShell (unauthenticated narrow card) · CenteredShell (authenticated centred column).",
      "DO put a <Topbar start={<brand/>} end={<actions/>}/> in `topbar` — CenteredShell wraps it in the padded `.app-topbar` chrome, so you get inline padding + border + backdrop with zero custom CSS. Do NOT hand-roll a bar with raw `padding-inline` — the bare Topbar primitive ships no inset (the .ui-topbar zero-padding footgun) and content sits flush to the edge.",
      "DO pick `width` by content: `sm` (~32rem) for a single settings form, `md` (default, ~46rem) for a My Page of stacked sections, `lg` (~64rem) for a service-launcher grid. All are wider than the auth CenteredShell's 24rem card.",
      "DO wrap an individual section in <Reveal> for entrance motion — CenteredShell stays layout-only and delegates prefers-reduced-motion handling to Reveal (same as the auth CenteredShell).",
      "DO NOT use the auth shape for an authenticated page (it is the UNAUTHENTICATED root and imposes auth-card geometry), and DO NOT force AppShell with an empty sidebar — use CenteredShell. Conversely, do NOT reach for CenteredShell to build a login page: the auth CenteredShell has its own `actions` slot for the locale/theme controls and a `measure=\"wide\"` for the split brand-panel login. Never nest it inside AppShell/auth CenteredShell (or vice-versa); it is a ROOT shell.",
      "DO use `align=\"center\"` (+ `width=\"sm\"`) for a SYSTEM-level standalone page — a 500/503 error surface, a maintenance notice. It centres the column in the 100dvh shell at 1440/1024/390 with no consumer `min-h-dvh` / flex CSS and no className; the knob is --centered-shell-column-offset-block. For an actual 403/404/500/503 page do NOT wire this by hand — use `ErrorSurface`, which renders this shell itself in `mode=\"system\"`.",
      "AUTH SHAPE: DO pass a single <Card> (with the form inside <CardContent>) as `children` — the auth CenteredShell centres it and constrains its width via `--auth-shell-card-max-width`; do NOT hand-roll a `.auth-shell-main` / `.ui-auth-scope` wrapper.",
      "AUTH SHAPE: DO use `variant=\"auth-canonical\" preset=\"login\"` for SCR-001 and pass <AuthIdentity>, <Card>, <AuthFooter> as direct children in that order (an anchor may wrap AuthIdentity). The preset owns the identity slot, card anchor, 20px section rhythm and compact card block inset for standalone and real requester states. Do not wrap the three sections in a consumer Flex/Stack or the semantic grid cannot anchor them.",
      "AUTH SHAPE: DO select a `preset` instead of overriding geometry: `preset=\"login\"` for the stable SCR-001 identity/card/footer anchor, `preset=\"registration\"` for the sign-up form and its pending-email state, `preset=\"device-authorization\"` for the 380px OAuth device-grant measure, `preset=\"context-selection\"` for the 25rem organisation/context picker, `preset=\"account-recovery\"` for the 432px SCR-008 recovery/MFA panel. Page-local width/inset/vertical-offset variables are the exact anti-pattern these presets replace.",
      "AUTH SHAPE: DO build the SOCIAL / PROVIDER ACTION row as a COMPOSITION — there is NO SocialLinks component: `<AuthDivider label=\"or\" />` followed by a `Flex direction=\"col\" gap=\"sm\"` of real `Button variant=\"outline\"` with the provider glyph as an aria-hidden icon. The package deliberately does not own it: which providers a product offers, in what order, and what consent they imply are product decisions, and a component would have to invent them. `disabled` / `loading` are the Button's own props — do not add a provider-specific API.",
      "AUTH SHAPE: DO build the ORGANIZATION CHOICE LIST as a COMPOSITION — there is NO OrganizationChoiceList component: `Card` > `CardContent flush` > a `<Flex as=\"ul\" marker=\"none\">` of `ListRow as=\"li\"` (leading Avatar, title, description, trailing Button). `CardContent flush` is what gives shared row dividers instead of a card outline per row. Its states are existing exports, never bespoke markup: Skeleton rows for loading, `EmptyState` for no invitations, `Alert tone=\"destructive\"` for a failed fetch and `Alert tone=\"warning\"` for permission-denied. See the auth-shell-context and auth-shell-registration frames.",
      "AUTH SHAPE: DO build the password-recovery and sign-in MFA CHALLENGE panels as a COMPOSITION inside `preset=\"account-recovery\"` — there is NO PasswordRecoveryPanel and NO MfaChallengePanel component: Card > CardHeader(CardTitle + CardDescription, INSIDE the bordered surface) > CardContent > AuthStack[ Alert notice · FormField fields · Button fullWidth · Flex justify=\"between\" wrap fallback row ]. Do NOT put AuthIdentity above the panel there (it always renders the hosted mark), and NEVER reuse TwoFactorSetup — that is the ENROLLMENT dialog, not a sign-in challenge. See the `auth-recovery-panels` pattern.",
      "AUTH SHAPE: DO combine `variant` and `preset` — they are orthogonal: `variant` owns control density + heading size, `preset` owns the page measure. `variant=\"auth-canonical\" preset=\"device-authorization\"` is the canonical device screen.",
      "AUTH SHAPE: DO let `preset=\"context-selection\"` space the auth column: it turns the card slot into a flex column with a tokenized `--auth-shell-card-stack-gap`, so an intro (<AuthIdentity>), the choice <Card> and a trailing \"remember\" row pass as three siblings with NO page-local spacing.",
      "AUTH SHAPE: DO NOT add a page-local width, inset or colour to hit an artboard — if a measure is missing, it is a library gap: a new preset or token, never consumer CSS (rules #44/#45).",
      "AUTH SHAPE: DO state the block placement with `align` when a product wants the opposite of its preset's default — align=\"center\" on preset=\"login\" for a vertically centred SCR-001, align=\"anchored\" to top-anchor a preset that centres. It collapses the preset's block-start inset to its block-end one on desktop AND mobile, so justify-content has nothing to fight. Re-declaring --auth-shell-login-flow-offset-block (or any preset offset token) from a consumer stylesheet is the anti-pattern it replaces.",
      "AUTH SHAPE: DO put the product/brand mark in `brand` (a <Logo> or an <Avatar>) — it renders as the top banner landmark; omit it and the banner is not rendered.",
      "AUTH SHAPE: DO put page-level controls in `actions` — the locale picker, the theme <ToggleGroup>, a \"need help?\" link. They land at the banner's inline end at a tokenized gap (`--auth-shell-bar-gap`), and the banner appears even with no `brand`. Do NOT hand-roll a top-right row with `ms-auto` on the `brand` content, and do NOT reach for CenteredShell just to get a topbar with actions: CenteredShell is the AUTHENTICATED shell.",
      "AUTH SHAPE: DO use `measure=\"wide\"` for the SPLIT login — a brand/marketing panel beside the auth card. The content slot opens to 64rem (`--auth-shell-wide-card-max-width`) and centres with auto margins, so the tall two-column layout starts at the top instead of overflowing above the scroll origin; lay the two halves out with <ResponsiveGrid columns={{ sm: 1, lg: 2 }}> and hide the panel below `lg`. It is IGNORED under a `preset` (a preset owns its flow geometry), so pick one or the other.",
      "AUTH SHAPE: DO use `footer` for compliance/legal/support links or a locale switch — it renders as the contentinfo landmark below the card.",
      "AUTH SHAPE: DO wrap the card in <Reveal> for the entrance animation (`<CenteredShell variant=\"auth\"><Reveal><Card/></Reveal></CenteredShell>`) — Reveal honours prefers-reduced-motion; the auth CenteredShell itself stays layout-only.",
      "AUTH SHAPE: DO NOT re-scope control height or heading size in the app — the auth CenteredShell already sets the comfortable control tier (44px, WCAG touch floor) and the larger auth heading via `--auth-shell-control-height` / `--auth-shell-heading-size`; a service retunes those tokens, not a bespoke class.",
      "AUTH SHAPE: DO tune the COMPACT auth card through its three independent knobs, never a consumer selector on `[data-slot=\"card-content\"]`: `--auth-shell-compact-card-inset` = the inline column, `--auth-shell-card-padding-block-compact` = the card's block (top/bottom) padding — it is wired to the card's `--card-space-shell-y` and really moves CardContent's block edges — and `--auth-shell-card-body-gap-compact` = the header↔body gap. Each defaults to today's canonical rhythm, so overriding one moves ONLY that axis.",
      "AUTH SHAPE: DO NOT nest the auth CenteredShell inside AppShell (or vice-versa) — the auth CenteredShell is the ROOT shell for unauthenticated pages (login/mfa/passkey/device/reset); AppShell is for the authenticated app."
    ],
    "useCases": [
      "Hosted GoDX ID 'My Page': <CenteredShell topbar={<Topbar start={<Logo />} end={<><AppSettingPicker kind=\"locale\"/>{yourUserMenu}</>}/>} footer={<AuthFooter … />} width=\"md\"> with an identity hero, an org picker, a service-launcher grid and a team list.",
      "Account / self-service settings surface (no admin sidebar): stacked <Card> sections (profile, security, sessions) in a centred `md` column under a topbar with a user menu.",
      "Standalone single settings page: `width=\"sm\"` with one <Card> of <Field>s and a save action.",
      "System error / maintenance page (500 · 503): `<CenteredShell align=\"center\" width=\"sm\">` around the canonical error body (status code <Text mono tabular> + <EmptyState icon tone title description action> + optional request-ID / maintenance line). See the `error-pages` pattern.",
      "Service launcher / app picker after sign-in: `width=\"lg\"` with a <ResponsiveGrid> of app cards under the brand topbar.",
      "AUTH SHAPE: Canonical Login (SCR-001): <CenteredShell variant=\"auth-canonical\" preset=\"login\"> with direct AuthIdentity · Card · AuthFooter children; the card remains anchored when requester is absent, one line or wraps to two lines.",
      "AUTH SHAPE: Login page: <CenteredShell variant=\"auth\" brand={<Logo/>} footer={<AuthFooter/>}> wrapping a <Card> with the email/password form and a primary <Button fullWidth>.",
      "AUTH SHAPE: MFA / passkey / device-authorisation step: same shell, a <Card> with the one-time-code <InputOTP> or a passkey prompt.",
      "AUTH SHAPE: OAuth device-grant screen (SCR-004): <CenteredShell variant=\"auth-canonical\" preset=\"device-authorization\"> — a 380px card at 1440/1024 and a 5px inline gutter at 390, with zero page-local CSS.",
      "AUTH SHAPE: Organisation / context selection (/select-context): <CenteredShell variant=\"auth-canonical\" preset=\"context-selection\" brand={<Logo mark=\"godx\" />}> with an <AuthIdentity> intro, a <Card><CardContent flush> list of <ListRow as=\"li\"> organisations, and a trailing \"remember this choice\" <Checkbox> — the preset spaces the three sections.",
      "AUTH SHAPE: Split product login: <CenteredShell variant=\"auth\" measure=\"wide\" brand={<Logo/>} actions={<><Select locale/><ToggleGroup theme/></>}> around a <ResponsiveGrid columns={{ sm: 1, lg: 2 }}> whose first cell is the brand/value panel (hidden below lg) and whose second is the auth <Card>. See docs/showcase/case4-login.",
      "AUTH SHAPE: Password reset / forgot-password / accept-invite: the centred single-card flow with a brand bar and a legal footer.",
      "AUTH SHAPE: SSO landing / success confirmation: pair with an <EmptyState tone=\"success\"> inside the card for an approved-device confirmation."
    ]
  },
  {
    "example": "import { Flex, MobileShell } from \"@godxjp/ui/layout\";\nimport { Button, Heading, Text } from \"@godxjp/ui/general\";\nimport { Inbox, Package, ScanLine, Truck } from \"lucide-react\";\n\nexport function HandyInbound() {\n  return (\n    <MobileShell\n      statusBar={<Text size=\"sm\" tabular>9:41</Text>}\n      header={\n        <Flex align=\"center\" justify=\"between\" gap=\"xs\" className=\"w-full\">\n          <Heading level={3} as=\"h1\">入庫</Heading>\n          <Button variant=\"ghost\" size=\"sm\">選択</Button>\n        </Flex>\n      }\n      actions={\n        <Button className=\"flex-[2]\">\n          <ScanLine aria-hidden=\"true\" />\n          スキャン\n        </Button>\n      }\n      tabBar={\n        <>\n          <Button variant=\"ghost\" aria-current=\"page\" className=\"h-full flex-col rounded-none\">\n            <Inbox aria-hidden=\"true\" />\n            <Text size=\"2xs\">入庫</Text>\n          </Button>\n          <Button variant=\"ghost\" className=\"h-full flex-col rounded-none\">\n            <Package aria-hidden=\"true\" />\n            <Text size=\"2xs\">梱包</Text>\n          </Button>\n          <Button variant=\"ghost\" className=\"h-full flex-col rounded-none\">\n            <Truck aria-hidden=\"true\" />\n            <Text size=\"2xs\">出庫</Text>\n          </Button>\n        </>\n      }\n    >\n      <Flex direction=\"col\" gap=\"sm\">\n        <Text>洗顔フォーム</Text>\n        <Text>日焼け止め</Text>\n      </Flex>\n    </MobileShell>\n  );\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "MobileShell",
    "props": [
      {
        "description": "The scrolling screen body — the ONLY scroll container in the shell and its only elastic band. Everything else is fixed chrome, so a long list scrolls under a stationary app bar and tab bar instead of taking them off screen with it.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "App bar (banner) pinned to the top: the screen title plus its inline actions. Absorbs the top safe-area inset when there is no statusBar. Omit → no banner. For a screen MODE (multi-select, search, edit) swap the whole node rather than stacking a second strip under it — replacing the bar's contents is the platform pattern on iOS and Android alike.",
        "name": "header",
        "type": "ReactNode"
      },
      {
        "description": "The band that sits IN the OS status-bar strip above the app bar — the carrier/clock row of a display-mode:standalone PWA, or the simulated one in a device-frame preview. It owns the top safe-area inset when present. Omit it in an ordinary browser tab, where the OS already paints that strip and the header takes the inset instead.",
        "name": "statusBar",
        "type": "ReactNode"
      },
      {
        "description": "The sticky action bar pinned above the tab bar — the screen's primary verb (Scan, Save, Hand over) and at most one secondary. It sits OUTSIDE the scroll region, so it is always reachable with no `position: sticky` and no scroll-padding hack, and it takes the home-indicator inset whenever no tabBar follows it. In a `height='viewport'` shell the action bar plus the tab bar publish their summed block size as `--toast-sticky-bar-height`, so the `Toaster` stack sits above the bottom chrome instead of over the primary verb (gh#1271).",
        "name": "actions",
        "type": "ReactNode"
      },
      {
        "description": "Bottom tab bar (navigation) — the app's top-level destinations. Children TILE: equal width, no seam, no page gutter, so you never hand-roll a grid with a column count. Always the last band, so it owns the home-indicator inset.",
        "name": "tabBar",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"viewport\"",
        "description": "Where the shell's one-screen height comes from. \"viewport\" (default) is the real app: exactly 100dvh, so the DOCUMENT never scrolls and the tab bar cannot slide away under a collapsing URL bar. \"fill\" fills a BOUNDED parent instead — a device-frame preview, or a phone view embedded in a wider page — where a viewport-tall root would overflow its frame. Nothing else differs between the two.",
        "name": "height",
        "type": "\"viewport\" | \"fill\""
      },
      {
        "defaultValue": "\"fill\"",
        "description": "How wide the shell is allowed to get. \"fill\" (default) takes the whole inline size it is given — a real handheld, where that IS the phone. \"phone\" caps it at --mobile-shell-max-inline-size (430px, the widest current handheld logical width) and centres the column: the SAME situation height=\"fill\" already names on the other axis, a handheld screen rendered on a viewport wider than a handheld. Measured at 1280px before this axis existed: max-inline-size none, shell 1232px wide, a four-destination tab bar spread across the whole screen.",
        "name": "width",
        "type": "\"fill\" | \"phone\""
      }
    ],
    "related": [
      "AppShell — the authenticated shell WITH a sidebar rail and a mobile drawer at the 900px step. Use it for an admin console that happens to be viewed on a phone; use MobileShell when the phone IS the product.",
      "CenteredShell — the authenticated no-sidebar shell whose main scrolls the PAGE. MobileShell is its handheld counterpart: same 'no rail' shape, opposite scroll contract.",
      "CenteredShell (auth shape) — the unauthenticated root. A login screen inside a handheld app still belongs to the auth CenteredShell, not MobileShell.",
      "Sheet — `side=\"bottom\"` is the handheld modal: scanners, pickers and forms open from the bottom over MobileShell rather than navigating away."
    ],
    "rules": [
      23,
      24,
      45
    ],
    "storyPath": "layout/mobile-shell.tsx",
    "tagline": "Handheld app shell (gh#354) — status band · app bar · the ONE scroll region · sticky action bar · bottom tab bar, with device safe-area insets and a document that never scrolls.",
    "usage": [
      "DO use MobileShell for a HANDHELD app screen — a warehouse/handy terminal, a driver app, a field-work PWA. It is the fourth root shell: AppShell (needs a sidebar) · the auth CenteredShell (unauthenticated card) · CenteredShell (authenticated scrolling document) · MobileShell (a phone app that does NOT scroll its document).",
      "DO let `children` be the only thing that scrolls. Put the primary verb in `actions` and navigation in `tabBar` — both sit outside the scroll region, so neither needs `position: sticky`, a z-index, or bottom padding to clear the other.",
      "DO NOT compose one out of <Card> + `ui-card-inset*` + `overflow-y-auto` (what docs/showcase/case6 did before gh#354). That reproduces the look and neither behaviour that matters on a device: the document still scrolls, and nothing pads out of env(safe-area-inset-*), so the notch covers the app bar and the home indicator covers the primary button.",
      "DO swap the `header` node for a screen MODE (select mode, search mode) instead of stacking a second contextual strip below it — one bar to read, and the platform pattern on both iOS and Android.",
      "DO use `height=\"fill\"` ONLY when the shell is inside a bounded parent (a device-frame preview). In a real app leave it at \"viewport\": that is what keeps the document from scrolling.",
      "DO NOT nest MobileShell inside AppShell / CenteredShell (or the reverse) — it is a ROOT shell. Retune the page gutter and the three band heights from the theme (--mobile-shell-inset-inline, --mobile-shell-header-bar-height, --mobile-shell-tab-bar-height, --mobile-shell-status-bar-height); never fork .ui-mobile-shell-* CSS."
    ],
    "useCases": [
      "Warehouse handheld (代理店ハンディ): a status band, an app bar with a select-mode text action, a scrolling item list, a dominant Scan button in `actions`, and a three-destination `tabBar` (inbound · packing · outbound). See the case6-agency-handy showcase.",
      "Driver / delivery app: route list in the scroll region, 'Arrived' as the single `actions` verb, tabs for today · history · profile.",
      "Field-inspection PWA installed to the home screen: `statusBar` paints the standalone strip, `header` carries the site name, and the form scrolls under both.",
      "A phone view embedded in a desktop device-frame preview: the same composition with `height=\"fill\"` inside a fixed-size frame."
    ]
  },
  {
    "example": "\n{`import { useState } from \"react\";\nimport { LayoutDashboard, FileText, Users, Shield, CreditCard, BookOpen } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { AppShell, createSidebarLink } from \"@godxjp/ui/layout\";\nimport { Sidebar, type SidebarSectionProp } from \"@godxjp/ui/layout\";\nimport { Topbar, TopbarItem } from \"@godxjp/ui/layout\";\n\n// The WHOLE router integration: pass the element type, the library composes every row\n// (icon · label · badge · active · collapsed rail). Inertia: inertiaSidebarLink(Link) from\n// \"@godxjp/ui/inertia\". Next.js: createSidebarLink(Link).\nconst NavLink = createSidebarLink(Link, \"to\");\n\nconst sections: SidebarSectionProp[] = [\n  {\n    label: \"Accounting\",\n    items: [\n      { id: \"dashboard\", label: \"Dashboard\", icon: LayoutDashboard, href: \"/dashboard\" },\n      {\n        id: \"ledger\",\n        label: \"Ledger\",\n        icon: BookOpen,\n        children: [\n          { id: \"journal\", label: \"Journal\", icon: FileText, href: \"/ledger/journal\" },\n          { id: \"chart-of-accounts\", label: \"Chart of Accounts\", icon: CreditCard, href: \"/ledger/coa\" },\n        ],\n      },\n    ],\n  },\n  {\n    label: \"Administration\",\n    items: [\n      { id: \"users\", label: \"Users\", icon: Users, href: \"/users\" },\n      { id: \"roles\", label: \"Roles\", icon: Shield, href: \"/roles\", disabled: true },\n    ],\n  },\n];\n\nexport default function Shell() {\n  const [activeId, setActiveId] = useState(\"dashboard\");\n  const [collapsed, setCollapsed] = useState(false);\n\n  return (\n    <AppShell\n      sidebarCollapsed={collapsed}\n      sidebar={\n        <Sidebar\n          activeId={activeId}\n          collapsed={collapsed}\n          onSelect={setActiveId}\n          sections={sections}\n          linkComponent={NavLink}\n          product={{ name: \"CoreBooks\", role: \"Admin Console\", color: \"hsl(var(--primary))\" }}\n          onProductClick={() => {/* open entity switcher */}}\n          footer={\n            <div className=\"text-muted-foreground text-xs\">\n              <div className=\"text-foreground font-medium\">Satoshi Yamamoto</div>\n              <div>Online · Tokyo branch</div>\n            </div>\n          }\n        />\n      }\n      topbar={\n        <Topbar\n          start={\n            <>\n              {/* A bar cell is a TopbarItem, never a Button: a Button in a bar is a\n                  --control-height pill floating in a taller strip, with its own hover\n                  fill and its own focus ring. */}\n              <TopbarItem aria-label=\"メニュー\" onClick={() => setCollapsed((c) => !c)}>\n                <PanelLeft />\n              </TopbarItem>\n              <Logo mark=\"godx\" label=\"CoreBooks\" />\n            </>\n          }\n          end={<TopbarItem aria-label=\"検索\" onClick={() => {}}><Search /></TopbarItem>}\n        />\n      }\n    >\n      <>{/* page content */}</>\n    </AppShell>\n  );\n}`}\n",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "Sidebar",
    "props": [
      {
        "description": "Tên khả truy cập của landmark điều hướng. Bắt buộc khi một tài liệu có nhiều hơn một `<nav>`.",
        "name": "ariaLabel",
        "type": "string"
      },
      {
        "description": "Ref to the navigation's scroll container (`.sb-nav-scroll`, the overflow-y element) — persist and restore its scrollTop across navigations or remounts (gh#1136).",
        "name": "scrollRef",
        "type": "React.Ref<HTMLElement>"
      },
      {
        "description": "The id of the currently active nav item. For group items, the parent is automatically highlighted when any descendant id matches.",
        "name": "activeId",
        "required": true,
        "type": "string"
      },
      {
        "description": "Ordered list of nav sections. Each section has an optional string label and a required items array of SidebarItemProp. A row's count pill is `item.badge` (CONTENT ONLY — a number, a string, \"9+\"; never a <Badge> element, which would nest a pill inside the pill the row already draws) and its emphasis is `item.badgeTone`: \"neutral\" (default, the quiet unread pill) or \"destructive\" (the count is addressed to the user — an @mention, a DM, a failure waiting on them). A row's TRAILING GLYPH is a different slot: `item.trailingIcon` takes the COMPONENT (like `item.icon`, not an element) and renders a bare 16px mark at the row's inline end — the ⌃⌄ of a workspace switcher, a → on a row that leaves the app — with no pill, no background and no radius, hidden on the collapsed rail exactly like the badge.",
        "name": "sections",
        "required": true,
        "type": "SidebarSectionProp[]"
      },
      {
        "description": "Called with the item id when a leaf nav item is clicked. Not called for group triggers or disabled items.",
        "name": "onSelect",
        "type": "(id: string) => void"
      },
      {
        "defaultValue": "false",
        "description": "When true, renders the icon-only collapsed rail. Labels become Tooltips on hover; group items open a portaled flyout popover on click. Section labels are hidden.",
        "name": "collapsed",
        "type": "boolean"
      },
      {
        "description": "Renders a product/app chip at the top of the sidebar (name, optional role subtitle, optional color swatch). Mutually exclusive with brand — brand takes precedence.",
        "name": "product",
        "type": "SidebarProductProp"
      },
      {
        "description": "Click handler for the product chip button. Use to open an entity/workspace switcher sheet or dropdown.",
        "name": "onProductClick",
        "type": "() => void"
      },
      {
        "description": "Custom brand slot rendered above the nav scroll area. When provided, the product chip is not rendered. PASS A FUNCTION and it is called with the EFFECTIVE collapsed value, which is what a plain node cannot see: AppShell reuses this same Sidebar for the mobile drawer and the drawer un-collapses the rows, so a lockup built from the consumer's own `collapsed` boolean renders glyph-only inside a full-width drawer. The workaround consumers reach for — a second hand-built Sidebar passed as AppShell's `mobileNav` — is exactly the override that switches off `railInDrawer`, silently dropping the `navRail` from mobile. This slot is APP scope (the product lockup); a PLATFORM switch does not go here.",
        "name": "brand",
        "type": "ReactNode | ((collapsed: boolean) => ReactNode)"
      },
      {
        "description": "The rail's ONE primary action (Linear \"New issue\", Jira \"+ Create\", Notion \"New page\"), rendered between the brand header and the scrolling nav and never scrolling with it (gh#1185). Put exactly one primary control here: a Button full width (fullWidth) when expanded, icon-only with aria-label + Tooltip when collapsed. PASS A FUNCTION for the same reason brand and footer take one: it receives the EFFECTIVE collapsed value, so AppShell's mobile drawer (which never collapses) gets the full button from the same Sidebar node. Inset: --sidebar-action-padding (inline matches the nav rows). Do NOT put the action in `brand` - that slot is the product lockup.",
        "name": "action",
        "type": "ReactNode | ((collapsed: boolean) => ReactNode)"
      },
      {
        "description": "Slot pinned to the bottom of the sidebar below the scrollable nav area. Commonly used for user identity, online status, or a mode switch. PASS A FUNCTION for the same reason brand takes one, and it is the same reason twice: both slots sit INSIDE the collapsible rail, so both need the EFFECTIVE collapsed value, which a node built outside the component cannot see. Reading your own `collapsed` boolean renders a glyph-only footer inside AppShell's full-width drawer (the drawer un-collapses the rail); building a second Sidebar for AppShell's `mobileNav` is exactly the override that switches railInDrawer off; doing neither leaves the expanded footer to reflow inside a 64px rail (measured: a two-line identity block went 255x66 docked to 63x111 collapsed, wrapping the name over three lines). A plain ReactNode still works unchanged.",
        "name": "footer",
        "type": "ReactNode | ((collapsed: boolean) => ReactNode)"
      },
      {
        "description": "THE framework-router contract. Supply only the link ELEMENT TYPE; the library keeps composing every row — icon slot, label, badge, data-active/aria-current, the icon-only collapsed rail and its tooltip name — and passes it to the link as SidebarLinkProp children. Applied to every row that carries an href: top-level leaves, submenu children, collapsed-rail leaves and collapsed flyout entries. Build it with createSidebarLink(Link, 'to') for React Router / TanStack, createSidebarLink(Link) or inertiaSidebarLink(Link) from @godxjp/ui/inertia for Inertia / Next.js.",
        "name": "linkComponent",
        "type": "SidebarLinkComponentProp"
      },
      {
        "description": "DEPRECATED — use linkComponent, or asChild on SidebarItem. Legacy escape hatch that left row CONTENT to the caller, which is how a <Link>{item.label}</Link> silently dropped every icon and badge in production. Still supported and still wins over linkComponent; rowProps now also carries the library-composed children, so spreading rowProps (or rendering rowProps.children) restores the canonical row.",
        "name": "renderItem",
        "type": "(item: SidebarItemData, rowProps: SidebarRenderItemProp) => ReactNode"
      },
      {
        "description": "Override the nav landmark's accessible name (defaults to a localized \"Main navigation\"). Required when more than one Sidebar renders at once (e.g. a docked sidebar + its mobile-drawer twin) — two nav landmarks sharing one name/role fail landmark-unique.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "AppShell — the shell that hosts Sidebar in its sidebar slot and owns the sidebarCollapsed layout grid; always compose Sidebar inside AppShell, not standalone in a page.",
      "Topbar — the horizontal bar that renders the collapse toggle (onToggleCollapsed) and its collapsed prop must mirror the sidebar's collapsed state.",
      "PageContainer — used for page-level title/subtitle/extra/breadcrumb inside AppShell's children slot, not inside Sidebar."
    ],
    "rules": [
      3,
      6,
      23,
      31
    ],
    "storyPath": "layout/sidebar.tsx",
    "subParts": [
      "SidebarHeader",
      "SidebarItem",
      "SidebarSection"
    ],
    "tagline": "Data-driven vertical nav rail with collapsible submenu groups and a collapsed icon-only mode — never build nav manually with raw buttons.",
    "usage": [
      "DO: Define all nav items as a SidebarSectionProp[] data structure and pass it to sections — never hand-roll nav buttons alongside or instead of the Sidebar.",
      "DO: Add children: SidebarItemProp[] to any SidebarItemProp to create a collapsible submenu group. The group auto-opens and highlights when activeId matches any descendant. The SAME field drives NavList (gh#815), so one grouped settings nav is one NavList and one <nav> landmark.",
      "DO: Mirror the collapsed boolean between AppShell's sidebarCollapsed prop and Sidebar's collapsed prop — they must stay in sync so the shell layout grid adjusts correctly.",
      "DO: Use the footer prop for user info or status — it is pinned below the scroll area and does not scroll away. Pass a FUNCTION `(collapsed) => node` whenever that content has to shrink on the rail: it is called with the EFFECTIVE collapsed value, so ONE Sidebar serves both the docked rail and AppShell's drawer. Returning null for a surface draws no footer chrome at all (no top border, no padding).",
      "DO: Put a row's trailing disclosure mark in `item.trailingIcon`, never in `item.badge`. `badge` wraps whatever it is given in the `.sb-badge` count capsule — a chevron passed there measures 36x24 with a 24x24 SVG inside, beside a 16x16 leading icon in the same 32px row. `trailingIcon` takes the component (`trailingIcon: ChevronsUpDown`) and the rail pins it to the leading icon's 16px box with no surface of its own.",
      "DO: Give every navigable item an `item.href` and let the library render it. A plain `href` becomes a real <a> (context-menu open-in-new-tab, middle-click); with `linkComponent` that same href drives your framework router's <Link>. Either way the link IS the row and the ONLY interactive element (no nested <button>). Never put a <button>/<a> inside a default row.",
      "DO: Wire a framework router with `linkComponent` — `createSidebarLink(Link, 'to')` (React Router / TanStack), `createSidebarLink(Link)` (Next.js), or `inertiaSidebarLink(Link)` from `@godxjp/ui/inertia`. That is the WHOLE integration: you pass the element type, the library composes the row. It threads through leaves, submenu children, the collapsed rail and the collapsed flyout. When you compose rows by hand, the same contract is `<SidebarItem item={item} asChild><RouterLink to=… /></SidebarItem>` — write NO children; the library injects the icon, label and badge. (`RouterLink` here is YOUR router's link component; this library now exports a `Link` of its own — antd's Typography.Link — which takes `href`, not `to`.)",
      "DON'T: Use `renderItem` in new code — it is DEPRECATED. It hands you a className + active state and leaves the row CONTENT to you, so `renderItem={(item) => <RouterLink href={…}>{item.label}</RouterLink>}` renders a row with NO icon and NO badge. That is the exact production regression that motivated `linkComponent`. If you must keep it, render `rowProps.children` (the library-composed row) instead of hand-writing `.sb-icon` / `.sb-label` spans, which are internal class names and not a public contract.",
      "DO: Rely on route-synchronized group expansion — a group OPENS automatically whenever `activeId` moves to one of its children (e.g. after a deep-link navigation), revealing the newly-active child; users can still collapse/expand manually.",
      "DO: Theme the nav ICON and the row/label SEPARATELY with tokens — the icon reads `--sidebar-nav-icon-foreground` (+ `-hover-`/`-active-`/`-disabled-` variants) and the row/label reads `--sidebar-nav-item-foreground` (+ `-hover-`/`-disabled-`). Resting defaults are unchanged (both = `hsl(var(--muted-foreground))`, hover = `hsl(var(--foreground))`; the ACTIVE row is its own group, see below), so setting `--sidebar-nav-icon-foreground: hsl(var(--foreground))` in your theme is all it takes to get canonical darker 16px icons beside muted labels. NEVER write a page-local `.sb-nav-item svg { color: … }` rule and never re-tint `--muted-foreground` globally to fix sidebar icons.",
      "DO: Give every RAIL item an `icon`. It is OPTIONAL on SidebarItemProp (gh#815 — NavList has no collapsed rail and routinely mixes rows), but the canonical rail collapses to icon-only, so a row without one renders an EMPTY 16px slot: the geometry and the label column survive, and the collapsed rail reads as a hole.",
      "DO: Distinguish an UNREAD count from one ADDRESSED TO THE USER with `item.badgeTone` — 'neutral' (the default, the pill unchanged) versus 'destructive' for an @mention, a direct message or a failure awaiting them. It emits `data-tone=\"destructive\"` on the existing `.sb-badge` and swaps two colour tokens (`--sidebar-badge-destructive-background` / `-foreground`); the pill's min-width, radius, inline pad and font size are shared by both tones, so mention rows and unread rows stay aligned in the same column. Retune all four `--sidebar-badge-*` knobs in your theme rather than styling the pill.",
      "DON'T: Reach for `item.badge` to place a GLYPH (a chevron, an arrow, a status dot). That slot is a COUNT capsule — 9999px radius, `hsl(var(--secondary))` fill, sized for digits — and it does not pin the SVG, so a lucide glyph renders at its 24px default inside a 36x24 grey pill. The row's trailing glyph slot is `item.trailingIcon`.",
      "DON'T: Put a `<Badge>` (or anything else that draws its own pill) inside `item.badge` to colour a count — the row ALREADY wraps whatever you pass in a `.sb-badge` pill, so you get two nested pills with two borders (measured: a 37.11x19.14 `.sb-badge` wrapping a 25.11x19.14 `<Badge>`). Pass the CONTENT only (`badge: 3`, `badge: '9+'`) and say what it MEANS with `badgeTone`.",
      "DON'T: Change icon SIZE or row geometry through these colour knobs — icon size stays `--sidebar-nav-icon-size` (16px) and row geometry stays `--sidebar-nav-item-height` / `--sidebar-nav-item-gap` / `--sidebar-nav-item-padding-x`. The active row's fill/label keep `--sidebar-item-active-background` / `--sidebar-item-active-foreground` — and since gh#651 BOTH nav depths default to the brand, not to neutral grey: the level-1 fill is `--primary` composited at `--sidebar-item-active-background-alpha` (12%, capped at 16% — above that the label drops under WCAG 2.2 SC 1.4.3) and the label is `hsl(var(--primary))` at level 1 and level 2 alike. If your theme was setting `--sidebar-item-active-background: hsl(var(--primary) / 0.12)` and `--sidebar-item-active-foreground: hsl(var(--primary))` to get that look, DELETE the block — it is the default now. The level-2 label knob used to be spelled `--sidebar-item-active-color`; that name is gone with no alias.",
      "DON'T: Manage collapse state inside the Sidebar — it is stateless. Hoist the boolean to your shell/page state and pass it down via both AppShell.sidebarCollapsed and Sidebar.collapsed.",
      "DON'T: Nest children more than one level deep — only top-level items can have children; grandchild items are not rendered.",
      "DON'T: Put a PLATFORM switch in the sidebar — not an `OrgSwitcher`, not an app switcher, neither as a row nor stacked into `brand` under the product lockup. The sidebar is APP scope (this app's own sections); which organization or which app you are in survives changing app and belongs to `AppShell`'s `navRail` or to the topbar. Stacking a second lockup under the first also gives the sidebar header a different height from the topbar, which is the visible symptom people report as \"the two sides do not line up\".",
      "DON'T: Build a second Sidebar by hand for `AppShell.mobileNav` just to un-collapse it — the drawer already un-collapses the rows on its own (`NavSurface`), and `brand` takes a function so the lockup follows too. An explicit `mobileNav` also turns off the drawer's rail strip, so a `navRail` you passed stops appearing on mobile."
    ],
    "useCases": [
      "Admin application shell nav with grouped sections (e.g. Operations / Fulfillment / Administration) where the sidebar can be collapsed to an icon rail for more content space.",
      "Accounting app with a collapsible 'Ledger' group containing Journal, Chart of Accounts, and Period Close sub-pages — activeId reflects the current sub-page and the group stays open automatically.",
      "Multi-tenant SaaS where onProductClick opens an entity/legal-entity switcher sheet and product.role shows the active tenant name beneath the product logo.",
      "Any app using AppShell where navigation must degrade gracefully to an icon-only rail on narrow viewports or via a user toggle in the Topbar.",
      "Apps with infrequent-access admin pages (Users, Roles, Password) grouped in a dedicated section that appears below primary operations sections.",
      "A chat / messaging rail listing channels, where most rows carry a neutral unread count and only the channels that @mentioned the user carry `badgeTone: 'destructive'` — the rail answers \"does anything need me personally?\" at a glance, without a second pill or a hand-styled dot."
    ]
  },
  {
    "example": "import { Topbar, TopbarItem, AppShell } from \"@godxjp/ui/layout\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Avatar, AvatarFallback } from \"@godxjp/ui/data-display\";\nimport { AppSettingPicker } from \"@godxjp/ui/navigation\";\nimport { PanelLeftClose, Search } from \"lucide-react\";\n\n// The shell gives you slots; YOU decide what goes in them.\n<AppShell\n  sidebar={<MySidebar />}\n  topbar={\n    <Topbar\n      start={\n        <>\n          <TopbarItem aria-label={t(\"toggleSidebar\")} onClick={toggle}>\n            <PanelLeftClose />\n          </TopbarItem>\n          <Avatar className=\"rounded-md\">\n            <AvatarFallback className=\"bg-primary text-primary-foreground font-bold\">C</AvatarFallback>\n          </Avatar>\n        </>\n      }\n      center={\n        <TopbarItem onClick={openSearch}>\n          <Search />\n          {t(\"search\")}\n        </TopbarItem>\n      }\n      end={\n        <>\n          <AppSettingPicker kind=\"locale\" />\n          <UserMenu />\n        </>\n      }\n    />\n  }\n>\n  {children}\n</AppShell>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "Topbar",
    "props": [
      {
        "description": "Opt-in pinned shared-device identity: one TopbarItem truncate with Avatar in icon and a string name. Capped at --topbar-identity-max-inline-size (40%); --topbar-identity-min-text-inline-size reserves 3ch. Stays outside scroll/menu while primary cells retain their minima. Only other clusters follow overflow; children overrides all slots. Do not combine identity with hideBelow, labelHideBelow or iconHideFrom. At extreme widths choose overflow=\"menu\" to keep other controls reachable.",
        "name": "identity",
        "type": "ReactNode"
      },
      {
        "description": "Instance inset using logical token steps.",
        "name": "pad",
        "type": "PadProp"
      },
      {
        "description": "bar uses shell bar height/inset. Renders a div, suitable for nested panel chrome.",
        "name": "height",
        "type": "\"bar\" | \"auto\""
      },
      {
        "defaultValue": "undefined",
        "description": "Inline-start cluster — typically the sidebar toggle (a `Button` with a `PanelLeftClose` icon), the brand mark (an `Avatar`), and primary nav.",
        "name": "start",
        "type": "ReactNode"
      },
      {
        "defaultValue": "undefined",
        "description": "Center cluster (grows + centers) — optional. e.g. a search trigger (`Button` + `Search` icon opening your command palette) or a page/entity switcher (`DropdownMenu`). ⚠ HIDDEN AT 1100px AND BELOW by default: the slot follows `--topbar-center-compact-display` (default `none`) so it cannot cover the start breadcrumb/title or the end utilities when a 16rem sidebar is docked. A global search placed here is therefore invisible on tablets and every phone until you set `--topbar-center-compact-display: flex`.",
        "name": "center",
        "type": "ReactNode"
      },
      {
        "defaultValue": "undefined",
        "description": "Inline-end cluster — settings pickers (`AppSettingPicker kind=\"locale\"|\"theme\"`), a notifications `Button`, the user-menu `DropdownMenu`.",
        "name": "end",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"scroll\"",
        "description": "What a bar that does not fit does with the cells that do not fit (gh#728). scroll (default) lets the BAR scroll on the inline axis while the start and end clusters keep their cells whole — every cell stays in the DOM and in the accessibility tree, Tab reaches each one in order, and the browser scrolls a focused cell into view; the scrollbar is suppressed (a horizontal bar inside a 48px chrome row would eat the height that caused the deficit), so the pointer affordance is drag/wheel. clip is the pre-gh#728 behaviour: a cell past the cluster's edge is simply not painted — still focusable, still announced, invisible and unreachable by pointer. Independent of TopbarItem hideBelow, which REMOVES a cell at a step; this is only about what happens once there is no budget left.",
        "name": "overflow",
        "type": "\"scroll\" | \"clip\" | \"menu\""
      },
      {
        "defaultValue": "undefined",
        "description": "Escape hatch — render fully custom bar content instead of the three slots. When set, `start`/`center`/`end`/`identity` are ignored.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Merged onto the bar element; arbitrary props (aria-*, etc.) are forwarded.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "AppShell — place Topbar in its `topbar` slot. AppShell also exposes its own `logo`/`topbarLeft`/`topbarRight` slots if you don't want a separate Topbar at all.",
      "Avatar — the brand mark for the `start` slot (use `shape=\"square\"` for a product glyph; never a `rounded-*` utility, which freezes the radius where no theme can reach it).",
      "AppSettingPicker — locale/theme/timezone/currency picker; the consumer drops it into `end`. Its appearance (icon-only, labelled, bordered) is configured on IT, not on Topbar.",
      "DropdownMenu — wrap a `Button` to build an entity switcher or user menu yourself, then place it in a slot."
    ],
    "rules": [
      2,
      3,
      5,
      6
    ],
    "storyPath": "layout/topbar.tsx",
    "tagline": "A PURE SLOT bar for the app shell — positions three clusters (start / center / end) and owns ONLY the bar layout. It bakes NO chrome: no product switcher, no search box, no notification bell, no language picker. The CONSUMER composes those from real primitives and drops them into a slot; whether a control is icon-only / labelled / bordered is that control's own config, never the shell's.",
    "usage": [
      "For shared devices use identity={<TopbarItem truncate icon={<Avatar aria-hidden=\"true\"><AvatarFallback>DB</AvatarFallback></Avatar>}>{user.name}</TopbarItem>}, optionally wrapped in DropdownMenuTrigger asChild. Keep the locale picker icon-only with appearance=\"bar\" in end. The name stays visible and bounded; full string children become title and accessible text. AccountChip is a separate avatar/name/action row, not a full-height menu cell.",
      "DO use `TopbarSearch`, not a bare `SearchInput`, for the bar's search (ADR C02): a SearchInput in a slot shrinks with the cluster — measured at 390px in a consumer bar its text area was 0ch. TopbarSearch keeps at least 8ch from 40rem up and folds to a named search cell below it.",
      "DO align the bar's search with the content column (ADR C11) by composition: keep AppShell's default `topbarSpan=\"content\"` and put `TopbarSearch` FIRST in `start` — the bar then starts on the rail edge with the same inset as PageContainer, measured 0px apart at 1024/1280/1440. Under `topbarSpan=\"full\"` the bar starts at the window edge after the brand, so no keyline is shared; there, put the search in `end` deliberately rather than near a column it cannot meet.",
      "DO compose the bar yourself: a brand mark (an `Avatar`) + sidebar toggle in `start`, a search trigger in `center`, settings pickers + notifications + user menu in `end`. The shell only positions; it never decides WHICH controls exist.",
      "DO build the sidebar toggle as a `TopbarItem` with a `PanelLeftClose`/`PanelLeftOpen` icon and your own `t()` aria-label, wired to AppShell's `sidebarCollapsed`. There is no baked toggle — but there IS a bar CELL, and it is not a Button: a Button in a slot is a --control-height pill floating in a taller bar, with its own hover fill and a ring drawn around the pill instead of the cell. The same holds for the notifications bell and the account trigger.",
      "DO put a locale/theme switcher in `end` using `AppSettingPicker` (or your own control) — icon-only vs labelled, bordered vs not, is THAT component's prop, not Topbar's. Topbar does not ship or force a language picker.",
      "DON'T wrap two+ TopbarItems in a `<Flex>` to group them in one slot (gh#883) — the slot is already a flex line with its own gap, and a `<Flex>` wrapper collapses to 16px and takes every item inside it down with it. Use a fragment (`<>…</>`, no DOM node) or pass an array instead; see TopbarItem's own DON'T for the measured before/after.",
      "DON'T look for `product`/`project`/`onSearchOpen`/`onNotificationsOpen`/`collapsed` props — they were removed. A chrome control only exists if YOU put it in a slot, so there is never a dead dropdown / empty search with nothing behind it.",
      "DO render Topbar inside `AppShell`'s `topbar` slot (or any `<header>`). For a non-three-cluster layout, pass `children` and lay it out yourself.",
      "DO decide, explicitly, what happens to the `center` slot at 1100px and below. It is REMOVED there by default (`--topbar-center-compact-display: none`) so it cannot cover the start or end clusters when a 16rem sidebar is docked — which also means a global search trigger in `center` is gone on tablets AND phones. This default arrived in 18.6.0 and changed behaviour for consumers who touched nothing but their lockfile. If your center content already has a compact presentation (an icon-only search trigger), opt back in globally with `:root { --topbar-center-compact-display: flex; }`; if it does not, move the trigger into `end` for compact widths. Never re-create either behaviour with a page-local media query.",
      "DO rely on the built-in shrink contract instead of hand-tuning widths: the bar never exceeds its shell allocation, `start` shrinks first and `center` yields its whole box, a long tenant/brand string truncates rather than spilling over a sibling or leaking a horizontal document scroll, and `end` keeps its natural width anchored inline-end — the locale picker and user menu stay visible at 1024px with a 16rem sidebar. If a label must degrade gracefully rather than be cut, give THAT element `truncate`/`text-overflow` yourself; don't add `overflow`/`flex` overrides to the slots.",
      "KNOW what happens when the shrink contract runs out: the BAR scrolls (`overflow=\"scroll\"`, the default), it does not slice. The start cluster keeps a floor of one bar cell and the end cluster keeps its cells whole, so what does not fit is off-port rather than half-painted — Tab still reaches every cell and the browser scrolls the focused one fully into view. Measured at 320x568 on a bar 44px over budget: start cluster 0 -> 32px, end cluster clientWidth 150.3/scrollWidth 285 -> 204/204, last end cell x=384.8 (outside a 390px viewport) -> fully inside it, 7/7 cells reachable by keyboard. `overflow=\"clip\"` restores the pre-gh#728 slicing for a bar you have measured as never overflowing (gh#728).",
      "KNOW the shrink contract reaches only the LAST child of `start` — and `Button` ships `shrink-0`, so any Button you put mid-slot (the classic entity switcher, with a brand mark before it and a screen title after) keeps its full width while the cluster clips it. Clipped, but still focusable: a keyboard user tabs to a control they cannot see (SC 2.4.7). Give such a control `fill` so it takes the leftover room and lets its label ellipse, and wrap that label in `<Text truncate>` — NOT `className=\"min-w-0 flex-1\"` plus a `truncate` span, which is the utility pair ui-audit blocks and which this guidance used to recommend. Budget the `end` cluster too — it is `flex: 0 1 auto` and shrink-weighted, but a cell in it is still a cell you chose to put on a phone: an ambient status chip there is space the start cluster does not get (a 93px environment Badge left `start` 25px of a 198px bar at 320). Hide ambient chips below `sm`."
    ],
    "useCases": [
      "Admin shell: `start` = sidebar toggle + brand mark (`Avatar`) + an entity switcher (`DropdownMenu` around a `Button`); `center` = a `Button` search trigger; `end` = `AppSettingPicker` (locale) + a notifications `Button` + a user `DropdownMenu`.",
      "Minimal shell (no search, no notifications): pass only `start` (brand mark) and `end` (user menu). Nothing else renders — no empty chrome.",
      "Marketing / docs header: pass `children` with a fully custom flex layout when the three-cluster model doesn't fit."
    ]
  },
  {
    "example": "import { Topbar, TopbarItem } from \"@godxjp/ui/layout\";\nimport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from \"@godxjp/ui/navigation\";\n\n<Topbar\n  end={\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <TopbarItem aria-label=\"Account\">\n          <Avatar><AvatarFallback>SD</AvatarFallback></Avatar>\n        </TopbarItem>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">…</DropdownMenuContent>\n    </DropdownMenu>\n  }\n/>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "TopbarItem",
    "props": [
      {
        "defaultValue": "false",
        "description": "Same vocabulary as Text.truncate: allow the cell to shrink to its control minimum and ellipse only its label. Keeps the icon; overrides labelHideBelow. Full text remains accessible; string children supply title automatically (including asChild). Pass title explicitly for rich children. Use in Topbar identity for a bounded always-visible shared-device name; default phone accounts still use labelHideBelow=\"sm\".",
        "name": "truncate",
        "type": "boolean"
      },
      {
        "description": "The cell's leading glyph, placed in a slot the CELL sizes (--topbar-icon-size). Prefer it over passing the glyph as a child whenever the glyph is wrapped: `.ui-topbar-item > svg` only reaches a DIRECT child, so a glyph inside a `<Flex hideBelow>` renders at lucide's intrinsic 24px — measured at 390px on an org-switcher cell, 1.5x the cell's step (gh#712). Decorative by convention (the cell's name comes from its own aria-label); pass <Icon as={…} size=… label=… /> here when the glyph needs a name or a different step. Rendered with asChild too (gh#726): the sized slot is inserted INSIDE the child element, ahead of that element's own children, and the child element (e.g. an <a>) stays the rendered root with every merged prop.",
        "name": "icon",
        "type": "ReactNode"
      },
      {
        "description": "Drop the icon slot FROM a breakpoint step upwards (Flex hideFrom's contract, scoped to the glyph), so the cell reads label-only on a wide bar. Decorative: the accessible name is unaffected (gh#726).",
        "name": "iconHideFrom",
        "type": "BreakpointProp"
      },
      {
        "description": "VISUALLY hide the label BELOW a breakpoint step (Flex hideBelow's contract, scoped to the label), so the cell collapses to icon-only on a narrow bar. The label stays in the accessibility tree (clipped, not display:none), so the icon-only cell keeps its accessible name. Under asChild the child element's own children are the label (gh#726).",
        "name": "labelHideBelow",
        "type": "BreakpointProp"
      },
      {
        "description": "Overlaid count; does not change the bar cell width. Rendered under asChild too (gh#1171): the pill goes inside the child element after its children, so `<TopbarItem asChild badge={3}><a href=\"/notifications\">…</a></TopbarItem>` keeps its count. Nothing renders for `undefined`, `\"\"` or the number `0` (gh#1341) — pass the count as it is, never `count === 0 ? undefined : count`; pass the string `\"0\"` to show a zero on purpose.",
        "name": "badge",
        "type": "ReactNode"
      },
      {
        "description": "Count tone, shared with Sidebar badges.",
        "name": "badgeTone",
        "type": "\"neutral\" | \"destructive\""
      },
      {
        "description": "Hide the bar cell below a design-system breakpoint without interrupting its stretch chain.",
        "name": "hideBelow",
        "type": "BreakpointProp"
      },
      {
        "defaultValue": "false",
        "description": "Render the bar-cell shape onto the child instead of emitting a <button> — for a router link, or a menu/popover trigger that must own the element.",
        "name": "asChild",
        "type": "boolean"
      },
      {
        "description": "Cell content — a glyph, a label, an Avatar, or any pair of them.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Root class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Topbar (the slot bar it belongs to)",
      "Button (the right control everywhere that is not a bar)",
      "AppShell (owns the bar height it stretches to)"
    ],
    "rules": [
      2,
      3,
      5,
      6
    ],
    "storyPath": "layout/topbar-item.tsx",
    "tagline": "ONE interactive cell of a Topbar slot — the account button, a settings or notifications trigger. Full bar height, the bar's own hover surface, and the focus mark hosted INSIDE the cell. Use it INSTEAD OF a Button in a Topbar slot: a Button there is a --control-height pill floating in a taller bar, with its own hover fill and a ring drawn around the pill.",
    "usage": [
      "By default collapse the account cell to its avatar on a phone (ADR C04); shared devices opt into Topbar identity + TopbarItem truncate instead: `<DropdownMenuTrigger asChild><TopbarItem aria-label={t(\"account\")} icon={<Avatar>…</Avatar>} labelHideBelow=\"sm\">{name}</TopbarItem></DropdownMenuTrigger>` and repeat the name (and email) in the menu's `DropdownMenuLabel`. Measured at 390px: without `labelHideBelow` the name took 74px of a 390px bar; with it the cell is 48x47, named, and the name is read in the open menu.",
      "DO put bar triggers in a Topbar slot as TopbarItem, not as Button — that is the difference between chrome and a control that landed in the chrome.",
      "DO wrap it in a DropdownMenuTrigger asChild for a user menu; the open state lights the cell via [data-state=open].",
      "DON'T set a height: the cell stretches to whatever the bar is (AppShell's grid row, --topbar-height, or the coarse-pointer bar), which is why there is no height knob.",
      "DO collapse a cell by breakpoint with its own props, never by hand-wrapping the glyph: `<TopbarItem asChild icon={<Target />} labelHideBelow=\"sm\"><a href=\"/goals\">Goals</a></TopbarItem>` replaces `<Flex hideFrom=\"sm\"><Icon as={Target} size=\"md\" /></Flex>` — icon-only below sm, the label still the accessible name. Add `iconHideFrom` for a label-only cell from a step up (gh#726).",
      "DON'T reach for it outside a Topbar — a full-bleed cell needs a bar to bleed to. Use Button anywhere else.",
      "DON'T wrap two or more TopbarItems in a `<Flex>` to place them together in one slot (gh#883) — a `<Flex>` is a REAL element with `align-self: auto` by default, so it sits between the item and the slot, collapses to its own content height, and every item inside it stretches only to THAT (measured: a 47px bar down to 16px). The slot (`.ui-topbar-start`/`-center`/`-end`) is already a flex line with its own `gap`, so it needs no wrapper at all: put a JSX FRAGMENT `<>…</>` around the items (renders no DOM node, so each item is still the slot's DIRECT child) or pass an array — `end={[<TopbarItem key=\"notifications\" …/>, <TopbarItem key=\"account\" …/>]}` — either way every item keeps its own `align-self: stretch` reaching the real bar height. Reach for `<Flex>` there only when you deliberately want the group NOT full height (rare in a topbar)."
    ],
    "useCases": [
      "Account / user-menu trigger in the topbar end slot",
      "Notifications bell, settings or theme trigger",
      "Sidebar collapse toggle in the topbar start slot"
    ]
  },
  {
    "example": "import { Topbar, TopbarSearch } from \"@godxjp/ui/layout\";\n\n<Topbar\n  start={\n    <TopbarSearch\n      ariaLabel={t(\"searchInCollection\")}\n      placeholder={t(\"searchInCollection\")}\n      onSearch={(q) => navigate({ to: \"/search\", search: { q } })}\n    />\n  }\n/>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "TopbarSearch",
    "props": [
      {
        "defaultValue": "\"sm\"",
        "description": "The step below which the field folds to a TopbarItem search cell (sm 40rem · md 48rem · lg 64rem · xl 80rem). CSS decides at the step, so SSR and the first paint agree and the typed query survives a resize.",
        "name": "collapseBelow",
        "type": "BreakpointProp"
      },
      {
        "description": "The search's accessible name — the field's AND the folded cell's. Defaults to the localized \"Search\". Name the scope (\"Search this collection\").",
        "name": "ariaLabel",
        "type": "string"
      },
      {
        "description": "Every SearchInput prop passes through unchanged: value / defaultValue / onValueChange, onSearch, placeholder, debounce, shortcut, status, disabled.",
        "name": "…SearchInputProp",
        "type": "SearchInputProp"
      }
    ],
    "related": [
      "SearchInput (the field it renders)",
      "TopbarItem (the folded cell)",
      "Topbar (the slot bar it belongs to)",
      "CommandPalette (⌘K jump-to; a different task from searching content)"
    ],
    "rules": [
      2,
      3,
      5,
      6
    ],
    "storyPath": "layout/topbar-search.tsx",
    "tagline": "The bar's one search box. From 40rem up an inline SearchInput whose text area never shrinks below 8ch; below it a named search cell that opens the field across the whole bar (Material 3 search bar → search view). Escape or the close cell folds it and returns focus to the cell.",
    "usage": [
      "DO use it for the search in a Topbar slot instead of a bare SearchInput: a SearchInput there shrinks with the cluster (0ch of text at 390px, measured in a consumer bar).",
      "DO name the scope in `ariaLabel`; it is the folded cell's only name.",
      "DO put it FIRST in `Topbar start` under the default `topbarSpan=\"content\"` when the search belongs to the page column: it then starts on the content keyline (0px apart at 1280).",
      "DON'T also render a separate search icon button for phones — the folded cell IS that button.",
      "Keyboard: Enter/Space on the cell opens and focuses the field; Escape closes and returns focus; Tab out of the open field folds it and leaves focus where it went. The cell is an APG disclosure (aria-expanded / aria-controls)."
    ],
    "useCases": [
      "A collection or workspace search in the app bar",
      "A global search that must survive a 320–390px phone bar"
    ]
  },
  {
    "example": "<NavList\n  label=\"Settings\"\n  activeId={route}\n  linkComponent={Link}\n  items={[\n    // A GROUP: one NavList, one <nav>, however many groups (gh#815).\n    {\n      id: \"account\",\n      label: \"Account\",\n      icon: User,\n      children: [\n        { id: \"profile\", label: \"Profile\", href: \"/settings/profile\" },\n        { id: \"organization\", label: \"Organization\", href: \"/settings/organization\" },\n      ],\n    },\n    // A LEAF, at the same level. `icon` takes the COMPONENT, never an element, and is optional.\n    { id: \"appearance\", label: \"Appearance\", icon: Palette, href: \"/settings/appearance\" },\n  ]}\n/>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "NavList",
    "props": [
      {
        "description": "Rows in reading order. Same shape as the rail's items, deliberately: one item vocabulary for both navigations. An item carrying `children` renders as a collapsible GROUP — the rail's own `.sb-nav-group`, opened by the route whenever a descendant is active (gh#815). `icon` is optional; a row without one keeps an empty 16px slot so the label column still aligns.",
        "name": "items",
        "required": true,
        "type": "SidebarItemProp[]"
      },
      {
        "description": "`id` of the current route. That row gets the active tokens and aria-current=\"page\".",
        "name": "activeId",
        "type": "string"
      },
      {
        "description": "Accessible name for the <nav> landmark. Required because a page routinely holds more than one navigation (breadcrumb, rail, this one).",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "description": "Router link component — identical contract to Sidebar.linkComponent. The library composes the row and passes it as children; the component only renders the <a>.",
        "name": "linkComponent",
        "type": "SidebarLinkComponentProp"
      },
      {
        "description": "Reports the activated row's id, for consumers driving navigation themselves.",
        "name": "onSelect",
        "type": "(id: string) => void"
      }
    ],
    "related": [
      "Sidebar",
      "MasterDetail",
      "PageContainer"
    ],
    "rules": [
      2,
      3,
      5,
      6
    ],
    "storyPath": "layout/nav-list.tsx",
    "tagline": "Vertical route navigation for INSIDE a page — the shape a settings nav takes. Renders the same `.sb-nav-item` rows as the Sidebar rail (icon column, label, badge, active tokens, `aria-current=\"page\"`) in a `<nav>` landmark, without the AppShell grid the rail depends on.",
    "usage": [
      "DO use NavList for a settings / account / preferences sub-navigation beside the pane it drives — typically as MasterDetail's `master`.",
      "DO group with `item.children` — one NavList, one `<nav>` landmark, however many groups. Before gh#815 NavList dropped `children` silently (it type-checked, rendered one flat row, and the subtree vanished), so the workaround was one NavList per group wrapped in SidebarSection, i.e. N landmarks for one navigation. Don't write that any more.",
      "DON'T repeat a group's icons on its children: a nested row draws the rail's dot marker instead of the 16px icon column, exactly like the rail's submenu. Icons belong on the rows that sit at the top level.",
      "DON'T reach for Sidebar here: it is the app's primary rail and lays into AppShell's grid area, so nested in a page it has no grid to lay into.",
      "DON'T hand-roll the rows out of Buttons with the current one encoded as a variant swap. That loses aria-current, loses the icon column the labels align to, and invents a different nav in every app.",
      "DON'T use Tabs: each entry here is a separate ROUTE the router renders, not a panel this component owns. Tabs would mean faking tab state from the URL and never rendering a TabsContent.",
      "DO pass `label` — the <nav> landmark needs a name to be distinguishable from the breadcrumb and the rail.",
      "There is no collapsed state by design: a page-level navigation has no rail to collapse into. Only the shell rail collapses. (A GROUP still opens and closes — that is `item.children`, not the rail's collapsed mode.)"
    ],
    "useCases": [
      "Settings page / settings nav / preferences nav: Account / Security / Notifications / Billing beside the selected settings form — grouped, in ONE <nav> landmark.",
      "Settings screen: Profile / Appearance / Security beside the selected settings form.",
      "Account area: a vertical route nav inside a PageContainer, driving the detail pane."
    ]
  },
  {
    "example": "import { MasterDetail } from \"@godxjp/ui/layout\";\n\n// Canonical: fluid list + fixed 320px detail rail (stacks below 40rem).\n<MasterDetail\n  masterLabel=\"Teams\"\n  detailLabel=\"Selected team\"\n  detailId=\"team-detail\"\n  master={<TeamTable onRowClick={select} detailId=\"team-detail\" />}\n>\n  <TeamDetail team={selected} />\n</MasterDetail>\n\n// Leading navigator rail instead.\n<MasterDetail\n  rail=\"master\"\n  railWidth=\"compact\"\n  masterLabel=\"Categories\"\n  master={<CategoryNav activeId={activeCategory} onSelect={setActiveCategory} />}\n>\n  <SettingsForm />\n</MasterDetail>\n\n// A long real collection: bound the master so it scrolls in place and the detail\n// stays near the top of a stacked mobile page.\n<MasterDetail\n  masterViewport=\"compact\"\n  masterLabel=\"Members\"\n  detailLabel=\"Selected member\"\n  master={<MemberList onRowClick={select} />}\n>\n  <MemberDetail member={selected} />\n</MasterDetail>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "MasterDetail",
    "props": [
      {
        "description": "One-pane navigation below the token-owned collapse threshold; omit to retain stacking. The consumer owns URL/history selection.",
        "name": "mobilePane",
        "type": "\"master\" | \"detail\""
      },
      {
        "description": "Back link shown above detail in mobile navigation mode.",
        "name": "detailBack",
        "type": "ReactNode"
      },
      {
        "description": "Selectable collection. Always FIRST in DOM order, so the stacked (mobile) order is list-then-detail whichever region owns the rail.",
        "name": "master",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Detail surface for the current master selection.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"detail\"",
        "description": "`master` = leading category/navigator rail beside a fluid detail surface.",
        "name": "rail",
        "type": "\"master\" | \"detail\""
      },
      {
        "description": "Token rail steps: 12rem, 18.75rem, 20rem, 24rem.",
        "name": "railWidth",
        "type": "\"narrow\" | \"compact\" | \"standard\" | \"wide\""
      },
      {
        "defaultValue": "\"auto\"",
        "description": "Bound the master collection to a scrollable viewport. `auto` (default) never bounds it — the region grows with its content. `compact` (--master-detail-master-viewport-compact, 20rem) / `standard` (28rem) cap its block size and scroll the collection INSIDE the region, and make that region a keyboard-reachable scroll container (tabIndex 0, --master-detail-master-viewport-inset reserves focus-ring room). No raw pixel prop exists — retune the tokens in a service theme.",
        "name": "masterViewport",
        "type": "\"auto\" | \"compact\" | \"standard\""
      },
      {
        "description": "Per-instance stacking threshold (sm=40rem, md=48rem, lg=64rem, xl=80rem; false never stacks). OMIT it to inherit the themeable --master-detail-collapse-below token (default 40rem) — that is the global knob a service theme retunes once.",
        "name": "collapseBelow",
        "type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | false"
      },
      {
        "description": "Accessible name for the master region landmark.",
        "name": "masterLabel",
        "type": "string"
      },
      {
        "description": "Accessible name for the detail region landmark.",
        "name": "detailLabel",
        "type": "string"
      },
      {
        "description": "Id of the detail region so the selection controls inside `master` can point at it with aria-controls, and the app can move focus to it after a selection (the region carries tabIndex={-1} for exactly this).",
        "name": "detailId",
        "type": "string"
      }
    ],
    "related": [
      "SplitPane — a main surface plus a COMPLEMENTARY aside (its own content), not a selection-driven detail; use MasterDetail when the trailing pane is the detail OF the selection.",
      "ResponsiveGrid — use for equal-width independent cards, never for a master-detail relationship.",
      "PageContainer — outer page scaffold that supplies page insets and title hierarchy around MasterDetail."
    ],
    "rules": [
      24,
      40
    ],
    "storyPath": "layout/master-detail.tsx",
    "tagline": "Responsive master-detail composition: a fluid list beside a token-owned 300px/320px fixed rail, with a themeable collapse threshold. `rail` picks which region is fixed — default `detail` (the canonical 1fr/320px list + detail rail); `master` for a leading navigator rail.",
    "usage": [
      "DO use MasterDetail for team, member, service or settings screens where a selectable collection drives a detail surface. Default `rail=\"detail\"` gives the canonical fluid list + 320px detail rail; pass `rail=\"master\"` for a leading navigator rail.",
      "DO keep selection and keyboard semantics on the controls inside `master` (`aria-pressed`, roving focus, or listbox semantics as appropriate) — only the caller knows which APG pattern applies; MasterDetail preserves whatever you render.",
      "DO wire the two together: give the master controls `aria-controls={detailId}`, and move focus to `document.getElementById(detailId)` when a selection replaces the detail (the region is `tabIndex={-1}` so that focus call works).",
      "DO choose `compact` for the 300px rail and `standard` for 320px. Never reproduce these tracks with consumer CSS.",
      "DO provide `masterLabel` and `detailLabel` when the surrounding headings do not already identify both regions.",
      "Use ResponsiveGrid.Item span for proportional tracks, or MasterDetail when the rail needs a bounded width.",
      "DO set `masterViewport=\"compact\"` (or `\"standard\"`) whenever the collection is a REAL, unbounded list. Left at `auto` a 200-row list renders ~3,700px tall, and once the layout stacks the detail lands thousands of pixels below the fold; bounded, the collection scrolls inside the rail and the detail stays near the top of the screen. Pass `masterLabel` too, so the scroll region is announced.",
      "DON'T reproduce that bound with a consumer `max-height`/`overflow` rule or a pixel prop — there is none by design. Retune `--master-detail-master-viewport-compact` / `-standard` in the service theme, and `--master-detail-master-viewport-inset` if the collection's focus ring needs more room.",
      "The collapse threshold is a real token: below `--master-detail-collapse-below` (default 40rem, measured against the COMPOSITION's own inline size, never the viewport) master stacks above detail. A theme retunes it globally, `collapseBelow` overrides it per instance. It is implemented as a flex-basis threshold rather than a media query precisely because a query CONDITION cannot read a var().",
      "Measured geometry with the defaults: 1440 → fluid master + 320px rail; 1024 → fluid master + 320px rail; 390 → stacked full-width master then detail. Same JSX at every width — no consumer-local CSS, grid tracks or per-screen spacing."
    ],
    "useCases": [
      "Teams screen: the team DataTable stays fluid while the selected team's detail keeps the 320px rail.",
      "Organization service access: services in the compact leading rail (`rail=\"master\"`) and selected service roles in the fluid detail surface.",
      "Settings navigator: categories in the leading rail and the selected configuration form in the detail surface."
    ]
  },
  {
    "docPath": "layout/draggable-panel.tsx",
    "example": "import { useState } from \"react\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { DraggablePanel } from \"@godxjp/ui/lab\";\nimport { Text } from \"@godxjp/ui/general\";\n\nfunction Assistant() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  return (\n    <DraggablePanel\n      title=\"アシスタント\"\n      placement=\"bottom-end\"\n      position={position}\n      onPositionChange={setPosition}\n    >\n      <Flex direction=\"col\" gap=\"sm\">\n        <Text size=\"sm\">この請求書の消費税区分について質問できます。</Text>\n      </Flex>\n    </DraggablePanel>\n  );\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/lab",
    "name": "DraggablePanel",
    "props": [
      {
        "description": "Panel name, shown in the title bar. A string also becomes the region's accessible name.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Panel body. Text stays selectable: only the title bar starts a drag.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Trailing slot in the title bar, before the close control.",
        "name": "extra",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"bottom-end\"",
        "description": "Resting corner before any movement. Logical directions, so it mirrors for an RTL locale with no extra work.",
        "name": "placement",
        "type": "\"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Panel width from the token ladder (18rem / 22rem / 28rem / xl → --centered-shell-width-md).",
        "name": "width",
        "type": "\"sm\" | \"md\" | \"lg\" | \"xl\""
      },
      {
        "defaultValue": "\"both\"",
        "description": "react-draggable's axis, ported verbatim. `none` keeps the panel mounted but pinned where it started.",
        "name": "axis",
        "type": "\"both\" | \"x\" | \"y\" | \"none\""
      },
      {
        "defaultValue": "\"viewport\"",
        "description": "react-draggable's bounds. `viewport` keeps the whole panel on screen so it can never be thrown off-screen and stranded. Its selector and {left, top, right, bottom} forms are deliberately absent: a selector reaches into internal DOM, and the object is spelled in physical directions that cannot mirror under RTL.",
        "name": "bounds",
        "type": "\"viewport\" | \"none\""
      },
      {
        "description": "react-draggable's controlled and uncontrolled offset from the resting corner, in CSS pixels on the physical axes. Pass `position` together with `onPositionChange` to own the value.",
        "name": "position / defaultPosition",
        "type": "{ x: number; y: number }"
      },
      {
        "description": "Fires with the CLAMPED offset after every pointer frame and every keyboard nudge. The panel reports where it is and never remembers it — where that is stored, and per what scope, is your decision.",
        "name": "onPositionChange",
        "type": "(position: { x: number; y: number }) => void"
      },
      {
        "description": "Presence renders the close control in the title bar (antd Modal's onCancel). Omit it for a panel the page controls entirely.",
        "name": "onClose",
        "type": "() => void"
      },
      {
        "description": "Optional localized strings for the close and move controls. Each key wins over t() when set. For script-injected embeds that cannot mount AppProvider without restyling the host page (gh#606).",
        "name": "labels",
        "type": "{ close?: string; move?: string }"
      },
      {
        "defaultValue": "false",
        "description": "react-draggable's disabled. The panel stays exactly where it is and the handle stops moving it, by pointer and by keyboard alike.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Root class. Geometry lives in the --draggable-panel-* tokens.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "ResizablePanel — resizes panes WITHIN a layout (react-resizable-panels). It changes how much room a pane gets; it cannot move a floating element around the viewport.",
      "Sheet — a panel pinned to an edge of the screen, with a scrim and focus handling. Reach for it when the panel is a step in a flow rather than a companion to the page.",
      "Popover — anchored to its trigger and closed on outside press. Reach for it for something short-lived beside a control, not for a surface that stays.",
      "Dialog — modal and focus-trapping. Reach for it when the page behind must be unusable until the person answers."
    ],
    "rules": [
      23,
      44
    ],
    "storyPath": "layout/draggable-panel.tsx",
    "tagline": "A floating surface the person using it can MOVE — drag it by the title-bar handle, or focus the handle and nudge it with the arrow keys. Bounded to the viewport, position reported through onPositionChange and never stored by the library.",
    "tier": "lab",
    "usage": [
      "DO import from `@godxjp/ui/lab` (the opt-in lab subpath): `import { DraggablePanel } from \"@godxjp/ui/lab\";`",
      "DO treat the position as yours. The panel reports it and stores nothing, so persisting it per user, per workspace or not at all is a decision you make.",
      "DO keep the body text short enough to read at 22rem, or let it scroll — the body is its own scroll region and the panel is capped at 70vh.",
      "DON'T hand-roll this with a fixed-position div and pointer maths: that is page-local CSS ui-audit blocks, and every consumer that floats anything would write it again.",
      "DON'T reach for it when the surface should interrupt. This is a labelled region, not a dialog: it traps no focus, dims no page and announces nothing. Use Dialog when the decision must be made before anything else can happen.",
      "DON'T drag from the whole surface by wrapping the body in your own pointer handler — dragging from anywhere makes the text unselectable in a panel whose whole purpose is text."
    ],
    "useCases": [
      "A floating assistant or chat box over a data-dense screen, moved aside when it covers the column being asked about.",
      "A persistent calculator, unit converter or note pad kept on screen while the person works in the page behind it.",
      "A live preview or inspector panel positioned wherever the reviewer wants it for the screen they are on.",
      "A media or call tile that has to stay visible but must not sit on top of the form being filled in."
    ]
  },
  {
    "example": "import { SplitPane } from \"@godxjp/ui/layout\";\n\n<SplitPane aside={<DetailPanel />} asideWidth=\"sm\">\n  <MainContent />\n</SplitPane>\n\n// Collapsible rail — closing with `null` does NOT remount <MessageList />.\n<SplitPane asideLabel=\"Thread\" aside={threadOpen ? <Thread /> : null}>\n  <MessageList />\n</SplitPane>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "SplitPane",
    "props": [
      {
        "description": "Main (left) content.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Aside (trailing) panel content. `null` means CLOSED: no `<aside>` element is rendered, the grid drops to one full-width column and the gap goes with it. Closing through this prop — instead of dropping SplitPane at the call site — keeps `children` mounted, because the wrappers stay in place and the main column keeps its position in the React tree.",
        "name": "aside",
        "required": true,
        "type": "ReactNode | null"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Width preset for the aside rail: sm 20rem, md 22rem, lg 30rem. `lg` is for a rail that carries a panel rather than a list, and it holds off splitting until the pane is 64rem wide so the main column stays the wider of the two.",
        "name": "asideWidth",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Accessible name for the `<aside>` complementary landmark. Required when more than one complementary landmark shares the document, so a screen-reader landmark list says which rail is which.",
        "name": "asideLabel",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Take the remaining height of the parent instead of growing with the content. Default false keeps the pane content-height, which is right inside a scrolling page. Set it for an app-shell surface whose columns own their own scrolling — a chat transcript with a pinned composer, a full-height table beside a detail rail: the pane and both columns get a DEFINITE height, so overflow inside a column scrolls that column instead of pushing the page taller. The parent still decides how much height there is to fill: inside a flex column, give the wrapper `flex: 1; min-height: 0`.",
        "name": "fill",
        "type": "boolean"
      },
      {
        "defaultValue": "\"main-first\"",
        "description": "Which pane comes first when the pane is too narrow to split and the columns stack. `aside-first` puts the aside on top — a detail page whose properties card must not fall below a long comment thread on a phone. It moves the `<aside>` first in the DOM (not a CSS `order` swap), so in the stacked layout reading and Tab order match the screen (WCAG 1.3.2 / 2.4.3); once the pane splits, grid placement keeps main in the leading column and the aside inline-end, geometry identical to the default. No antd equivalent (antd's closest tool is Grid `Col order`, a visual-only reorder).",
        "name": "stackOrder",
        "type": "\"main-first\" | \"aside-first\""
      }
    ],
    "related": [
      "ResponsiveGrid — use when you need more than two columns, or when both columns must have equal or percentage-based widths rather than a fixed-rem aside. SplitPane always gives main a `1fr` and aside a fixed rem width.",
      "PageContainer — use as the outer scaffold that provides page padding and vertical rhythm; nest SplitPane inside PageContainer, not the other way around.",
      "Sheet — use when the detail/context panel should slide in as an overlay (drawer) rather than sitting permanently beside the main content. Prefer Sheet on mobile or when the aside content is secondary and on-demand.",
      "Flex direction='col' — use when content is purely vertical (single column, sequential sections). SplitPane is the right pick only when a persistent side panel is needed at the same hierarchy level as the main content."
    ],
    "rules": [
      24
    ],
    "storyPath": "layout/split-pane.tsx",
    "tagline": "Two-column layout with a main content area and a fixed-width aside panel.",
    "usage": [
      "DO: pass all right-panel content via the `aside` prop — it renders inside a semantic `<aside>` element at a fixed rem width (sm=20rem, md=22rem). The `children` prop fills the main `1fr` column. Both accept any ReactNode.",
      "DO: reach for `fill` when the pane is an app-shell surface rather than a block in a scrolling page — a chat transcript whose composer stays pinned to the bottom, a full-height table beside a detail rail. It is the supported way to say \"be as tall as what is left\"; a consumer that instead styles the pane's wrapper divs from outside (`[&>*]`, `[&>*>*]`) is targeting this component's internal DOM by position and will break the day another wrapper appears.",
      "DO: set `stackOrder=\"aside-first\"` when the aside is what a phone user needs first — an issue's properties / agent card above its comment thread. DON'T render the aside twice (once above, once beside) or reorder with a CSS `order` override: the prop already puts it first in the DOM, so focus order follows the stacked screen, and restores the side-by-side placement once the pane splits.",
      "DO: choose `asideWidth=\"sm\"` for compact detail panels (filters, quick stats, key-value summaries) and the default `asideWidth=\"md\"` for richer panels (forms, timelines, long metadata lists).",
      "DO: tune the main ↔ aside distance with the `--split-pane-gap` token (default `--space-stack-md`, 16px, split and stacked) — set it once in theme.css to the column step your design grid reads between panes, e.g. `--split-pane-gap: var(--space-inline-xl)` so an editor's aside sits further from the text than its paragraphs sit from each other (gh#1310). DON'T override `gap` on the pane's wrapper from outside.",
      "DO: wrap SplitPane inside `PageContainer` or `PageContainer.Inset` — SplitPane provides no page padding of its own. It is a grid primitive, not a page scaffold.",
      "DO: close a collapsible rail with `aside={null}` — a Slack-style thread, a Linear-style detail panel — rather than swapping `<SplitPane aside={<Thread />}>{page}</SplitPane>` for a bare `{page}`. Both look the same on screen; only the prop keeps `children` mounted. The conditional swap changes the DEPTH of `{page}` in the React tree, so React remounts it: measured in a consumer as a message list jumping from scrollTop 400 to the bottom the instant a thread opened, losing the reader's place and any component state below it.",
      "DON'T: leave `SplitPane` mounted with an empty `aside={<div />}` to avoid that remount — an empty aside still reserves its rail, leaving a blank 20-30rem column. `aside={null}` removes the element AND the column (the pane sets `data-aside=\"closed\"` on itself, so the collapse is one CSS rule, not a call-site width override).",
      "DON'T: expect two columns below 48rem OF THE PANE'S OWN WIDTH (64rem for `asideWidth=\"lg\"`). The split is a CSS container query on the pane, not a viewport media query, so a narrow embedded pane on a large screen correctly stays single-column and a wide pane on a small screen still splits. Below the threshold it stacks (main on top, aside below — `stackOrder=\"aside-first\"` flips that); for layouts that must stay side-by-side at any width use CSS Grid or `ResponsiveGrid`.",
      "DON'T: add a CSS `overflow: hidden` or fixed height on the SplitPane wrapper; both columns carry `min-width: 0` to handle overflow correctly, and the grid uses `minmax(0, 1fr)` — adding external constraints will break the overflow contract.",
      "DON'T: hand-roll a two-column div layout with flexbox or CSS Grid when SplitPane already ships — that duplicates the responsive breakpoint logic and the semantic `<aside>` element."
    ],
    "useCases": [
      "Invoice / transaction detail page: list of records in `children` (DataTable), selected-record detail panel in `aside` (Descriptions + Timeline).",
      "Accounting ledger drill-down: account list on the left, chart-of-accounts metadata or running balance breakdown on the right using `asideWidth=\"sm\"`.",
      "Document review workflow: PDF or rich-text viewer in `children`, approval form or annotation panel in `aside`.",
      "Settings page with a category list or Steps navigator in `children` and a live preview or summary card in `aside`.",
      "Kanban or task board where the main area holds the board columns and the aside shows the focused task detail without navigating away."
    ]
  },
  {
    "example": "import { Button, Logo, Text } from \"@godxjp/ui/general\";\nimport { AppShell, ErrorSurface, PageContainer, Sidebar } from \"@godxjp/ui/layout\";\n\n// 403 — APPLICATION mode: the body inside the shell the route ALREADY renders.\n// The sidebar/topbar/breadcrumb are preserved; the surface builds no chrome.\nexport function ForbiddenPage() {\n  return (\n    <AppShell sidebar={<Sidebar activeId=\"reports\" sections={sections} />}>\n      <PageContainer title=\"レポート\" breadcrumb={[{ label: \"ホーム\", to: \"/\" }, { label: \"レポート\" }]}>\n        <ErrorSurface\n          mode=\"application\"\n          status={403}\n          title={t(\"errors.403.title\")}\n          description={t(\"errors.403.description\")}\n          permission=\"reports.view\"\n          organization=\"株式会社ゴッドエックス\"\n          action={<Button onClick={goHome}>{t(\"errors.backHome\")}</Button>}\n        />\n      </PageContainer>\n    </AppShell>\n  );\n}\n\n// 503 — SYSTEM mode: the surface owns the page. No className, no min-h-dvh, no media query.\nexport function MaintenancePage() {\n  return (\n    <ErrorSurface\n      mode=\"system\"\n      status={503}\n      brand={<Logo glyph=\"G\" />}\n      title={t(\"errors.503.title\")}\n      description={t(\"errors.503.description\")}\n      maintenance={{\n        start: \"2026-08-02T18:00:00Z\",   // ISO-8601 instant, server-sent\n        end: \"2026-08-02T20:00:00Z\",\n        timeZone: \"Asia/Tokyo\",           // IANA — explicit, so SSR and client agree\n        progress: 40,                     // 0-100, server-sent (never from the client clock)\n      }}\n      footer={<Text size=\"xs\" tone=\"muted\">2026 GodX</Text>}\n      action={<Button onClick={reload}>{t(\"errors.reload\")}</Button>}\n    />\n  );\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "ErrorSurface",
    "props": [
      {
        "description": "The shell contract, not a skin. \"application\" (400/403/404) renders ONLY the surface block — put it in AppShell's children (usually inside a PageContainer) so the sidebar/topbar/breadcrumb stay mounted; it cannot build chrome, because nav data and the user menu are consumer-owned. \"system\" (500/503) renders the whole page: CenteredShell align=\"center\", so no consumer min-h-dvh / flex class / media query.",
        "name": "mode",
        "required": true,
        "type": "\"application\" | \"system\""
      },
      {
        "description": "The HTTP status (a NUMBER, not a string). It is the input that drives the default icon (TriangleAlert 400 · ShieldAlert 403 · SearchX 404 · ServerCrash 500 · Wrench 503) and tone (warning 400/403/503 · muted 404 · destructive 500). Also rendered as the compact tabular status code, announced as 'HTTP status 403' rather than the cardinal number. 400 is the malformed-request page — a route reached with parameters the server refuses to interpret (a bad id, a missing launch parameter) — and belongs in mode=\"application\" like 403/404.",
        "name": "status",
        "required": true,
        "type": "400 | 403 | 404 | 500 | 503"
      },
      {
        "description": "Headline. Consumer-owned copy from the APP's own t() — @godxjp/ui ships no product text.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "The ONE recovery action (a Button, or Button asChild wrapping a router Link). A single slot IS the enforcement: pass a fragment with two buttons and only the first renders, with a development-time console error. Support contact goes in `description`, never in a second CTA.",
        "name": "action",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Supporting sentence under the title. Put support-contact guidance here. Its measure is owned by --empty-state-description-max-width.",
        "name": "description",
        "type": "ReactNode"
      },
      {
        "description": "Support correlation id, rendered as a <dt>/<dd> metadata row with a localized 'Request ID' label and a mono/tabular value so it can be read out or copied accurately. Pass the bare id — never write it into `description` as prose.",
        "name": "requestId",
        "type": "string"
      },
      {
        "description": "The permission/role the viewer is missing (403). Pass the bare permission name ('reports.view'); the localized 'Required permission' label is the surface's.",
        "name": "permission",
        "type": "ReactNode"
      },
      {
        "description": "The organization/tenant the failed request was scoped to. Together with `permission` this is what distinguishes a wrong-workspace 403 from a missing-role 403.",
        "name": "organization",
        "type": "ReactNode"
      },
      {
        "description": "Planned-outage timing (503). `start`/`end` are ISO-8601 INSTANTS and `timeZone` an IANA id: the surface formats them with Intl.DateTimeFormat(locale).formatRange() and keeps the ISO value in <time dateTime>. NEVER pass a pre-formatted '18:00 - 20:00 JST' string. `progress` (0-100) adds a labelled Progress meter named via Intl.NumberFormat percent style; it is SERVER-SENT on purpose — deriving it from the client clock breaks SSR hydration.",
        "name": "maintenance",
        "type": "{ start: string; end?: string; timeZone?: string; progress?: number }"
      },
      {
        "description": "Override the status-derived icon. Use only when the product has a truer glyph for the failure, never to change perceived severity.",
        "name": "icon",
        "type": "ComponentType<{ className?: string }>"
      },
      {
        "description": "Override the status-derived tone (same union as EmptyState.tone). Defaults: 400/403/503 warning · 404 muted · 500 destructive.",
        "name": "tone",
        "type": "\"muted\" | \"info\" | \"success\" | \"warning\" | \"destructive\""
      },
      {
        "description": "Semantic heading level of `title`. Defaults to 2 in application mode (a PageContainer h1 sits above) and 1 in system mode (the surface IS the page). Choose it to keep the outline valid, never for size.",
        "name": "titleLevel",
        "type": "1 | 2 | 3 | 4"
      },
      {
        "description": "system mode ONLY — brand slot above the status code (a Logo). Ignored in application mode, where the shell already shows the brand.",
        "name": "brand",
        "type": "ReactNode"
      },
      {
        "description": "system mode ONLY — the page footer (contentinfo): copyright, status page link, locale switch.",
        "name": "footer",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"sm\"",
        "description": "system mode ONLY — measure of the centred column (the CenteredShell width tier).",
        "name": "width",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Root element id.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root class override (rarely needed).",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "CenteredShell — the viewport-centred page shell that ErrorSurface renders internally for mode=\"system\". Use it directly only for a standalone surface that is NOT one of the four HTTP statuses.",
      "AppShell + PageContainer — what you wrap around a mode=\"application\" surface. ErrorSurface never builds them: nav sections and the user menu are consumer-owned data.",
      "EmptyState — the zero-state primitive ErrorSurface composes for its icon/title/description/action body. Use it directly for an empty LIST or an empty section, not for an HTTP exception page.",
      "AlertQueryError / humanError — inline, in-page query failure feedback. ErrorSurface is the whole PAGE; an inline alert is the right pick when the surrounding page still works.",
      "Progress — the meter ErrorSurface renders for maintenance.progress."
    ],
    "rules": [
      23,
      24
    ],
    "storyPath": "layout/error-surface/index.tsx",
    "tagline": "Package-owned semantic exception surface for 400 / 403 / 404 / 500 / 503. `mode` is the SHELL CONTRACT: \"application\" renders the body you put inside the AppShell the route already provides (chrome PRESERVED, never reconstructed); \"system\" owns the whole page via CenteredShell align=\"center\" (package-owned 1440/1024/390 geometry). Exactly one recovery action, plus semantic request-id / permission / organization / maintenance metadata slots.",
    "usage": [
      "DO use ErrorSurface for ANY 400 / 403 / 404 / 500 / 503 page. It is a real import from @godxjp/ui/layout — never hand-compose an error page from the auth CenteredShell + a generic Card, and never add a consumer-local `.canonical-auth-card`-style class (that workaround IS the regression).",
      "DO put mode=\"application\" INSIDE the shell the route already renders: <AppShell …><PageContainer …><ErrorSurface mode=\"application\" …/></PageContainer></AppShell>. The surface returns only its own block on purpose, so the sidebar/topbar/breadcrumb survive and the user can navigate away.",
      "DO use mode=\"system\" for 500/503 and pass NOTHING for geometry — the surface renders CenteredShell align=\"center\" itself. A className=\"min-h-dvh flex items-center\" is always wrong.",
      "DO pass ONE action. A second CTA is dropped with a development error; put 'contact support' in `description`.",
      "DO pass `maintenance` as ISO-8601 instants + an IANA timeZone and let Intl format it. A hand-built window string cannot localize, and a client-derived `progress` breaks SSR hydration.",
      "DO let `status` pick the icon and tone. Override them only for a truer product glyph — never to recolour a status into a different severity.",
      "DO NOT retune the geometry with a className: --error-surface-max-width, --error-surface-gap, --error-surface-padding-block(-compact), --error-surface-meta-* and --error-surface-progress-max-width are the knobs. The metadata divider defaults to `none` (rule #44); opt in with --error-surface-meta-border.",
      "DO NOT write the request id, the missing permission or the maintenance window into `description` as prose — each is a semantic <dt>/<dd> slot, and prose loses the label↔value relationship for a screen reader.",
      "DO NOT use the auth shape for an error page: it is the UNAUTHENTICATED root and imposes auth-card geometry."
    ],
    "useCases": [
      "Inertia/Laravel exception page (SCR-006): one Error.tsx receives `status` from Handler::render() and renders <ErrorSurface mode={status >= 500 ? \"system\" : \"application\"} status={status} … />, with Error.layout keeping the authenticated shell for 403/404 only.",
      "Forbidden report inside the console: <ErrorSurface mode=\"application\" status={403} permission=\"reports.view\" organization=\"Acme KK\" action={<Button asChild><Link href=\"/\">Back</Link></Button>} /> inside the existing AppShell + PageContainer.",
      "Planned maintenance page: <ErrorSurface mode=\"system\" status={503} maintenance={{ start, end, timeZone: \"Asia/Tokyo\", progress: 40 }} brand={<Logo glyph=\"G\" />} footer={…} />.",
      "Unexpected failure with a support correlation id: <ErrorSurface mode=\"system\" status={500} requestId=\"01J9Z0…\" action={<Button onClick={reload}>Reload</Button>} />."
    ]
  },
  {
    "example": "import { CenteredShell, Flex, Topbar } from \"@godxjp/ui/layout\";\nimport { LegalDocumentShell } from \"@godxjp/ui/lab\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport { useState } from \"react\";\n\nexport function TermsPage() {\n  const [activeSection, setActiveSection] = useState(\"acceptance\");\n\n  return (\n    <CenteredShell width=\"lg\" topbar={<Topbar start={<Brand />} />}>\n      <LegalDocumentShell\n        title=\"利用規約\"\n        version=\"2.4\"\n        effectiveDate=\"2026-04-01\"\n        summary=\"本規約は、当社が提供するサービスの利用条件を定めるものです。\"\n        contentsLabel=\"目次\"\n        activeSection={activeSection}\n        onActiveSectionChange={setActiveSection}\n        sections={[\n          { id: \"acceptance\", title: \"第1条(本規約への同意)\", content: <Text as=\"p\">…</Text> },\n          { id: \"accounts\", title: \"第2条(アカウントの管理)\", content: <Text as=\"p\">…</Text> },\n        ]}\n        documentNavigation={\n          <Flex direction=\"col\" gap=\"xs\" role=\"group\" aria-label=\"法的文書\">\n            <Button variant=\"secondary\" size=\"sm\" fullWidth aria-current=\"page\">利用規約</Button>\n            <Button variant=\"ghost\" size=\"sm\" fullWidth>プライバシーポリシー</Button>\n          </Flex>\n        }\n        footerAction={<Button size=\"sm\">同意する</Button>}\n      />\n    </CenteredShell>\n  );\n}",
    "group": "layout",
    "importPath": "@godxjp/ui/lab",
    "name": "LegalDocumentShell",
    "props": [
      {
        "description": "Document title — the <h1> that names the <article> (e.g. 'Terms of Service'). Sized two type steps above the section <h2>s (--legal-document-title-font-size, default the --font-size-2xl step) so the page does not read flat. Around it, PageContainer may omit `title` so this stays the page's only <h1>.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "The document's sections in reading order. Drives BOTH the contents list and the body: `id` is the REAL anchor target (<section id> + href='#id'), `title` becomes the <h2> AND the contents label, `content` is the consumer-owned legal copy.",
        "name": "sections",
        "required": true,
        "type": "{ id: string; title: string; content: ReactNode }[]"
      },
      {
        "description": "Bare version identifier (e.g. '2.4'). Rendered as a localized 'Version {version}' — never pass a pre-localized sentence.",
        "name": "version",
        "type": "string"
      },
      {
        "description": "ISO 8601 calendar date (yyyy-MM-dd) or a full ISO instant. Formatted with Intl.DateTimeFormat in the active locale and emitted inside <time dateTime={effectiveDate}>, so the machine-readable value is always the ISO input. NEVER pass a pre-formatted date string.",
        "name": "effectiveDate",
        "type": "string"
      },
      {
        "defaultValue": "\"effective\"",
        "description": "What the date means, which picks its localized caption: 'effective' (施行日 / Effective, a policy in force from that date) or 'updated' (更新日 / Updated, a wiki page or help article last revised). Never prefix a label of your own to the date: word order differs per locale.",
        "name": "dateKind",
        "type": "\"effective\" | \"updated\""
      },
      {
        "description": "Short plain-language summary rendered under the metadata, above the contents.",
        "name": "summary",
        "type": "ReactNode"
      },
      {
        "description": "Accessible name + visible caption of the contents <nav>. Defaults to a localized 'Contents'. Override it when two documents share a view, so the two nav landmarks stay distinguishable (axe landmark-unique, WCAG 2.4.1).",
        "name": "contentsLabel",
        "type": "string"
      },
      {
        "description": "Controlled active section id (the entry marked aria-current='location'). Pair with onActiveSectionChange; omit both for the uncontrolled form.",
        "name": "activeSection",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial active section id. Defaults to the first section.",
        "name": "defaultActiveSection",
        "type": "string"
      },
      {
        "description": "Fires on contents-anchor activation, on an initial hash deep link, and continuously from the scroll spy as the reader moves through the document.",
        "name": "onActiveSectionChange",
        "type": "(sectionId: string) => void"
      },
      {
        "description": "Rail slot above the contents list — a switcher across the legal set (Terms · Privacy · Cookies). Rendered as a plain wrapper, so the consumer owns its semantics.",
        "name": "documentNavigation",
        "type": "ReactNode"
      },
      {
        "description": "Slot below the last section — accept / download / print / contact actions.",
        "name": "footerAction",
        "type": "ReactNode"
      },
      {
        "description": "Root element id; also seeds the internal ids.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root class override (rarely needed).",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "CenteredShell — the page shell to put a LegalDocumentShell inside (brand bar + centred column + footer). LegalDocumentShell is the document, not the page chrome; never nest two shells of the same kind.",
      "SplitPane — a generic main + fixed aside. It gives similar GEOMETRY but owns no scroll-spy / hash / focus behaviour, so it is the wrong pick for a document with a table of contents.",
      "PageContainer — for a titled section inside an app page; LegalDocumentShell already renders its own document header (h1 + version + effective date + summary).",
      "Text / Heading — compose the section `content` from these; the shell only supplies the section heading (h2) and the body wrapper."
    ],
    "rules": [
      23,
      24
    ],
    "storyPath": "layout/legal-document-shell/index.tsx",
    "tagline": "Long-form legal/policy document surface (terms, privacy, DPA, cookie policy, SLA) — semantic article/nav/section landmarks, real anchors, a sticky table-of-contents rail, scroll-spy aria-current, hash deep links with a token-driven scroll offset and focus handoff. All legal text stays consumer-owned.",
    "tier": "lab",
    "usage": [
      "DO use LegalDocumentShell for ANY long-form legal/policy document — terms of service, privacy policy, DPA, cookie policy, SLA, EULA, security policy. It is the only primitive that owns the document behaviour: scroll-spy active section, hash deep links, scroll offset, focus handoff and reduced-motion scrolling.",
      "DO pass `effectiveDate` as an ISO 8601 string ('2026-04-01'). The shell formats it with Intl.DateTimeFormat in the active locale and keeps the ISO value in <time dateTime>. A pre-formatted string ('April 1, 2026') is a bug — it will not localize.",
      "DO keep ALL legal text in the consumer: the shell only receives `sections` (+ the `summary` / `documentNavigation` / `footerAction` slots). It never ships legal copy.",
      "DO give every section a URL-safe, page-unique `id` — it is simultaneously the <section id>, the contents href, the deep-link target and the aria-current key.",
      "DO NOT hand-roll this from CenteredShell + SplitPane + `.legal-*` CSS. The geometry is the easy half; the scroll spy, hash offset, focus handoff and aria-current wiring are what the shell exists to own, and no token can express them.",
      "DO NOT add a consumer `className` for the measure, the rail width, the section rhythm or the dividers — every one of those is a --legal-document-* token. Dividers default to `none` (rule #44): opt in with `--legal-document-toc-border: 1px solid hsl(var(--border))`.",
      "DO NOT expect a viewport media query: the shell owns its query container, so the one-column ⇄ two-column split (56rem) is decided by the SHELL's own width. Below it the contents are a STATIC compact block (never pinned on a phone) whose entries HUG their labels, so the active fill marks one entry rather than a full-column bar (gh#1310); above it they are a sticky rail.",
      "DO set `lang` (AppProvider does, from the locale): under `:lang(ja)` the summary and section bodies break between Japanese PHRASES (`word-break: auto-phrase`, gh#1310), as `Text break=\"phrase\"` does; Vietnamese and English keep `word-break: normal`."
    ],
    "useCases": [
      "Hosted legal terms/privacy screen (SCR-005): <CenteredShell width=\"lg\" topbar={<Topbar …/>}><LegalDocumentShell title=\"利用規約\" version=\"2.4\" effectiveDate=\"2026-04-01\" contentsLabel=\"目次\" sections={sections} activeSection={active} onActiveSectionChange={setActive} documentNavigation={yourDocumentSwitcher} footerAction={<Button>同意する</Button>} /></CenteredShell>",
      "In-app policy viewer inside a Dialog/Sheet during onboarding: the same `sections` with `footerAction` carrying the accept button; the shell stays single-column because its container is narrow.",
      "Deep-linkable DPA / sub-processor document: link to /legal/dpa#data-retention — the shell selects, scrolls to (with the scroll offset) and focuses that section on arrival.",
      "Multi-document legal set (Terms · Privacy · Cookies): render the switcher in `documentNavigation` so it sits above the contents in the sticky rail."
    ]
  },
  {
    "example": "import { Breadcrumb } from \"@godxjp/ui/layout\";\n\n<Breadcrumb items={[\n  { label: \"ホーム\", to: \"/\" },\n  { label: \"会員管理\", to: \"/members\" },\n  { label: \"田中 太郎\" },\n]} />",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "Breadcrumb",
    "props": [
      {
        "description": "Array of { label, to?, menu?, ellipsis? } — omit `to` on the last (current) segment. `ellipsis` (`boolean | { tooltip?: ReactNode }`, gh#1046; antd Breadcrumb has none, the shape is antd Typography's) keeps the trail on one line and cuts that segment's label with an ellipsis, showing the full label in a tooltip on hover and on keyboard focus of the crumb link; `{ tooltip: false }` drops the tooltip. Link and current segments only. `--breadcrumb-item-max-inline-size` (default `none`) caps such a segment. `menu` (Ant Design `BreadcrumbItemType.menu`) hangs a sibling picker off that segment: `{ items: { value, label, to?, disabled? }[], onSelect? }`. The segment then renders as a menu button, not a link — an entry with `to` becomes a real anchor inside the menu while keeping `role=\"menuitem\"`.",
        "name": "items",
        "required": true,
        "type": "BreadcrumbItemProp[]"
      },
      {
        "description": "Ant Design `separator`. Defaults to the chevron glyph; a string (`\"/\"`) or any node replaces it and `\"\"` removes it. Always `aria-hidden` — the trail's structure is the `<ol>`/`<li>`, so a screen reader never reads the glyph.",
        "name": "separator",
        "type": "React.ReactNode"
      },
      {
        "description": "Ant Design `itemRender`. Replaces ONE segment's body while Breadcrumb keeps owning the `<nav>` landmark, the `<ol>`/`<li>`, the separators and `aria-current`. Use it to hand the trail a router `<Link>` (Inertia, react-router) instead of a bare `<a>`.",
        "name": "itemRender",
        "type": "(item: BreadcrumbItemProp, info: { index: number; isLast: boolean; items: BreadcrumbItemProp[] }) => React.ReactNode"
      },
      {
        "description": "Override the nav landmark's accessible name (defaults to a localized \"Breadcrumb\"). Required when more than one Breadcrumb renders on the same page/view — two nav landmarks sharing one name/role fail landmark-unique.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "PageContainer — accepts `breadcrumb` as `BreadcrumbItemProp[]` (raw array, not a ReactNode); use this when each page owns its own breadcrumb and you want it co-located with the page title, actions, and body.",
      "AppShell — accepts `breadcrumb` as `ReactNode`; pass `<Breadcrumb items={…} />` here when the breadcrumb is a persistent shell-level strip that sits above all page content rather than being owned by individual pages.",
      "Steps — use instead of Breadcrumb when showing progress through an ordered multi-step flow (wizard, checkout, onboarding); Steps conveys sequence and completion state, not spatial location.",
      "PrefetchLink — if ancestor breadcrumb segments should prefetch their destination query on hover/focus, consider pairing the `to` values with `PrefetchLink` in a custom breadcrumb or pre-warming the cache on mount; Breadcrumb's internal links are plain react-router-dom `<Link>` with no prefetch behaviour."
    ],
    "rules": [],
    "storyPath": "navigation/breadcrumb.tsx",
    "tagline": "Standalone breadcrumb nav rendering an ordered trail of page segments.",
    "usage": [
      "DO import from `@godxjp/ui/layout` (not from a navigation or general sub-path) and pass a single `items` prop — an ordered array of `{ label, to? }` objects. No children, no sub-components, no render-prop API.",
      "DO omit `to` on the last (current-page) segment — the component automatically renders it as a `<span aria-current=\"page\">` instead of a router `<Link>`. Passing `to` on the last item does NOT make it a link; drop it intentionally.",
      "DO pass the Breadcrumb node as a ReactNode to the `breadcrumb` prop of `AppShell` (or `AppShell`) for shell-level breadcrumbs, or to `PageContainer`'s `breadcrumb` prop (which accepts `BreadcrumbItemProp[]` directly — not a ReactNode). When passing to `PageContainer`, pass the raw array; when passing to `AppShell`, wrap it: `breadcrumb={<Breadcrumb items={…} />}`.",
      "DON'T hand-roll a breadcrumb strip (divs with chevrons, anchors, separators) — Breadcrumb ships the `<nav aria-label=\"Breadcrumb\">` + `<ol>` + `aria-hidden` chevrons. Any custom trail is a violation of the no-hand-roll rule and will fail `npm run ui:audit`.",
      "DON'T use Breadcrumb for tab-style or step-style navigation (multi-step forms, wizard progress). Those flows belong to `Steps`. Breadcrumb is strictly a spatial location trail, not a process indicator.",
      "The component is fully uncontrolled and stateless — it renders whatever `items` you pass. Dynamic breadcrumbs (route-derived, breadcrumb context, etc.) must be assembled in the parent and passed down as a plain array; there is no internal routing awareness."
    ],
    "useCases": [
      "Per-page location trail on any admin page deeper than two levels — e.g. Home → Accounting → Invoices → Invoice #1042 — passed to `PageContainer`'s `breadcrumb` prop so it appears above the page `<h1>`.",
      "Persistent shell-level breadcrumb in a `AppShell` or `AppShell` layout that updates as the user navigates between Inertia/React Router pages; constructed from route params and passed as a ReactNode to `AppShell`'s `breadcrumb` prop.",
      "Master-detail drill-down in an accounting app: the detail page (journal entry, partner, bank account) shows a breadcrumb back to the list and to the domain root, giving the user a one-click escape without using the browser back button.",
      "Embedded sub-panel breadcrumb inside a `SplitPane` or `Sheet` where a secondary content area has its own navigable hierarchy and needs a compact location indicator.",
      "Audit log or document history page where the entity being reviewed (invoice, payment) is the current segment and the parent module (Accounting, Receivables) is a clickable ancestor.",
      "Prefetch pairing: wrap ancestor segments' `to` values with `PrefetchLink` semantics by putting them in `items` — each non-last item with `to` is already rendered as a router `<Link>`, so hovering naturally prefetches if `PrefetchLink` is used elsewhere on the same route."
    ]
  },
  {
    "docPath": "general/float-button.tsx",
    "example": "import { FloatButton } from \"@godxjp/ui/lab\";\nimport { MessageCircle, Share2 } from \"lucide-react\";\n\n<FloatButton.Group trigger=\"click\" type=\"primary\" icon={<MessageCircle />} aria-label=\"Trợ lý\">\n  <FloatButton tooltip=\"Hỏi trợ lý\" badge={{ count: 3 }} />\n  <FloatButton tooltip=\"Chia sẻ\" icon={<Share2 />} />\n  <FloatButton.BackTop />\n</FloatButton.Group>",
    "group": "general",
    "importPath": "@godxjp/ui/lab",
    "name": "FloatButton",
    "props": [
      {
        "description": "The glyph. Falls back to a document mark when the button carries neither an icon nor `content`, which is antd's own default.",
        "name": "icon",
        "type": "ReactNode"
      },
      {
        "description": "A short caption UNDER the glyph. Legal only with shape=\"square\" — a circle has no room, and asking for one logs antd's dev warning.",
        "name": "content",
        "type": "ReactNode"
      },
      {
        "description": "antd 6's deprecated spelling of `content`, kept so an antd call site compiles unchanged. `content` wins when both are given. Prefer `content` in new code.",
        "name": "description",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"default\"",
        "description": "antd's word for the FILL, not the native button type — `default` is the surface-with-hairline mark, `primary` the brand fill. The native attribute is `htmlType`.",
        "name": "type",
        "type": "\"default\" | \"primary\""
      },
      {
        "defaultValue": "\"circle\"",
        "description": "Round mark or rounded square. A FloatButton inside a Group inherits the group's shape, so a stack can never mix the two.",
        "name": "shape",
        "type": "\"circle\" | \"square\""
      },
      {
        "description": "Hover/focus label. When it is a STRING and no aria-label is given it also becomes the button's accessible name — an icon-only control needs a name, and a tooltip alone never reaches a touch user (WCAG 4.1.2).",
        "name": "tooltip",
        "type": "ReactNode | { title, side, align, sideOffset }"
      },
      {
        "description": "Renders an <a> instead of a <button>, exactly as antd does.",
        "name": "href",
        "type": "string"
      },
      {
        "description": "Anchor target; only meaningful beside `href`.",
        "name": "target",
        "type": "string"
      },
      {
        "description": "The corner count/dot mark. `overflowCount` defaults to 99 and `showZero` to false, both antd's. antd's `offset` and `size` are deliberately not ported — `offset` is a raw px tuple no token step spells.",
        "name": "badge",
        "type": "{ count?: number; dot?: boolean; overflowCount?: number; showZero?: boolean; color?: string }"
      },
      {
        "description": "Non-interactive. A Group's trigger inherits it. Remember the house rule: the REASON a control is disabled is visible text, never the tooltip.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "\"button\"",
        "description": "The native button type, since `type` is taken by the fill. antd 5.21+.",
        "name": "htmlType",
        "type": "\"button\" | \"submit\" | \"reset\""
      }
    ],
    "related": [
      "Button — the same control IN the layout flow. A float button is a Button that gave up its place in the page and took a corner instead.",
      "Popover — anchors a panel to a trigger; it does not place the trigger.",
      "Alert — `variant=\"banner\"` is a full-bleed strip, not a corner mark.",
      "Toolbar / PageContainer extra — where an action belongs when it IS part of this page's content."
    ],
    "rules": [],
    "storyPath": "general/float-button.tsx",
    "tagline": "Ant Design's corner action, ported whole — a control pinned to the viewport above the page, with FloatButton.Group (a stack, or a menu behind one trigger) and FloatButton.BackTop. The corner insets are tokens, so a service moves the mark without a media query.",
    "tier": "lab",
    "usage": [
      "DO import from `@godxjp/ui/lab` (the opt-in lab subpath): `import { FloatButton } from \"@godxjp/ui/lab\";`",
      "DO give every float button a name — `tooltip` as a plain string is enough, since it becomes the aria-label too.",
      "DO move the mark with the `--float-button-offset-block-end` / `--float-button-offset-inline-end` tokens when a sticky action bar or a mobile tab bar is already in that corner.",
      "DO pass `target={() => element}` to FloatButton.BackTop inside a shell that owns its own scroll region, so it watches that region rather than the document.",
      "DON'T hand-roll it as a Button with `position: fixed` at the call site — that is page-local CSS the audit blocks, and it is the exact gap gh#558 was filed for.",
      "DON'T reach for Popover: it anchors a PANEL to a trigger, so it answers where the panel goes, not where the trigger goes.",
      "DON'T put a caption on a circle. `content` needs shape=\"square\"."
    ],
    "useCases": [
      "An assistant or composer that must stay reachable while the reader works — a chat you may want open is not a one-shot trigger in a bar.",
      "A stack of secondary actions collapsed behind one corner trigger (share / export / print).",
      "Back to top on a long document or a long list.",
      "A corner action carrying an unread count, via `badge`."
    ]
  },
  {
    "example": "import { Button } from \"@godxjp/ui/general\";\nimport { Trash2 } from \"lucide-react\";\n\n<>\n  <Button>保存</Button>\n  <Button variant=\"outline\" size=\"sm\">編集</Button>\n  <Button variant=\"ghost\" size=\"icon-sm\"><Trash2 className=\"size-4\" /></Button>\n</>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Button",
    "props": [
      {
        "defaultValue": "\"default\"",
        "description": "Visual style. `dashed` = outline with a dashed border (Ant-style add-row / placeholder action).",
        "name": "variant",
        "type": "\"default\" | \"destructive\" | \"outline\" | \"dashed\" | \"secondary\" | \"ghost\" | \"link\" | \"bare\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Size preset (height, padding, icon dims). `touch` (gh#1304) is the glove step for wet or gloved hands — food-production stations, a HACCP 良/否 verdict: 56px tall (`--control-height-touch`) and never narrower than it is tall, under a mouse as well as a finger. Button owns no siblings, so lay a row of touch Buttons out with a gap of at least `--control-touch-gap` (8px), e.g. `Flex direction=\"row\" gap=\"sm\"` (8px); pair with `fullWidth` for a full-width bar.",
        "name": "size",
        "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | \"touch\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Corner radius from the tokens — `default` (control radius), `pill` (fully rounded, --radius-pill), `sharp` (square, --radius-sharp). Use the prop instead of a `rounded-*` className.",
        "name": "shape",
        "type": "\"default\" | \"pill\" | \"sharp\""
      },
      {
        "defaultValue": "false",
        "description": "Span the full container width (`width:100%`) instead of sizing to content.",
        "name": "fullWidth",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Take the space the siblings leave, and let a long label ELLIPSE instead of widening the row — the same axis Flex calls `fill`, for the same reason. Button ships flex-shrink: 0, which is right almost everywhere and wrong in a constrained bar: an account menu holding an avatar plus a person's name keeps its full width while the cluster clips it, so a keyboard user tabs to a control they cannot see (SC 2.4.7). Sets flex: 1 1 auto and min-inline-size: 0; pair it with a <Text truncate> label. This is what replaces className=\"min-w-0 flex-1\", which ui-audit blocks — and which this package's own Topbar guidance used to recommend.",
        "name": "fill",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Allow multi-line text labels to grow the button height. Uses the active size tier as its minimum height; use with fullWidth in narrow collections.",
        "name": "wrap",
        "type": "boolean"
      },
      {
        "defaultValue": "\"center\"",
        "description": "Logical content alignment. Use start for full-width collection actions; respects RTL.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "defaultValue": "false",
        "description": "Render as the kit's Slot — merge props onto the child (<a>/<Link>).",
        "name": "asChild",
        "type": "boolean"
      },
      {
        "description": "Disable the button.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "In-flight state — shows a leading `Loader2` spinner (replaces a leading icon if present), sets `aria-busy=\"true\"`, and blocks activation (non-interactive, pointer-events disabled) while keeping the label visible so the width doesn't jump. Prefer this over a hand-rolled `<Loader2 className=\"animate-spin\">` inside the button. Ignored when `asChild` (Slot requires a single child).",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Optional label to swap in while `loading` (pass the `t()`-translated string, e.g. `loadingText={t('saving')}`). When omitted the original children stay beside the spinner.",
        "name": "loadingText",
        "type": "string"
      },
      {
        "description": "Click handler. Does not fire while `loading` or `disabled`.",
        "name": "onClick",
        "type": "React.MouseEventHandler<HTMLButtonElement>"
      },
      {
        "description": "Trailing borderless counter pill after the label (filter tabs / segmented toggles, e.g. \"Chờ bay 18\"). Localized via `Intl.NumberFormat`; styled per variant — never nest a `Badge` in a Button for this. Ignored when `asChild`.",
        "name": "count",
        "type": "number"
      },
      {
        "defaultValue": "99",
        "description": "Cap for `count` (Ant Badge parity) — when `count` exceeds it the pill renders `{overflowCount}+` (e.g. `99+`).",
        "name": "overflowCount",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Whether the pill renders when `count` is 0 (Ant Badge parity). Pass `false` to hide the pill at zero.",
        "name": "showZero",
        "type": "boolean"
      },
      {
        "description": "Localized description of what `count` MEANS, folded into the accessible name — the SAME prop and the same contract Toggle has carried since gh#312, added here in gh#734 because a counted Button measured `\"Git3\"` in the accessibility tree, not \"Git, 3 pages\". The pill is aria-hidden and the spoken clause is an sr-only sibling, so a text button announces as `\"<label>, <count> <countLabel>\"`; with an explicit `aria-label` (an icon-only counted button) the clause is folded into that instead. Pass a t()-resolved string — the library does not own this wording. Even without it the name is now `\"Git, 3\"` rather than `\"Git3\"`; `countLabel` is what turns the digits into a unit.",
        "name": "countLabel",
        "type": "string"
      }
    ],
    "related": [
      "TopbarItem — the same trigger INSIDE a Topbar slot. A Button there is a --control-height pill floating in a taller bar, with its own hover fill and a ring around the pill instead of the cell; TopbarItem is the bar cell itself. Anywhere that is not a bar, Button is right.",
      "DropdownMenu — when a button needs to reveal a list of actions (e.g. 'Actions ▾' in a DataTable row), wrap the Button as a `DropdownMenuTrigger` inside a `DropdownMenu` compound; don't open a Sheet/Dialog just to show a list of options.",
      "ButtonRefetch — a pre-wired Button variant from `@godxjp/ui/query` that binds directly to a TanStack Query result (shows spinner, auto-disables while fetching, retries on click). Use it instead of a raw Button whenever the action is a query refetch — do not pass `onClick`/`disabled` to it manually.",
      "AlertMutationFeedback — for surfacing mutation errors and a retry action; it renders its own retry Button internally. Do not add a separate Button alongside AlertMutationFeedback for the same mutation.",
      "PrefetchLink — use when the goal is purely navigation with hover-prefetch (Inertia v3 prefetch); it renders as an `<a>` not a button. Only reach for `Button asChild + Link` when the navigation control must look like a button (primary CTA style)."
    ],
    "rules": [
      23
    ],
    "storyPath": "general/button/index.tsx",
    "subParts": [
      "ButtonRefetch"
    ],
    "tagline": "Core button with variant + size presets, built on cva with the kit's own Slot (asChild).",
    "usage": [
      "DO pick the right variant for intent: `default` (primary CTA, one per section), `destructive` (irreversible actions like delete/revoke), `outline` (secondary actions alongside a primary), `secondary` (less prominent actions), `ghost` (toolbar icon-only actions), `link` (inline text-style navigation without an underline by default).",
      "DO use icon-only sizes (`icon`, `icon-xs`, `icon-sm`, `icon-lg`) exclusively for buttons that contain only an SVG — these sizes set equal width/height. For text+icon buttons use `default|sm|lg|xs` sizes; icons inside are auto-sized to 1rem via `[&_svg:not([class*='size-'])]:size-4`.",
      "DO use `asChild` to render the button as a React Router/Inertia `<Link>` or native `<a>` while keeping all button styling and a11y: `<Button asChild variant=\"outline\"><Link href={route('invoices.show', id)}>詳細</Link></Button>`. Never wrap a `<button>` around an `<a>` — that is invalid HTML.",
      "DON'T use raw `<button>` elements anywhere in the UI — always use this `Button`. The only exception is an `aria-hidden` native control used as an e2e/a11y hook paired with a visible godx-ui control.",
      "DO set `type=\"submit\"` explicitly on form submit buttons (the default HTML button type inside `<form>` is already `submit`, but being explicit prevents accidental double-submissions when a `type=\"button\"` sibling exists). For cancel/reset actions set `type=\"button\"` to avoid accidental form submission.",
      "DON'T apply raw padding, height, or `rounded-*` overrides to `Button` via `className` — the size variants encode the full box model. If a custom size is truly needed, use `buttonVariants` from `@godxjp/ui/general` to compose a new cva class rather than fighting the existing ones.",
      "DO use the `loading` prop for async/pending actions instead of hand-rolling `<Loader2 className=\"animate-spin\">` inside the button — `loading` renders the spinner, sets `aria-busy`, and blocks activation for you; pair with `loadingText={t('saving')}` to swap the label. For a TanStack Query refetch use `ButtonRefetch` (it owns its own loading lifecycle) rather than wiring `loading` manually.",
      "DO pass `countLabel` with every `count` — without it the number is read as a bare digit run glued to the label. `<Button count={3} countLabel={t('common.pages')}>Git</Button>` announces \"Git, 3 pages\".",
      "DON'T reach for `<Button count aria-pressed>` to build a FILTER CHIP — Button paints no pressed state, so the chip looks identical selected and unselected. That is `<Toggle variant=\"soft\" shape=\"pill\" count countLabel>` (gh#734)."
    ],
    "useCases": [
      "Primary form submission in a Dialog or Sheet (e.g. `<Button type=\"submit\" disabled={form.processing}>保存</Button>`) — the `disabled` prop greys it out and blocks pointer events, preventing double-submit during async operations.",
      "Destructive confirmation inside a Dialog — pair `tone=\"destructive\"` Button as the confirm action and `variant=\"outline\"` as Cancel; never use `variant=\"default\"` for a delete action.",
      "Icon-only toolbar actions in a DataTable column (edit, delete, copy) using `size=\"icon-sm\"` + `variant=\"ghost\"` + a Lucide icon child — gives equal-width square targets that don't distort the row.",
      "Navigation links styled as buttons (e.g. 'New Invoice', 'Back to list') using `asChild` + Inertia `<Link>` — preserves SPA navigation while using the button's visual treatment.",
      "Async mutation trigger in an accounting workflow (e.g. 'Sync from MF', 'Export CSV') — disable on processing state; pair with `AlertMutationFeedback` for error/retry UI rather than inline `try/catch` alerts.",
      "Refetch / retry trigger when NOT using TanStack Query — for manual cache refresh inside a TanStack Query context use `ButtonRefetch` instead, which owns its own `disabled`/`onClick` lifecycle."
    ]
  },
  {
    "absorbed": [
      "Paragraph",
      "Typography"
    ],
    "example": "import { Text } from \"@godxjp/ui/general\";\n\n<Text size=\"xs\" tone=\"muted\">補助テキスト</Text>\n<Text weight=\"medium\" tabular>¥1,240,000</Text>\n<Text size=\"xs\" mono tone=\"muted\">RC-204881</Text>\n\n// A link in running content — the affordance, composed onto a router link.\n<Text asChild link><Link href=\"/view/PKG-12\">ログイン画面の余白</Link></Text>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Text",
    "props": [
      {
        "description": "Text decoration, including deleted Markdown content.",
        "name": "decoration",
        "type": "\"none\" | \"underline\" | \"line-through\""
      },
      {
        "description": "Inline code chip: compose Text as=\"code\" chip without Prose wrapper.",
        "name": "chip",
        "type": "boolean"
      },
      {
        "defaultValue": "\"sm\"",
        "description": "Type-scale step. NEVER an arbitrary px (`text-[13px]` is banned) — pick the nearest step. Ten steps in two ramps: 2xs…2xl is the golden-ratio UI ramp (≈11…22px, the dense enterprise scale); 3xl…5xl is the DISPLAY ramp (≈28/42/54px, derived from --font-size-display) for a marketing hero, CTA headline or stat figure. The same ladder `Heading size` reads, so a headline and the figure beside it are one step name apart.",
        "name": "size",
        "type": "\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"4xl\" | \"5xl\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Semantic foreground colour. Replaces `text-muted-foreground` etc. on a raw span.",
        "name": "tone",
        "type": "\"default\" | \"muted\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"inherit\""
      },
      {
        "defaultValue": "\"regular\"",
        "description": "Font weight — the 3-weight canon: regular 400 (body), medium 500 (label), bold 700 (emphasis). `semibold` is an alias that renders at the canon's 500 (there is no 600 face), so code written in the common Tailwind vocabulary type-checks without a fourth weight.",
        "name": "weight",
        "type": "\"regular\" | \"medium\" | \"semibold\" | \"bold\""
      },
      {
        "description": "Logical text alignment.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "description": "Single-line ellipsis. Mutually exclusive with `clamp` (`clamp` wins).",
        "name": "truncate",
        "type": "boolean"
      },
      {
        "description": "Multi-line clamp — max rendered lines (integer ≥ 1); overflow ends in an ellipsis. Visual-only — the full text stays in the DOM / accessible name. Mutually exclusive with `truncate` (`clamp` wins; dev builds warn).",
        "name": "clamp",
        "type": "number"
      },
      {
        "defaultValue": "\"normal\"",
        "description": "Whitespace handling. 'normal' is CSS's own behaviour (newlines and space runs collapse) and stays the default; passed explicitly it also releases a `nowrap` the text inherits (a table cell's, gh#1083). 'pre-wrap' is for text a PERSON typed — a plain-text note, an issue description, a pasted log — where the line breaks and the indentation are CONTENT: it keeps both, still wraps long lines at the container edge, and breaks an over-long unbroken token (a URL, an id) instead of overflowing. Use it INSTEAD of `className=\"whitespace-pre-wrap\"`. Precedence is explicit: `truncate` is a single-line contract and wins (dev builds warn); `clamp` COMPOSES with it, showing the first N preserved lines. Not for rendered Markdown/HTML — that is `Prose`, which styles rendered elements and does nothing to whitespace.",
        "name": "whitespace",
        "type": "\"normal\" | \"pre-wrap\""
      },
      {
        "defaultValue": "\"normal\"",
        "description": "Where an over-long unbroken token may break. THE prop for a machine identifier — an email address, a coupon code, a login id, a template key — in a DataTable/TableCell or a Descriptions value: `break=\"anywhere\"` emits `overflow-wrap: anywhere` (and releases a table cell's inherited `nowrap`), so the column shrinks to the viewport instead of scrolling the page. Use it INSTEAD of `className=\"[overflow-wrap:anywhere] break-words whitespace-normal\"`. NOT `whitespace=\"pre-wrap\"`: that is for typed line breaks, and its `break-word` does not lower min-content, so in a table cell it breaks nothing (measured: a 71-char token holds a 320px table at 630px with pre-wrap, 320px with break=\"anywhere\"). `break=\"phrase\"` is THE prop for Japanese PROSE in a narrow measure — a trainee's katakana name, an organisation, a status in a DataTable cell: it breaks between phrases (`word-break: auto-phrase`, Chrome 119+) instead of between any two characters, never starts a line with a small kana or 「ー」 (`line-break: strict`), adds `text-wrap: pretty`, and releases the cell's `nowrap` (measured in a 974px 8-column table: 「チャン・ヴァ｜ン・ミン」 → 「チャン・｜ヴァン・｜ミン」, 4 mid-katakana breaks → 0). It keeps every phrase whole, so the column is as wide as its longest phrase: a 20+ character katakana compound scrolls the table rather than being cut. Browsers without `auto-phrase` keep their own CJK breaking. Independent of `whitespace` (both may be set); `truncate` wins (dev builds warn); `clamp` composes.",
        "name": "break",
        "type": "\"normal\" | \"anywhere\" | \"phrase\""
      },
      {
        "description": "Tabular figures for aligned numbers.",
        "name": "tabular",
        "type": "boolean"
      },
      {
        "description": "Monospace family for codes / ids.",
        "name": "mono",
        "type": "boolean"
      },
      {
        "defaultValue": "\"span\"",
        "description": "Rendered element. `code`/`kbd` are monospace by default.",
        "name": "as",
        "type": "\"span\" | \"p\" | \"div\" | \"a\" | \"label\" | \"strong\" | \"em\" | \"small\" | \"code\" | \"kbd\" | \"dt\" | \"dd\" | \"caption\" | \"abbr\""
      },
      {
        "description": "This text IS a link: underline on hover AND on keyboard focus at --text-link-underline-offset, plus the focus mark every other interactive element draws. An AFFORDANCE, not a colour — `tone` still owns the colour and merely defaults to `primary` here, so `link tone=\"destructive\"` reads destructive and still underlines.",
        "name": "link",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Render the typography ONTO the child instead of emitting an element — the shape a router link takes (`<Text asChild link><Link href=…>…</Link></Text>`). The child owns the element and its navigation; Text owns the type step, tone, weight and truncation.",
        "name": "asChild",
        "type": "boolean"
      },
      {
        "description": "When set, Text renders as a `<label>` bound to this control id (polymorphic label use).",
        "name": "htmlFor",
        "type": "string"
      },
      {
        "description": "Anchor target, for `as=\"a\"`. Declared explicitly rather than by widening the base to AnchorHTMLAttributes, for the same reason `htmlFor` is declared explicitly for `as=\"label\"`: the element union IS the contract, so each polymorphic branch names the attributes it actually accepts instead of every span silently offering an href it will never render.",
        "name": "href",
        "type": "string"
      },
      {
        "description": "Anchor browsing context, for `as=\"a\"`.",
        "name": "target",
        "type": "React.HTMLAttributeAnchorTarget"
      },
      {
        "description": "Anchor relationship, for `as=\"a\"` — pair `rel=\"noopener noreferrer\"` with `target=\"_blank\"`.",
        "name": "rel",
        "type": "string"
      },
      {
        "description": "Anchor download hint, for `as=\"a\"`.",
        "name": "download",
        "type": "boolean | string"
      },
      {
        "description": "antd's emphasis axis. This library's `tone` is the SAME axis and is wider (it also carries default/primary/info), so `tone` wins when both are passed and dev builds warn. Folds secondary → muted, danger → destructive.",
        "name": "type",
        "type": "\"secondary\" | \"success\" | \"warning\" | \"danger\""
      },
      {
        "description": "Renders the run as unavailable: the disabled ink, `cursor: not-allowed`, no text selection, and `aria-disabled` so a screen reader hears \"unavailable\" rather than merely seeing it de-emphasised.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Copy affordance beside the text. `true` copies the rendered children. `text` may be a string or an async function, so the value can be fetched on demand. `icon` and `tooltips` each take `[idle, copied]`; `tooltips: false` drops the tooltip but keeps the accessible name. `format: \"text/html\"` writes an HTML flavour alongside the plain one. `onCopy` fires only AFTER the write resolves — a clipboard refusal leaves the button unconfirmed instead of claiming a copy that never happened.",
        "name": "copyable",
        "type": "boolean | { text, onCopy, icon, tooltips, format, tabIndex }"
      },
      {
        "description": "In-place editing. The run is REPLACED by an auto-growing textarea that inherits its type step, family and weight, so editing is WYSIWYG. Enter confirms with the trimmed value then fires `onEnd`; blur confirms WITHOUT `onEnd` (antd's split); Escape cancels. `triggerType: [\"text\"]` makes the text itself the affordance and renders no icon. Focus returns to the edit button when the editor closes.",
        "name": "editable",
        "type": "boolean | { text, editing, icon, tooltip, onStart, onChange, onCancel, onEnd, maxLength, autoSize, triggerType, enterIcon, tabIndex }"
      },
      {
        "description": "Single-line truncation, antd's spelling. antd drops `rows` / `expandable` / `onExpand` on `Text` — an inline run has no second line to expand into — and that omission is ported; for a multi-line run use `clamp` (antd's `Paragraph` was retired in v32). It is the SAME axis as `truncate` / `clamp` and OUTRANKS both (dev builds warn), because it is the only spelling that can also carry a suffix or a tooltip. `tooltip: true` (the children) or a node shows the full text in a Tooltip ONLY while the run is actually clipped — on pointer hover, and on keyboard focus of the Text itself (`asChild` link) or of the nearest focusable control around it (a Tree `treeitem`, a DataTable sort button, a link); the Text never becomes a tab stop of its own and its accessible name stays the full text. With `asChild` the child element is the truncated box (one `<a>`, not `<a><a>`). A `<Text ellipsis>` in `ColumnDef.header` truncates inside its column like a cell. On one line, `suffix` is never cut: the text shrinks and the suffix keeps its width (a file extension stays visible — gh#1276).",
        "name": "ellipsis",
        "type": "boolean | { suffix, symbol, defaultExpanded, expanded, onEllipsis, tooltip }"
      },
      {
        "description": "Which side the copy / edit / expand cluster sits on. Logical, so it mirrors in RTL. Default `end`.",
        "name": "actions",
        "type": "{ placement?: \"start\" | \"end\" }"
      },
      {
        "description": "Wrap the content in a real `<code>` element. Different from `mono`, which only swaps the font family: this changes what the content IS, so it reaches assistive technology.",
        "name": "code",
        "type": "boolean"
      },
      {
        "description": "Wrap in `<mark>` — highlighted.",
        "name": "mark",
        "type": "boolean"
      },
      {
        "description": "Wrap in `<u>` — underlined.",
        "name": "underline",
        "type": "boolean"
      },
      {
        "description": "Wrap in `<del>` — struck through.",
        "name": "delete",
        "type": "boolean"
      },
      {
        "description": "Wrap in `<strong>` — bold AND semantically strong. Composes with `weight`: `weight` paints, `strong` means. The seven decoration flags nest in antd's order (strong → u → del → code → mark → kbd → i).",
        "name": "strong",
        "type": "boolean"
      },
      {
        "description": "Wrap in `<kbd>` — a key or key combination.",
        "name": "keyboard",
        "type": "boolean"
      },
      {
        "description": "Wrap in `<i>` — italic.",
        "name": "italic",
        "type": "boolean"
      },
      {
        "description": "antd's private alias for `as`. Accepted so antd code pastes in unchanged; `as` wins when both are passed.",
        "name": "component",
        "type": "string"
      }
    ],
    "rules": [
      2,
      23
    ],
    "storyPath": "general/typography.tsx",
    "tagline": "Typographic primitive — use INSTEAD of a hand-rolled `<span className=\"text-[13px] font-medium text-muted-foreground\">`. Size is a type-scale step (never px); tone/weight are tokens.",
    "usage": [
      "DO use `<Text>` for ALL body / inline / caption text instead of a styled `<span>`/`<p>`. Pick `size` from the scale; never write `text-[13px]`/`text-[11px]` or `font-semibold` by hand.",
      "DO use `tone` for colour (`muted`/`primary`/semantic), `tabular` for numbers, `mono` for codes — not `className=\"text-muted-foreground font-mono tabular-nums\"`.",
      "DO use `break=\"phrase\"` for Japanese prose (a name, an organisation, a status) in a table cell — never `className=\"[word-break:auto-phrase]\"`; it is not for a machine id, which is `break=\"anywhere\"`.",
      "DO use `break=\"anywhere\"` for an email, code or id in a table cell or Descriptions value — never `className=\"[overflow-wrap:anywhere] break-words whitespace-normal\"`, and never `whitespace=\"pre-wrap\"`, which does not shrink a table column.",
      "DO use `clamp={n}` for a description limited to n lines (card grids) and `truncate` for a one-line ellipsis — never `className=\"line-clamp-2\"` (banned utility). They are mutually exclusive; `clamp` wins.",
      "For a heading, use `<Heading level>` instead of a large-size `<Text>`.",
      "DO use `link` for a link inside RUNNING CONTENT — an issue subject in a table cell, a page name in a list, a \"see all\" at the end of a row — and compose it onto your router link with `asChild`. Never `className=\"text-primary hover:underline\"`: that is consumer rules 6 and 7 in one string, and it leaves a keyboard user with no underline because `hover:` cannot fire for them.",
      "DON'T reach for `Button variant=\"link\"` in running content. `.ui-button` is a CONTROL box — `white-space: nowrap`, `flex-shrink: 0`, a `--control-height` tier and inline padding — so in a table cell it cannot wrap to a second line and cannot share the cell's line box. If you find yourself writing `whitespace-normal` back on top of it, you wanted `<Text link>`. `Button variant=\"link\"` stays right for a link-LOOKING action that submits or opens something."
    ],
    "useCases": [
      "A muted caption under a value: `<Text size=\"xs\" tone=\"muted\">2026年5月度</Text>`.",
      "A monospace id in a list row: `<Text size=\"xs\" mono tone=\"muted\">RC-204881</Text>`.",
      "A staff email in a narrow DataTable cell at 320px, wrapping instead of scrolling the page: `<Text size=\"sm\" break=\"anywhere\">{staff.email}</Text>`.",
      "A trainee's katakana name in a DataTable cell, breaking between phrases instead of mid-katakana: `render: (row) => <Text break=\"phrase\">{row.name}</Text>` (pair long-heading columns with `headerWrap: true`).",
      "An emphasized inline figure: `<Text weight=\"medium\" tabular>¥1,240,000</Text>`.",
      "A service-card description clamped to 2 lines on a narrow (390px) index: `<Text as=\"p\" size=\"sm\" tone=\"muted\" clamp={2}>{description}</Text>`.",
      "An issue subject linking out of a table cell, wrapping to two lines: `<Text asChild link truncate={false}><Link href={`/view/${key}`}>{subject}</Link></Text>`.",
      "A destructive link in a settings row: `<Text as=\"a\" href=\"/danger\" link tone=\"destructive\">取り消す</Text>`."
    ]
  },
  {
    "absorbed": [
      "Title"
    ],
    "example": "import { Heading } from \"@godxjp/ui/general\";\n\n<Heading level={2}>請求書一覧</Heading>\n<Heading level={3} tone=\"muted\">補足セクション</Heading>\n<Heading level={1} size=\"5xl\" align=\"center\">Ship it everywhere</Heading>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Heading",
    "props": [
      {
        "defaultValue": "medium",
        "description": "Độ đậm theo canon 3 bậc (400 · 500 · 700). Đặt `bold` cho tiêu đề cần nhấn.",
        "name": "weight",
        "type": "\"regular\" | \"medium\" | \"bold\""
      },
      {
        "defaultValue": "2",
        "description": "Heading level — renders the matching <h*> and, unless `size` overrides it, sizes from --heading-h{1..4}.",
        "name": "level",
        "type": "1 | 2 | 3 | 4"
      },
      {
        "description": "Visual size, overriding the step `level` would have taken — the SAME ladder `Text size` reads. `level` still owns the document outline, so a marketing hero is `<Heading level={1} size=\"5xl\">`: a real <h1> at 54px, with no admin screen's <h1> moving. Omit it and `level` decides, exactly as before. Use this INSTEAD of a bespoke `.display` class with a raw font-size.",
        "name": "size",
        "type": "\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"4xl\" | \"5xl\""
      },
      {
        "description": "Override the rendered element (e.g. a visual h2 that is a real <h1>).",
        "name": "as",
        "type": "\"h1\" | \"h2\" | \"h3\" | \"h4\" | \"div\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Semantic foreground colour.",
        "name": "tone",
        "type": "\"default\" | \"muted\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"inherit\""
      },
      {
        "description": "Logical text alignment.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "description": "Single-line ellipsis.",
        "name": "truncate",
        "type": "boolean"
      }
    ],
    "rules": [
      6,
      23
    ],
    "storyPath": "general/typography.tsx",
    "tagline": "Section heading sized from the --heading-h* tokens. `level` sets the size AND the semantic <h1..h4>; `size` overrides the size alone, and its top three steps are the display ramp a marketing hero needs.",
    "usage": [
      "DO use `<Heading level>` for section titles instead of a raw `<h2 className=\"text-lg font-semibold\">`. The level drives both the token size and the semantic element.",
      "Inside a Card use `<CardTitle>`; use `<Heading>` for free-standing page/section headings not covered by a component slot."
    ],
    "useCases": [
      "A section heading on a dashboard: `<Heading level={3}>今月のKPI</Heading>`.",
      "A visually-smaller heading that must stay an <h1> for a11y: `<Heading level={1} as=\"h1\">…</Heading>`.",
      "A marketing hero headline: `<Heading level={1} size=\"5xl\">` — a real <h1> on the display ramp, instead of a page-local `.display` class."
    ]
  },
  {
    "example": "import { Link } from \"@godxjp/ui/general\";\n\n<Link href=\"/docs/billing\">請求の設定</Link>\n<Link href=\"https://example.com\" target=\"_blank\">外部サイト</Link>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Link",
    "props": [
      {
        "defaultValue": "\"a\"",
        "description": "Rendered element.",
        "name": "as",
        "type": "\"a\" | \"span\""
      },
      {
        "description": "Single-line truncation. antd allows only a boolean on `Link`, and that restriction is ported.",
        "name": "ellipsis",
        "type": "boolean"
      }
    ],
    "related": [
      "Text",
      "Button",
      "PrefetchLink"
    ],
    "rules": [
      2,
      6,
      23
    ],
    "storyPath": "general/typography.tsx",
    "tagline": "antd Typography.Link — an anchor that already carries this library's inline link affordance (underline on hover AND on keyboard focus), plus antd's rel guard for target=\"_blank\".",
    "usage": [
      "DO use `Link` when you want an anchor with the link affordance already on it. `<Text link>` is the same affordance for a run that is not an anchor, and `<Text asChild link>` is the shape a router link takes.",
      "DON'T reach for `Button variant=\"link\"` inside running content: that is a CONTROL box (nowrap, a control height, inline padding), so in a table cell it neither wraps nor shares the cell's line box.",
      "A `target=\"_blank\"` with no `rel` of its own is given `noopener noreferrer`, because the opened document otherwise keeps a live handle back into this one."
    ],
    "useCases": [
      "A documentation link at the end of a helper line: `<Link href=\"/docs/billing\">請求の設定</Link>`.",
      "An external link that must not leak the opener: `<Link href=\"https://example.com\" target=\"_blank\">外部サイト</Link>`."
    ]
  },
  {
    "example": "import { Logo } from \"@godxjp/ui/general\";\n\n// Full lockup — mark + wordmark as ONE element (no wrapper, no page CSS).\n<Logo glyph=\"c\" wordmark=\"CoreBooks\" />\n\n// Canonical brand-green GoDX lockup — identity role, independent of --primary.\n<Logo mark=\"godx\" tone=\"success\" wordmark=\"GoDX\" />\n\n// Bare mark standing alone → give it an accessible name.\n<Logo label=\"CoreBooks\" size=\"lg\" />\n\n// Product lockup — \"GoDX | ID\", one master, package-owned divider and gaps.\n<Logo mark=\"godx-lockup\" productSuffix=\"ID\" />\n\n// The whole lockup as a link — the <a> IS the lockup, no wrapper to align.\n<Logo asChild mark=\"godx\" tone=\"success\" wordmark=\"GoDX\">\n  <a href=\"/\" />\n</Logo>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Logo",
    "props": [
      {
        "defaultValue": "\"g\"",
        "description": "The brand glyph — a short mark (letter/initials) or a custom inline <svg>. Only read when mark=\"glyph\".",
        "name": "glyph",
        "type": "React.ReactNode"
      },
      {
        "defaultValue": "\"glyph\"",
        "description": "\"godx-lockup\" renders the FULL master lockup — identity mark + the drawn \"GoDX\" logotype as one artwork (4.787:1, so the `size` tier drives HEIGHT and width follows); pair it with `productSuffix` for \"GoDX | ID\". \"godx\" renders THE CANONICAL GoDX IDENTITY MARK as an inline vector owned by the package — use it for hosted-identity surfaces (the auth CenteredShell brand bar, AuthIdentity, CenteredShell topbar); do NOT re-draw or import a brand SVG in the app. Its box + colour are tokenized (--logo-godx-size-{xs,sm,md,lg} driven by the `size` prop, pinnable at every tier via --logo-godx-size; --logo-godx-color, defaulting to the --brand IDENTITY role = canonical emerald #009766, never --primary and never the --success status green) and it drops the boxed fill/radius. \"glyph\" keeps the configurable boxed-glyph treatment.",
        "name": "mark",
        "type": "\"glyph\" | \"godx\" | \"godx-lockup\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Box size tier (tokenised) — it applies to EVERY mark, including mark=\"godx\". The boxed glyph reads --logo-size-* (md = 1.75rem); the identity mark reads its own --logo-godx-size-* scale (xs 1.5 / sm 1.75 / md 2 / lg 2.5rem — the artwork is a capsule inside a square viewBox, so it needs slightly more box to read at the same optical weight). On a `wordmark` lockup the tier scales the mark and the wordmark together. To freeze the identity box at ONE size across every tier, a service theme sets --logo-godx-size (unset by default).",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "\"primary\"",
        "description": "Semantic fill role. The boxed glyph's INK defaults to --logo-identity-foreground (near-black), NOT --brand-foreground: --brand-foreground is the artwork KNOCKOUT colour and clears only 3.67:1 on the emerald, while a boxed glyph renders real TEXT and owes WCAG 2.2 SC 1.4.3's 4.5:1 (14px bold is not \"large text\"). Re-theming --brand to a DARK fill? Override --logo-success-foreground to re-invert the ink.",
        "name": "tone",
        "type": "\"primary\" | \"success\""
      },
      {
        "description": "Readable product name rendered BESIDE the mark as ONE lockup — pass the localized product name (or an inline <svg> logotype when a real asset exists). Set it INSTEAD of hand-rolling `inline-flex items-center gap-2` around a Logo and a Text. The lockup root takes ref/className/…props; the mark becomes decorative and the wordmark text carries the accessible name, so the pair is announced once. Colour/face/weight/tracking/size and the mark↔wordmark gap are tokens (--logo-wordmark-*); on mark=\"godx\" / tone=\"success\" the wordmark is canonical emerald from the --brand identity role and NEVER reads --primary or --success.",
        "name": "wordmark",
        "type": "React.ReactNode"
      },
      {
        "description": "The PRODUCT name that follows the brand, set off by the lockup's own rule — `<Logo mark=\"godx-lockup\" productSuffix=\"ID\" />` renders \"GoDX | ID\" (later \"GoDX | Console\", \"GoDX | Admin\"). It renders after `wordmark` when both are set, and it turns Logo into a lockup on its own. The suffix is TEXT in the lockup's own type scale, NEVER a second artwork file: the rule's colour/width/height (--logo-divider-color / -width / -height / -alpha, the logotype ink at 0.25 alpha so it has a dark theme the kit's hardcoded #C5C8D6 does not) and the gap around it (--logo-product-suffix-gap) are package tokens. With mark=\"godx-lockup\" and no `wordmark` the brand half is DRAWN (paths, unreachable by AT), so the package restores \"GoDX\" as sr-only text and the lockup is announced \"GoDX <suffix>\" — as TEXT rather than role=\"img\"+aria-label, so an asChild link keeps its link role.",
        "name": "productSuffix",
        "type": "React.ReactNode"
      },
      {
        "description": "Accessible name. Set → exposed as a named image (role img); omitted → decorative (aria-hidden), the correct default when a readable wordmark sits beside it. With `wordmark` set, `label` overrides the lockup's name (the wordmark text is otherwise the name). It also overrides the \"GoDX <productSuffix>\" name a drawn-logotype lockup composes for itself (role=\"img\" makes the sr-only brand word presentational).",
        "name": "label",
        "type": "string"
      },
      {
        "description": "Borrow the single child element as the logo ROOT instead of rendering a <span> — the way to make the whole logo a LINK (<a>, or a router <Link>). The borrowed tag itself carries data-slot=\"logo-lockup\"/.ui-logo-lockup and the mark + wordmark become its children, so there is no wrapper element between the link and the lockup. Use it instead of wrapping Logo in your own <a>: .ui-logo-lockup is display:inline-flex, so a plain <a> (display:inline) puts the lockup on a line box and the strut's descender nudges the mark up in a topbar — the only fix left to the consumer is className=\"flex\" on the <a>, which ui-audit rejects as no-utility-layout.",
        "name": "asChild",
        "type": "boolean"
      }
    ],
    "related": [
      "AuthIdentity (@godxjp/ui/layout) — the auth heading block. It draws `brand`, else the active preset's `brandMark` (the GoDX mark under `<AppProvider preset={godxPreset}>`), else no mark — so don't add a second Logo above it.",
      "Text / Heading — only for a bespoke lockup the `wordmark` prop cannot express; for the ordinary mark + product name, use `wordmark` (it owns the gap, face and brand colour as tokens).",
      "Avatar — use Avatar for a PERSON/entity image or initials; use Logo for the PRODUCT brand mark. They look similar (square/rounded glyph) but carry different meaning."
    ],
    "rules": [
      45,
      46
    ],
    "storyPath": "general/logo.tsx",
    "tagline": "The product brand mark — a glyph/identity box, or (with `wordmark`) the full mark + wordmark LOCKUP. Tokenised size/radius/type/gap, and `size` scales every branch (boxed glyph, identity mark, lockup); the boxed fill reads --primary, while the GoDX identity mark and wordmark read the --brand IDENTITY role instead, so an action-colour re-theme never recolours the brand.",
    "usage": [
      "DO import from `@godxjp/ui/general`: `import { Logo } from \"@godxjp/ui/general\";`",
      "DO use Logo INSTEAD of hand-rolling `<span aria-hidden className=\"grid size-7 place-items-center rounded-md bg-primary text-sm font-bold text-primary-foreground\">g</span>` — that repeats literal size/radius and puts type utilities on a bare span (rules #45/#46).",
      "DO leave `label` unset when a readable wordmark sits beside the mark (shell header, topbar) — the mark stays decorative and the wordmark carries the accessible name. Set `label` only when the mark stands alone.",
      "DON'T pass more than 1–2 glyphs — the box is square and centres its content; a long string overflows. The product NAME goes in `wordmark`, never in `glyph`.",
      "DO use `wordmark` for the full lockup: `<Logo mark=\"godx\" tone=\"success\" wordmark=\"GoDX\" />`. It replaces the hand-rolled `<span className=\"inline-flex items-center gap-2\"><Logo/><Text/></span>` — the gap, face, weight, tracking, per-tier size and the brand colour are all tokens, so a shell header / auth brand bar needs no page CSS.",
      "NOTE: the package ships NO wordmark ARTWORK — `wordmark` typesets the name in the design-system display face (`--logo-wordmark-font-family`). When design supplies a real logotype, pass it as an inline `<svg>` node to `wordmark`; do not approximate letterforms in CSS.",
      "DO use `<Logo mark=\"godx-lockup\" productSuffix=\"ID\" />` for a product surface branded \"GoDX | ID\" — the divider, the gaps, the `size` scale and the light/dark master are the package's. DON'T inline the kit's flattened \"GoDX | ID\" SVG in the app: it is a second master in its own coordinate system, with global `id=\"title\"`/`id=\"desc\"` that collide across instances, a hardcoded `#0B0F3B` ink with no dark variant, and a hardcoded `#C5C8D6` rule (gh#649).",
      "DON'T typeset the suffix yourself as `<Logo mark=\"godx-lockup\" /><Text>ID</Text>` — that has no divider and the spacing is not the lockup's token.",
      "DON'T re-tint via `className=\"bg-*\"` — the fill reads the `--primary` role token; retune it through a service theme (`--primary`, `--logo-radius`, `--logo-size-*`), not utilities.",
      "GoDX LOOK = THE PRESET (v32): the package defaults are neutral (ink `--primary`, no product mark). A GoDX surface gets the emerald-teal identity (#008066, gh#1345), the fonts and the GoDX mark in AuthIdentity from `import \"@godxjp/ui/themes/godx.css\"` (after the base stylesheet) plus `import { godxPreset } from \"@godxjp/ui/themes/godx\"` and `<AppProvider preset={godxPreset}>`. `mark=\"godx\"` / `\"godx-lockup\"` still render the GoDX artwork wherever you place them explicitly.",
      "DO use `mark=\"godx\"` for the canonical GoDX identity mark on hosted-identity screens — it is ALREADY in the package as real inline vector artwork. DON'T pass a hand-drawn brand SVG as `glyph`, and don't ship a brand asset in the app, to reproduce it.",
      "DO make the brand a link with `asChild`, not with a wrapper: `<Logo asChild mark=\"godx\" wordmark=\"GoDX\"><Link href=\"/\" /></Logo>`. Writing `<a className=\"flex\"><Logo …/></a>` instead is the exact shape ui-audit rejects (no-utility-layout), and dropping the `flex` misaligns the mark."
    ],
    "useCases": [
      "App-shell header brand lockup — `<Logo glyph=\"c\" wordmark=\"CoreBooks\" />` in the sidebar/topbar: mark decorative, wordmark readable, spacing tokenized.",
      "Auth screen — a standalone labelled mark above the sign-in form: `<Logo label=\"CoreBooks\" size=\"lg\" />`.",
      "Tenant/workspace switcher row — a small `size=\"sm\"` mark as the leading slot of a ListRow or menu item.",
      "Custom SVG brand — pass an inline `<svg>` as `glyph` to render a real logomark on the primary fill instead of a letter.",
      "Hosted GoDX identity surface — `<Logo mark=\"godx\" tone=\"success\" />` in an auth CenteredShell `brand` bar or inside <AuthIdentity>: the canonical GoDX mark the package already owns. There is no separate identity-mark component and no asset to import.",
      "Brand lockup in a shell header / auth brand bar — `<Logo mark=\"godx\" tone=\"success\" wordmark=\"GoDX\" />`: one element, brand-green, no wrapper div and no page CSS.",
      "Product-branded surface (\"GoDX ID\" legal pages, a Console/Admin topbar) — `<Logo mark=\"godx-lockup\" productSuffix=\"ID\" />`: the master lockup, the package's divider, and one accessible name, \"GoDX ID\"."
    ]
  },
  {
    "example": "import { Reveal } from \"@godxjp/ui/general\";\nimport { Card, CardContent } from \"@godxjp/ui/data-display\";\n\n// single entrance\n<Reveal>\n  <Card><CardContent>…</CardContent></Card>\n</Reveal>\n\n// staggered column\n{items.map((item, i) => (\n  <Reveal key={item.id} delay={Math.min(i + 1, 6) as 1 | 2 | 3 | 4 | 5 | 6}>\n    <Card><CardContent>{item.label}</CardContent></Card>\n  </Reveal>\n))}\n\n// scroll reveal — same component, the trigger moved to the viewport\n<Reveal on=\"view\">\n  <Card><CardContent>…</CardContent></Card>\n</Reveal>\n\n// wait until a quarter of the section is visible, and replay on every re-entry\n<Reveal on=\"view\" amount={0.25} once={false}>\n  <Card><CardContent>…</CardContent></Card>\n</Reveal>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Reveal",
    "props": [
      {
        "description": "Content to reveal on enter.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "defaultValue": "0",
        "description": "Stagger ordinal — an INDEX into the motion ladder, never a raw ms. Each step adds one `--reveal-stagger-step` of delay so a column of reveals cascades. 0 = enter immediately.",
        "name": "delay",
        "type": "0 | 1 | 2 | 3 | 4 | 5 | 6"
      },
      {
        "defaultValue": "\"mount\"",
        "description": "What STARTS the entrance. `mount` is the historical behaviour (plays as soon as the element renders), so existing call sites are unchanged. `view` holds the entrance until the element reaches the viewport. A trigger, not a second component — same keyframes, same tokens, same reduced-motion contract, which is why there is no `ScrollReveal` here.",
        "name": "on",
        "type": "\"mount\" | \"view\""
      },
      {
        "defaultValue": "true",
        "description": "on=\"view\" only. Reveal once and never re-hide. Default `true` because a Reveal is a ONE-SHOT entrance under either trigger — this keeps `view` semantically identical to `mount`. (Motion's useInView, where the prop comes from, defaults it to `false`; that is the neutral reading for a general-purpose observer, not for an entrance primitive.) `false` replays the entrance on every re-entry.",
        "name": "once",
        "type": "boolean"
      },
      {
        "defaultValue": "\"some\"",
        "description": "on=\"view\" only. How much of the element must be visible before it reveals — Motion's `amount`, name and type unchanged, mapped to IntersectionObserver `threshold` the same way (`some` → 0, `all` → 1, a number passes through). `all` is clamped to the most the element's own box can attain inside the viewport, because the ratio is measured against the TARGET's box: without the clamp a section taller than the viewport could never reach 1 and would stay hidden forever.",
        "name": "amount",
        "type": "\"some\" | \"all\" | number"
      },
      {
        "defaultValue": "false",
        "description": "Merge the reveal onto the single child element (no wrapper <div>) — use when an extra box would break a grid/flex layout. Under `on=\"view\"` the observed box is that child's own.",
        "name": "asChild",
        "type": "boolean"
      }
    ],
    "related": [
      "CenteredShell (auth shape) — pairs with Reveal for the auth card entrance; the auth shape delegates all motion to Reveal.",
      "Card — the most common thing to wrap in <Reveal> (dashboard cards, auth card, settings sections).",
      "ResponsiveGrid — combine with `<Reveal delay={n}>` (or `asChild`) per grid item for a staggered grid reveal.",
      "Carousel — wrap the WHOLE Carousel in one `<Reveal on=\"view\">`, never each CarouselItem: the rail clips its slides, so a per-slide observer never fires for the ones outside the window.",
      "ScrollArea — same rule as Carousel: reveal the scroller, not the children it clips."
    ],
    "rules": [],
    "storyPath": "general/reveal.tsx",
    "tagline": "The official entrance-motion primitive (staggered fade-up) — reads DS motion tokens and honours prefers-reduced-motion, replacing hand-rolled @keyframes + .app-reveal/.d1..d6 classes.",
    "usage": [
      "DO use <Reveal> INSTEAD of hand-rolling `@keyframes auth-fade-up` + `.app-reveal` + `.d1..d6` in a consumer global.css — that repeats literal durations/delays and violates the tokens-only rule. Reveal reads `--duration-slow` / `--ease-emphasized` / `--reveal-distance` / `--reveal-stagger-step`.",
      "DO use `on=\"view\"` for a scroll reveal INSTEAD of adding a scroll listener or a second component. It is the same primitive with the trigger moved; the library deliberately ships no ScrollReveal/AnimateOnScroll, and no animation runtime.",
      "DO NOT gate your own visibility on an observer. `on=\"view\"` never does: the stylesheet's resting state is the finished, fully visible one, and only a mounted component holding a live IntersectionObserver writes the hidden state. A server render, jsdom, a browser without the API, and `prefers-reduced-motion: reduce` (under which NO observer is attached at all) therefore all show the content.",
      "DO reach for `amount` rather than a margin when a reveal should wait — the default `\"some\"` fires on the first visible pixel (threshold 0), which is the earliest and never reads as late. There is deliberately no `margin`/`rootMargin` prop: it accepts px/% only, so it could not read a design token.",
      "DO stagger a list/column by passing an increasing `delay` (1, 2, 3…) to successive siblings — the ordinal maps to `--reveal-stagger-step`, so a service retunes the cascade rhythm from one token.",
      "DO pass `asChild` when wrapping an element that must keep its own box in a grid/flex row (the reveal merges onto that element instead of adding a <div>).",
      "DO rely on the built-in reduced-motion behaviour — under `prefers-reduced-motion: reduce` the animation is dropped and content renders in its final, fully-visible position with no layout shift. Never gate visibility on the animation.",
      "DON'T put `on=\"view\"` on an element a scroller CLIPS — a Carousel slide, a horizontal ScrollArea child, anything inside `overflow: hidden`/`clip`. The observer's root is the VIEWPORT, and a clipped box has an EMPTY intersection rect, so an item parked outside its rail never leaves `data-reveal-state=\"out\"` and stays at `opacity: 0` forever. Measured on a 4-slide rail at 1440: 3 of the 4 slides sat invisible after scrolling the whole document, and stayed invisible. This is the one failure this component may not have. Reveal the RAIL (one `<Reveal on=\"view\">` around the whole `<Carousel>`/`<ScrollArea>`), never the slides.",
      "DON'T budget a reveal per card inside a rail even when it does work — one region is the entrance, the cascade belongs to a grid whose items all share the page's scroll. `delay` caps at 6, so a long list should pass `Math.min(i, 5) + 1` rather than the raw index.",
      "DO expect a re-filtered list to re-enter: changing a filter unmounts and remounts the wrappers, so each new item gets a fresh observer and animates again. Items already scrolled past are re-revealed on their way back, which is correct — they are never left hidden.",
      "DO NOT set a raw ms delay or a literal translate distance — the whole point is that `delay` is a controlled ordinal and the distance/duration come from tokens."
    ],
    "useCases": [
      "Auth card entrance: `<CenteredShell variant=\"auth\"><Reveal><Card/></Reveal></CenteredShell>` — the sign-in card fades up on load, respecting reduced-motion.",
      "Staggered dashboard: map stat cards with `<Reveal delay={i + 1}>` so the row cascades in.",
      "Section reveal on a settings/detail page — wrap each Card in <Reveal> for a calm entrance without hand-written CSS.",
      "asChild on a grid item: `<Reveal asChild delay={2}><ResponsiveGrid.Item/></Reveal>` keeps the grid cell intact while animating it in.",
      "Long marketing/landing page: `<Reveal on=\"view\">` per section so each band enters as the reader reaches it, instead of all of them firing above the fold on load.",
      "A long feed or report where only the first screen should animate on load: `<Reveal on=\"view\" amount={0.25}>` — a quarter visible before the entrance starts."
    ]
  },
  {
    "absorbed": [
      "Spin",
      "Spinner",
      "Loading",
      "LoadingIndicator"
    ],
    "example": "import { Activity } from \"@godxjp/ui/general\";\n\n// a channel typing indicator — no live region by default (it flickers with every socket event)\n<Activity label={t(\"channel.typing\", { name: \"佐藤\" })} />\n\n// an ambient sync mark in a header\n<Activity variant=\"bar\" tone=\"info\" label={t(\"sync.running\")} />\n\n// a reconnect notice that must actually be heard, announced once and politely\n<Activity announce=\"polite\" tone=\"warning\" label={t(\"realtime.reconnecting\")} />",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Activity",
    "props": [
      {
        "defaultValue": "\"dots\"",
        "description": "The mark. `dots` = three dots rising in sequence, the ellipsis convention (someone is typing). `pulse` = a single breathing mark (live / recording). `bar` = an indeterminate sweep (syncing / streaming).",
        "name": "variant",
        "type": "\"dots\" | \"pulse\" | \"bar\""
      },
      {
        "defaultValue": "\"sm\"",
        "description": "Size step. Default `sm` — an ambient mark is never the loudest thing on screen. Scales the mark AND the label together (the mark is em-based off `--activity-font-size-*`).",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "\"muted\"",
        "description": "Semantic colour intent for the mark and the label.",
        "name": "tone",
        "type": "\"default\" | \"muted\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"inherit\""
      },
      {
        "description": "Localized description of WHAT is happening ('佐藤さんが入力しています…', 'Hưng đang nhập…'). Rendered as visible Text beside the mark when `children` are absent; when `children` ARE present it becomes an sr-only description instead. Consumer-owned copy — the library never invents it, and 'N people are typing' must be pluralized with the consumer's Intl.PluralRules.",
        "name": "label",
        "type": "ReactNode"
      },
      {
        "description": "Richer visible content in place of `label` (a name in a <strong>, a Badge…). The mark stays aria-hidden; pass `label` alongside for the sr-only description.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "Announce the label to assistive technology. Default `false` and then NO live region is emitted at all — the DELIBERATE default, because an ambient indicator that fires a live region on every socket event is a screen-reader flood. `'polite'` wraps ONLY the label in one aria-live='polite' aria-atomic='true' region; the mark stays outside it and aria-hidden.",
        "name": "announce",
        "type": "false | \"polite\""
      }
    ],
    "related": [
      "Reveal — the one-shot ENTRANCE counterpart. Reveal runs once on mount; Activity runs forever. Both live in styles/motion.css and both drop their animation under prefers-reduced-motion.",
      "Skeleton — content is LOADING (aria-busy + a shaped placeholder). Use Skeleton when the content itself has not arrived; use Activity when content is present and something is happening elsewhere.",
      "Button (loading) — THIS action is in flight, on a control. Not an ambient state.",
      "Progress — a DETERMINATE amount is done. Activity's `bar` variant is the indeterminate case, where no percentage exists.",
      "DataState — antd `Spin`'s wrapper form (`<Spin spinning>{children}</Spin>`) over a query: skeleton → prerequisite → empty → error, with cause-aware retry. This is what to reach for when the REGION is loading.",
      "antd `Spin` — no such component here, by a standing parity ruling (antd Spin is not ported). Its four jobs are Activity / Skeleton / DataState / Button `loading`. Its `delay` (flicker guard) is tracked separately against Button as `loadingDelay`; its `fullscreen` is a Dialog/app-shell concern, not an indicator one."
    ],
    "rules": [],
    "storyPath": "general/activity.tsx",
    "tagline": "The official AMBIENT-motion primitive and the standalone indeterminate indicator (antd `Spin`) — a continuous, unbounded 'something is happening right now, elsewhere' mark (someone typing, a sync running, a response streaming, a recording live). Loading a REGION is Skeleton; an in-flight ACTION is Button loading; a known percentage is Progress.",
    "usage": [
      "DO read this first if you came looking for a Spin/Spinner (gh#830). antd's `Spin` is ONE component with a `spinning` boolean covering four different situations; here those are four components, because the accessible semantics of each are genuinely different: (1) something is happening ELSEWHERE, indefinitely → Activity, no live region by default; (2) the CONTENT of this region is loading → Skeleton (aria-busy + aria-live, shaped placeholders) or DataState (the whole skeleton → prerequisite → empty → error lifecycle); (3) THIS action is in flight → Button `loading` (aria-busy + activation blocked on the control itself); (4) a percentage is known → Progress. Picking by shape ('I want the round one') is how a persistent indicator ends up telling a screen reader the page is busy forever.",
      "DO use `variant='bar'` for the indeterminate case — that IS this library's indeterminate indicator, and the mark being a sweeping bar rather than a rotating circle is a system-level decision, not a gap. antd's `Spin percent='auto'` (a synthesized percentage that never reaches 100) is deliberately NOT ported: a fabricated number over an unknown wait is a determinate-looking lie, and Progress is there for when the number is real.",
      "DO use <Activity> INSTEAD of hand-rolling `@keyframes typing-bounce` in a consumer app CSS. That re-derives interval/amplitude/stagger the DS owns as tokens (`--duration-loop`, `--activity-interval`, `--activity-stagger-step`, `--activity-mark-offset`) and needs its own prefers-reduced-motion guard — the guard consumers forget.",
      "DO NOT reuse Skeleton for an ambient indicator. Skeleton hard-codes `aria-busy='true'` + `aria-live='polite'` because it means CONTENT IS LOADING; a persistent typing indicator built on it tells every screen reader the region is busy for as long as anyone is typing, and re-announces. Activity emits neither by default.",
      "DO NOT reuse Button `loading`. That is a spinner bound to an in-flight action, on a control. Activity means something is happening indefinitely, ELSEWHERE.",
      "DO leave `announce` at its default `false` for a flickering affordance (typing, presence). Opt into `announce='polite'` only when the change genuinely must be heard (a reconnect banner, a recording that just started) — and never on a value that changes on every socket event.",
      "DO always pass a localized `label` (or `children`) when the indicator carries meaning: the mark alone announces nothing and communicates nothing under reduced motion. Route the string through your app's t()/Intl.PluralRules — the library ships no copy for it.",
      "DO rely on the built-in reduced-motion behaviour — under `prefers-reduced-motion: reduce` the loop is dropped and each mark falls back to a DESIGNED resting state (three solid dots / a solid pulse mark / a bar segment parked at the reading-start), never to nothing, with no layout shift (WCAG 2.2 SC 2.3.3 and SC 2.2.2).",
      "DO retune the ambient feel from a service theme, not per call site: `--activity-interval` (rhythm), `--activity-stagger-step` (dot offset), `--activity-mark-size` / `--activity-mark-offset` (mark and travel), `--activity-mark-rest-alpha`, `--activity-pulse-mark-size`, `--activity-gap`, `--activity-font-size-{xs,sm,md,lg}`, `--activity-bar-{width,height,radius,segment-width,track-alpha}`, `--activity-color`.",
      "DO reserve the indicator's row height in the surrounding layout (it sits above a composer): Activity itself never changes size, but the row appearing and disappearing is the consumer's layout to keep stable.",
      "DO NOT add `role='status'` yourself — that implies an unconditional polite live region, which is exactly what the `announce` default exists to keep off. Activity is not focusable and is not a tab stop."
    ],
    "useCases": [
      "A chat channel's typing affordance under the composer: `<Activity label={t('channel.typing', { name })} />` — appears and disappears on socket events without ever announcing.",
      "A live-sync pulse in a page header: `<Activity variant='bar' tone='info' label='同期中…' />`.",
      "A streaming assistant response: `<Activity variant='dots' label='生成しています…' />` under the partial answer.",
      "A recording / live mark: `<Activity variant='pulse' tone='destructive' label='録画中' />`.",
      "A live-updating dashboard tile: `<Activity variant='pulse' size='xs' label='リアルタイム更新中' />` beside the metric.",
      "A reconnect notice that MUST be heard once: `<Activity announce='polite' tone='warning' label='接続を再試行しています…' />`."
    ]
  },
  {
    "example": "import { Lock, ShieldCheck } from \"lucide-react\";\nimport { Icon, Text } from \"@godxjp/ui/general\";\n\n// decorative — aria-hidden, on the same step as the text beside it\n<Text size=\"sm\">\n  <Icon as={Lock} size=\"sm\" tone=\"muted\" /> {t(\"invoice.encrypted\")}\n</Text>\n\n// the glyph IS the value — give it a name\n<Icon as={ShieldCheck} size=\"md\" tone=\"success\" label={t(\"session.secure\")} />",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Icon",
    "props": [
      {
        "description": "The glyph COMPONENT — `as={Lock}`, not `<Lock />`. Every lucide-react icon qualifies. Icon renders ONTO it (the sized element is the <svg> itself), so no wrapper box enters the row and Button's own `svg` rule still sees a direct svg child.",
        "name": "as",
        "required": true,
        "type": "ComponentType<SVGProps<SVGSVGElement> & RefAttributes<SVGSVGElement>>"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Step of the nine-step icon scale (10/12/14/16/20/24/36/40/48px at a 16px root — docs/TOKENS.md). NOT the four-step control SizeProp: a glyph beside 2xs caption text and a 48px empty-state mark are the same primitive. An explicit step OUT-RANKS the context rules, so `size=\"lg\"` inside a Button renders at 20px, not the button's 16px.",
        "name": "size",
        "type": "\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"4xl\""
      },
      {
        "description": "Semantic colour intent, the SAME vocabulary Text uses (a status glyph reads the AA-safe --text-warning, never the --warning fill role). Omitted by default, and that default is load-bearing: a glyph inherits currentColor, so an Icon inside a Button, a Badge or a toned Text paints in that surface's own ink without being told.",
        "name": "tone",
        "type": "\"default\" | \"muted\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"inherit\""
      },
      {
        "description": "Accessible NAME, for a glyph that is the only thing saying what it says (a lock in a status cell with no text beside it). Supplying it renders role=\"img\" + aria-label; leaving it off (the default) renders aria-hidden, which is correct for a glyph beside a visible label. Consumer-owned copy — route it through your t().",
        "name": "label",
        "type": "string"
      },
      {
        "description": "Merged onto the glyph. NOT the place to size it — the sizing rule is (0,3,1) and deliberately out-ranks a utility, so pass `size` instead.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Button / DropdownMenuItem / TopbarItem / ListRow — the four contexts that size a glyph for you. Inside them a bare glyph is already correct; reach for Icon there only when you want a DIFFERENT step.",
      "Text — shares the `tone` vocabulary, and is the usual thing an Icon sits beside. Match the icon step to the text step (`size=\"sm\"` beside `Text size=\"sm\"`).",
      "Logo — a brand mark, not a glyph: it has its own sizes and its own tone axis.",
      "Avatar — a person or an entity, not an icon; it brings its own box and must not be squeezed into an icon measure."
    ],
    "rules": [],
    "storyPath": "general/icon.tsx",
    "tagline": "A glyph on the --icon-size-* scale, and the ONLY supported way to render a standalone one. A lucide component ships width=\"24\" height=\"24\"; only four rules in this library override that (.ui-button svg, the menu row, the topbar cell, the ListRow leading slot), so everywhere else a glyph draws at 24px beside 14px text.",
    "usage": [
      "DO use <Icon> for ANY glyph that is not already inside Button / DropdownMenuItem / TopbarItem / a ListRow leading slot — in a Text, a table cell, an <a>, a Flex. Those four are the only contexts in the library that size a glyph for you; everywhere else a bare lucide icon renders at 24px.",
      "DO NOT reach for `className=\"size-4\"` or `w-[16px]` — docs/CONSUMER-RULES.md §3/§8 forbid them, and they re-derive a metric the theme owns. DO NOT pass lucide's own `size={16}` either: that is a literal where the design system has a scale.",
      "DO leave `label` off for a decorative glyph beside a visible label — Icon is aria-hidden by default, which is what WCAG 2.2 SC 1.1.1 asks for. Announcing the same thing twice is the usual defect.",
      "DO pass a localized `label` when the glyph carries meaning nothing else on screen says (a lock alone in a status column). It becomes role=\"img\" + that name.",
      "DO leave `tone` unset inside a Button, a Badge or a coloured surface, so the glyph inherits that surface's ink. Set it only when the glyph carries its own status meaning.",
      "DO retune the SCALE from the theme (`--icon-size-sm`, `--icon-size-lg`, …) rather than per call site; a single instance that genuinely sits off the scale sets `--icon-glyph-size` inline (tier 2, docs/TOKENS.md).",
      "DO NOT wrap the glyph in a <span> to size it — a wrapper adds a box to every flex row that holds an icon, and it puts an element between Button and the svg its own rule targets."
    ],
    "useCases": [
      "A status glyph inline with 14px body text: `<Text size=\"sm\"><Icon as={Lock} size=\"sm\" tone=\"muted\" /> 暗号化済み</Text>`.",
      "A lock/unlock column in a table, where the glyph IS the value: `<Icon as={Lock} size=\"sm\" label={t('row.locked')} />`.",
      "A glyph that must be bigger than the control around it: `<Button><Icon as={Download} size=\"lg\" />…</Button>`.",
      "An icon inside a breakpoint wrapper (`<Flex hideBelow=\"sm\"><Icon as={Building2} size=\"lg\" /></Flex>`), where a direct-child rule can no longer reach it.",
      "An external-link marker at the end of a link: `<Icon as={ArrowUpRight} size=\"sm\" />`."
    ]
  },
  {
    "absorbed": [
      "DataGrid"
    ],
    "example": "import { useState } from \"react\";\nimport { Badge, DataTable, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\n\ntype Invoice = {\n  id: string;\n  customer: string;\n  amount: number;\n  status: \"paid\" | \"pending\" | \"overdue\";\n};\n\nconst columns: ColumnDef<Invoice>[] = [\n  { key: \"id\", header: \"Invoice #\", width: \"w-32\" },\n  { key: \"customer\", header: \"Customer\" },\n  {\n    key: \"status\",\n    header: \"Status\",\n    render: (row) => (\n      <Badge\n        tone={\n          row.status === \"paid\" ? \"success\" : row.status === \"overdue\" ? \"destructive\" : \"warning\"\n        }\n      >\n        {row.status}\n      </Badge>\n    ),\n  },\n  { key: \"amount\", header: \"Amount\", align: \"right\", sortable: true },\n];\n\nexport default function InvoiceList({\n  invoices,\n  loading,\n}: {\n  invoices: Invoice[];\n  loading: boolean;\n}) {\n  const [selected, setSelected] = useState<Set<string>>(new Set());\n  const [sort, setSort] = useState<{ key: string; direction: \"asc\" | \"desc\" } | undefined>();\n\n  return (\n    <DataTable\n      data={invoices}\n      columns={columns}\n      getRowId={(row) => row.id}\n      selectable\n      selected={selected}\n      onSelectChange={setSelected}\n      sort={sort}\n      onSortChange={setSort}\n      loading={loading}\n      empty={\n        <EmptyState\n          title=\"No invoices found\"\n          description=\"Adjust your filters or create a new invoice.\"\n        />\n      }\n    >\n      <DataTable.Toolbar>\n        <DataTable.BulkActions>\n          <button type=\"button\" onClick={() => setSelected(new Set())}>\n            Mark paid\n          </button>\n        </DataTable.BulkActions>\n        <DataTable.DensityToggle />\n      </DataTable.Toolbar>\n    </DataTable>\n  );\n}",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "DataTable",
    "props": [
      {
        "description": "Array of row data. When empty and loading is false, a built-in EmptyState renders automatically inside the table body — no external guard needed.",
        "name": "data",
        "required": true,
        "type": "T[]"
      },
      {
        "description": "Lean column definitions (adapted to TanStack internally — `meta.lean` is the declared home for every custom column option, so `priority` needs no second TanStack channel). Each column: { key: string; header: ReactNode; ariaLabel?: string; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hideBelow?: 'sm'|'md'|'lg'|'xl' (same contract as Flex hideBelow — stamped as data-hide-below on th/td); hiddenOnMobile?: boolean (alias for hideBelow:'md'); enableHiding?: boolean; pin?: 'end'; priority?: 'primary'|'secondary'|'meta'|'actions'; flush?: boolean }. flush (gh#1016 — the same word as TableCell flush / CardContent flush) drops the column's BODY cell padding so a self-padded row primitive owns the inset: a selectable list whose single column renders a `ListRow` writes `{ key: 'row', header: '', ariaLabel: …, flush: true, render: (m) => <ListRow asChild …><a/></ListRow> }` — the ListRow then sits 16px from the checkbox (not 32px) and the row is ListRow's own height; an unread ListRow in a flush cell paints `--list-row-unread-background` across the whole table row (checkbox cell included). The header keeps its padding, so the header label sits on the ListRow's inset. Never zero the cell with a className instead. priority is the column-priority contract read by preset=\"action-collection\" — DataTable stamps it as data-priority on the <th> AND every <td> of the column, so the preset can allocate the narrow-frame measure; leave the free-text column unmarked (it takes the remaining space), and prefer priority over width under the preset because an explicit width utility wins the cascade and defeats the measure. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow — pin at most one column. ariaLabel gives a VISUALLY-EMPTY header (header='' — an action or selection column) a screen-reader name (e.g. 'Actions'/'Select'): it renders as an sr-only label inside the <th> so the column is never nameless (axe: empty-table-header). ANT DESIGN PARITY on the same column: fixed:'start'|'end' freezes the column against a scroll edge (logical, so it mirrors in RTL; the stacking offsets are MEASURED from the rendered header, so several adjacent frozen columns are correct at any width — pin:'end' is the older spelling of fixed:'end'). ellipsis holds the cell to one line (it also switches the table to table-layout: fixed, without which no ellipsis truncates anything); a cell that IS truncated — rendered cells too — gets a title from its text and a tab stop whose keyboard focus unfolds the text (PatternFly Truncate), and an ellipsis column's HEADER wraps instead of truncating (gh#1262). Truncation is detected on the cell OR on a descendant that ends in an ellipsis — a `Text truncate` link beside a badge or avatar — and the title then carries that descendant's text (gh#1269). headerWrap lets any column's header wrap (default true with ellipsis, false otherwise). sorter is antd's richer `sortable`: true | (a, b) => number | { compare, multiple }, where multiple is the MULTI-column sort priority (highest sorts first). sortOrder / defaultSortOrder / sortDirections control and shape the cycle per column, and showSorterTooltip explains the next step. filters + onFilter + filteredValue / defaultFilteredValue / filterMultiple add a real filter menu to the header (filterMultiple: false makes it single-choice); omit onFilter for a server filter and drive it from the table's onFilterChange.",
        "name": "columns",
        "required": true,
        "type": "ColumnDef<T>[]"
      },
      {
        "defaultValue": "(row) => String(row.id)",
        "description": "Extracts a stable unique string key per row. Required when selectable is true or rows lack an 'id' field. Falls back to row.id cast to string.",
        "name": "getRowId",
        "type": "(row: T) => string"
      },
      {
        "description": "Human name of a row — what its selection checkbox (or radio) is announced as: `Select row {label}`. Default: the text of the `priority: \"primary\"` column, else of the first column, when that value is a string or number; the row id only as a last resort, because an id is a KEY and announced it reads a UUID aloud. Set it when the first column is not the row's name (an avatar, a status badge, an id). `rowSelection.getCheckboxProps` `aria-label` still overrides a single row.",
        "name": "getRowLabel",
        "type": "(row: T) => string"
      },
      {
        "defaultValue": "false",
        "description": "Adds a checkbox column and a SelectAll header checkbox. Use with selected + onSelectChange for controlled selection, or omit both for uncontrolled.",
        "name": "selectable",
        "type": "boolean"
      },
      {
        "description": "Controlled set of selected row IDs. Pair with onSelectChange. Omit for uncontrolled.",
        "name": "selected",
        "type": "Set<string>"
      },
      {
        "description": "Per-row STATE — a leading-edge rail plus a weak wash, in the same six tone names Card `accent` uses, so a row needing attention and a card needing attention are one vocabulary. Return undefined for an ordinary row. This is the supported alternative to painting rows through rowClassName: ui-audit treats any prop whose name ends in `className` as a class expression, so a `border-l-4 bg-amber-50` inside that arrow is an error in a consumer. NEVER the only signal — colour alone cannot carry meaning (WCAG 1.4.1), so keep the reason in a cell (a Badge, a status column) and let the rail make that cell findable in a long table. The rail reads the --mark-* tier, which is held at 3:1 against its own row by src/tokens/__tests__/tone-mark-contrast.test.ts.",
        "name": "rowTone",
        "type": "(row: T) => \"primary\" | \"success\" | \"warning\" | \"info\" | \"attention\" | \"destructive\" | undefined"
      },
      {
        "description": "Called with the full new selection set after any checkbox interaction.",
        "name": "onSelectChange",
        "type": "(next: Set<string>) => void"
      },
      {
        "description": "Makes rows clickable for navigation. Row click is suppressed when the user clicks an interactive descendant (button, a, input, select, textarea, [role=menuitem]).",
        "name": "onRowClick",
        "type": "(row: T) => void"
      },
      {
        "defaultValue": "'compact'",
        "description": "Controlled row density across all three tiers (compact 28 / default 36 / comfortable 48) — drive it from a 表示密度 radio. Omit to let DataTable manage it internally (DataTable.DensityToggle flips compact↔comfortable).",
        "name": "density",
        "type": "'compact' | 'default' | 'comfortable'"
      },
      {
        "description": "Called when the user toggles density. Only needed when density is controlled.",
        "name": "onDensityChange",
        "type": "(density: 'compact' | 'default' | 'comfortable') => void"
      },
      {
        "description": "Zebra rows: every EVEN LOGICAL record paints --table-row-striped-background (default --muted at 0.8 alpha — every text role on it stays at AA, computed at the row so dark mode and scoped themes follow). Parity is by record, not DOM row — an expanded detail row is skipped when counting and wears its own record's stripe; on a paged table the count restarts per rendered page. Frozen (`fixed`) cells wear the stripe over their opaque base; hover, selection, `rowClassName` and `rowTone` all still read on a striped row. OMIT to inherit the theme default (`--table-row-striped-alpha`, 0% unless the service set it); `true` / `false` override it for this table. Element Plus `stripe` / Bootstrap `.table-striped`; antd has no prop.",
        "name": "striped",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Highlight a row on hover even when it is not clickable. onRowClick already implies hover; use this for read-only tables that still want the hover affordance.",
        "name": "hoverable",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Pin the header to the top while the body scrolls (ヘッダ追従). Set false to let it scroll away with the rows.",
        "name": "stickyHeader",
        "type": "boolean"
      },
      {
        "defaultValue": "'default'",
        "description": "Named collection contract — the SAME preset the Table primitive owns, forwarded to the table DataTable renders. 'default' emits NO attribute and matches no selector, so an existing DataTable is byte-identical. 'action-collection' is the canonical dense approval/action queue: below collapseBelow the desktop INTRINSIC column widths give way to the token-owned column-PRIORITY measures (--table-action-collection-*) under table-layout: fixed, cells wrap, and the bordered surface drops its --table-surface-min-inline-size floor — so requester · target · reason · requested date · row actions all stay inside a 390px frame with no horizontal scroll. Mark each column with `priority` on its ColumnDef. Semantics are untouched (no display change, no role rewriting, no card swap), so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. Measured: table 1182 / 766 / 388px at 1440 / 1024 / 390, document scrollWidth === clientWidth at every width, LTR and RTL. 'stacked-record-collection' is the other direction, for a WIDE, HETEROGENEOUS record set that has no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Each cell then carries its column's header inline above the value, DERIVED from the same ColumnDef.header the <th> uses — so the card cannot drift from the table, and a column with a deliberately empty header names itself with ariaLabel (gh#864). In card mode the CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The labels are aria-hidden: the real <th> is still in the DOM and remains the accessible-name source, so screen-reader table navigation is unchanged at every width. Reach for this instead of building a parallel Card tree beside the table; two trees for one dataset means two sets of labels to keep in sync.",
        "name": "preset",
        "type": "'default' | 'action-collection' | 'stacked-record-collection'"
      },
      {
        "defaultValue": "'sm'",
        "description": "Step at which preset=\"action-collection\" switches to the compact priority measures, or preset=\"stacked-record-collection\" folds its rows into cards. Measured against the TABLE'S OWN container (a container query on sm 40rem · md 48rem · lg 64rem · xl 80rem), not the viewport — a table inside a master rail collapses before the page does, and the same table folds by the width it is GIVEN. Ignored while preset is 'default'.",
        "name": "collapseBelow",
        "type": "'sm' | 'md' | 'lg' | 'xl'"
      },
      {
        "description": "Accessible name for the horizontal-scroll REGION — the tabindex=\"0\" wrapper a keyboard user lands on to scroll a table wider than its container — NOT for the <table> element (pass aria-label for that; it still reaches the table). OPTIONAL: left out, the region takes the localized dataTable.scrollRegion default (\"Scrollable table\"), so no consumer has to invent a name for every table. Pass a plain string when the page can say WHICH table; a non-string node cannot be an aria-label and falls back to the default. The wrapper carries role=\"group\" (not \"region\" — a named region is a LANDMARK, and several tables on one page would then collide under axe landmark-unique) and is emitted ONLY while the region actually has overflow to reach, measured at runtime: a table that fits adds no tab stop, no role and no name. (gh#817)",
        "name": "label",
        "type": "LabelProp"
      },
      {
        "description": "Active sort state (controlled/server surface). When provided alongside onSortChange, sortable columns show directional arrow icons and clicking the active column twice clears sort (calls onSortChange(undefined)). Omit both sort and onSortChange to sort client-side via TanStack.",
        "name": "sort",
        "type": "{ key: string; direction: 'asc' | 'desc' }"
      },
      {
        "description": "Called when a sortable column header is clicked. Receives undefined when sort is cleared (third click on same column). Providing sort or onSortChange opts into the controlled (server) sort surface; omit both and the table sorts client-side via TanStack.",
        "name": "onSortChange",
        "type": "(sort: { key: string; direction: 'asc' | 'desc' } | undefined) => void"
      },
      {
        "description": "Global search term surfaced by DataTable.Search. Omit both for client-side filtering; pass them to drive a server query (with manualFiltering).",
        "name": "globalFilter / onGlobalFilterChange",
        "type": "string / (next: string) => void"
      },
      {
        "description": "Pagination state. THREE shapes: the TanStack `{ pageIndex, pageSize }` (surfaced by a composed DataTable.Pagination); antd's TablePaginationConfig `{ total, current (1-based), pageSize, pageSizeOptions, showSizeChanger, showTotal, position, onChange(page, pageSize) }`; or `false` (no pager, rows unsliced). The antd object WITHOUT a composed DataTable.Pagination renders the table's own footer — the real Pagination (total beside the page numbers) at `position` (TablePaginationPositionProp[], logical: topStart|topCenter|topEnd|bottomStart|bottomCenter|bottomEnd|none; default ['bottomEnd'] = antd bottomRight), `size=\"sm\"` on a compact table so it matches the toolbar's sm controls. Server-paged: `pagination={{ total, current, pageSize, onChange }}` with `data` = the current page — a total larger than data.length with ≤ pageSize rows reads as server paging (antd's rule), giving ceil(total / pageSize) pages. A composed DataTable.Pagination keeps its own footer (never two pagers). `hideOnSinglePage` defaults to FALSE here (antd's table default), so a list filtered down to one page keeps its total and rows-per-page select; pass `hideOnSinglePage: true` for the standalone Pagination's behaviour.",
        "name": "pagination / onPaginationChange / rowCount",
        "type": "{ pageIndex: number; pageSize: number } | TablePaginationProp | false / OnChangeFn / number"
      },
      {
        "description": "Column show/hide state surfaced by DataTable.ViewOptions ('set view'). Internal if omitted.",
        "name": "columnVisibility / onColumnVisibilityChange",
        "type": "VisibilityState / OnChangeFn<VisibilityState>"
      },
      {
        "defaultValue": "false",
        "description": "Default false so the simple data+columns case sorts/filters/paginates in-browser. Set the relevant flag true and drive the matching state from your query for server-side behaviour.",
        "name": "manualSorting / manualFiltering / manualPagination",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "When true, swaps the body for SHAPED skeleton rows rendered inside the table's own grid (one border, aligned columns) — never a separate <SkeletonTable> in a Card (that double-borders). With React Query keepPreviousData, drive this off isPlaceholderData (pagination/search) || isLoading (first load), NOT isLoading alone. Suppresses the empty state while true.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Custom content rendered inside the table body when data is empty and loading is false. Defaults to a built-in EmptyState with a localised 'No data' message. Pass a custom <EmptyState title='...' description='...' action={...}/> to tailor the message.",
        "name": "empty",
        "type": "ReactNode"
      },
      {
        "description": "FAILURE state. Pass error={isError} — `true` renders the built-in localized destructive EmptyState ('Couldn't load this list') announced with role='alert'; any other node REPLACES that copy (e.g. an <Alert> carrying an error code + request id). `false`/`undefined` means the read succeeded. NEVER pass a raw Error object (it is not renderable).",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "description": "PERMISSION-DENIED state — the read was REFUSED (403), not failed. `true` renders the built-in localized warning EmptyState ('You don't have access to this list') with NO retry, announced politely (aria-live) because a permission boundary is expected information, not a fault. Takes precedence over `error`. Any other node replaces the copy.",
        "name": "denied",
        "type": "ReactNode"
      },
      {
        "description": "Retry handler surfaced as a Retry button inside the BUILT-IN error state only. Omit it to render the error without a retry affordance; it is intentionally never offered for `denied` (repeating a 403 cannot succeed).",
        "name": "onRetry",
        "type": "() => void"
      },
      {
        "description": "Full row-selection configuration (antd rowSelection). Supersedes — and can be mixed with — selectable/selected/onSelectChange, which drive the same state. type:'radio' makes the column single-choice (no header checkbox at all). getCheckboxProps is the declared home for 'this row cannot be selected' (disabled) and for a per-row accessible name. preserveSelectedRowKeys keeps a key selected after its row leaves `data` (server paging / a filter), which is the only way a select-across-pages bulk action can be correct. selections adds bulk entries under the header checkbox (true = the built-in all · invert · none; a list mixes antd's DataTable.SELECTION_ALL / SELECTION_INVERT / SELECTION_NONE with custom entries in the order given). selectAllLabel names the header checkbox — say what it really selects (on a server-paged table: the page) instead of replacing it through columnTitle. matching (no antd equivalent; Gmail/Jira/GitHub) is the server-paged 'select all N matching' banner: once the whole page is ticked and total exceeds the page, a polite status above the header offers 'Select all N matching rows'; choosing it reports onSelectedChange(true) — send your FILTER, not the ids, for the bulk write — and while selected every row of any page shows ticked. Unticking a row or 'Clear selection' reports false. With matching set, the header checkbox defaults to 'Select all rows on this page'.",
        "name": "rowSelection",
        "type": "{ type?: 'checkbox'|'radio'; selectedRowKeys?: string[]; defaultSelectedRowKeys?: string[]; onChange?: (keys, rows) => void; getCheckboxProps?: (row) => { disabled?, 'aria-label'? }; preserveSelectedRowKeys?: boolean; selections?: true | ({ key, text, onSelect } | DataTable.SELECTION_ALL | DataTable.SELECTION_INVERT | DataTable.SELECTION_NONE)[]; hideSelectAll?: boolean; columnTitle?: ReactNode; selectAllLabel?: string; matching?: { total: number; selected: boolean; onSelectedChange: (selected: boolean) => void } }"
      },
      {
        "description": "Expandable detail rows (antd expandable). Supplying expandedRowRender adds a leading expand column before the selection column and renders the panel in a real <tr> spanning every column, so the table's grid semantics survive. rowExpandable gates the affordance per row; expandedRowKeys + onExpandedRowsChange make it controlled. The detail cell is inset like any cell (antd) and wraps (white-space: normal) — retune the inset with --table-row-expanded-padding; flush: true drops it for a detail that owns its inset (a nested table, a ListRow list). Do NOT wrap the detail in Card/CardContent or Flex pad for spacing (gh#1083).",
        "name": "expandable",
        "type": "{ expandedRowRender?: (row, index, expanded) => ReactNode; rowExpandable?: (row) => boolean; defaultExpandAllRows?: boolean; expandedRowKeys?: string[]; onExpandedRowsChange?: (keys) => void; expandRowByClick?: boolean; columnTitle?: ReactNode; flush?: boolean }"
      },
      {
        "description": "Footer totals row (antd summary), rendered in a real <tfoot> so it keeps the column widths and the screen-reader row navigation. Receives the rows currently rendered (post sort/filter/page), so a page total and a grand total are both expressible. Compose the return with <TableRow>/<TableCell> from the Table primitive.",
        "name": "summary",
        "type": "(rows: readonly T[]) => ReactNode"
      },
      {
        "description": "Scroll envelope (antd scroll). x is the table's MINIMUM inline size — it scrolls horizontally past it; y is the body's MAXIMUM block size — it scrolls vertically past it, with the sticky header staying put. Both are published as --table-scroll-inline-size / --table-scroll-block-size, so the lengths stay data and the geometry stays in the stylesheet. Setting x also switches the table to `table-layout: fixed`, which is what makes column widths (and `ellipsis`) authoritative.",
        "name": "scroll",
        "type": "{ x?: number | string; y?: number | string }"
      },
      {
        "description": "Sticky header (antd sticky). Supersedes stickyHeader when given. The object form carries the offset a page-level fixed topbar needs, published as --table-sticky-offset.",
        "name": "sticky",
        "type": "boolean | { offsetHeader?: number | string }"
      },
      {
        "description": "Per-row DOM props merged onto the <tr> (antd onRow) — a context menu, a drag handle, a data attribute for an E2E hook. The returned onClick/onKeyDown/className COMPOSE with the built-in row-click and row-tint behaviour rather than replacing it. For plain row navigation prefer onRowClick, which already handles the keyboard and the interactive-descendant guard.",
        "name": "onRow",
        "type": "(row: T, index: number) => React.HTMLAttributes<HTMLTableRowElement>"
      },
      {
        "defaultValue": "false",
        "description": "Draw the vertical rules between columns (antd bordered), forwarded to the Table primitive. The surface keeps drawing the outer frame, so the two never stack. Reach for it when the table carries merged cells or a dense numeric grid.",
        "name": "bordered",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Explain the NEXT sort step in a tooltip on every sortable header (antd showSorterTooltip). Defaults to false, not antd's true, so an existing table gains no hover chrome; a column's own showSorterTooltip overrides it either way.",
        "name": "showSorterTooltip",
        "type": "boolean"
      },
      {
        "defaultValue": "['asc', 'desc']",
        "description": "Table-wide sort cycle (antd sortDirections, in this library's asc/desc spelling). The cycle runs through the listed directions and then clears, so ['desc','asc'] sorts descending first — the right default for a date or amount column. A column's own sortDirections wins.",
        "name": "sortDirections",
        "type": "('asc' | 'desc')[]"
      },
      {
        "description": "Column filters changed, keyed by column — this library's split of the `filters` argument antd passes to the table-level onChange. Pair it with a column's filteredValue to drive filtering from a server query; omit both and the column filters client-side through its onFilter.",
        "name": "onFilterChange",
        "type": "(filters: Record<string, (string | number | boolean)[]>) => void"
      },
      {
        "description": "Extra classes applied to the root wrapper div (ui-data-table-root).",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Compound sub-parts: DataTable.Toolbar, DataTable.Search (global filter), DataTable.ViewOptions (column show/hide), DataTable.SelectAll, DataTable.BulkActions (ReactNode children OR a (count)=>node render-prop), DataTable.DensityToggle, DataTable.Pagination (cursor first/next when given cursor+hasMore+onChange, else numbered page-size form), DataTable.RowActions (kebab trigger), DataTable.Content. If no DataTable.Content is present in children, one is auto-rendered.",
        "name": "children",
        "type": "ReactNode"
      }
    ],
    "related": [
      "Table — raw primitive (TableHeader/TableBody/TableRow/TableCell). Use DataTable instead; only reach for Table directly when you need a non-standard layout that DataTable cannot express.",
      "SkeletonTable — standalone skeleton placeholder rendered before any DataTable mounts (e.g. in a Suspense fallback or deferred-prop skeleton slot). DataTable.loading covers in-table loading; SkeletonTable covers pre-mount skeletons.",
      "EmptyState — standalone empty state for non-table lists. DataTable already embeds EmptyState in its body; only use bare EmptyState for card content, non-tabular lists, or zero-state pages outside a DataTable.",
      "LineChart / BarChart / AreaChart / PieChart (@godxjp/ui/charts) — when the SHAPE or trend of aggregated data matters more than exact per-row figures, visualize it with a chart instead of (or alongside) the table; keep DataTable when users need to read, sort, or act on individual rows.",
      "DataState / InfiniteQueryState — TanStack Query lifecycle widgets from @godxjp/ui/query. Prefer these over DataTable when your list is driven by useQuery/useInfiniteQuery and you want automatic skeleton/empty/error handling at the query level rather than at the table level."
    ],
    "rules": [
      24,
      31,
      35,
      37
    ],
    "storyPath": "data-display/data-table/index.tsx",
    "tagline": "The one TanStack-powered compound admin list — sticky header, sorting, global search, column visibility ('set view'), bulk selection, BOTH cursor and numbered pagination, density, and built-in empty/loading states. Keep the SIMPLE `data` + lean `columns` (ColumnDef) API for the common case; opt into the full grid chrome via the compound parts. Internally driven by @tanstack/react-table (a real dependency). Lives on @godxjp/ui/data-display only (it is NOT on the runtime-neutral root/admin barrel because it pulls TanStack). Keyboard focus uses the shared inset ring in clipping surfaces; scrollports reveal partially clipped keyboard targets without changing their layout.",
    "usage": [
      "DO use `striped` on dense list tables (many columns, a row the eye must follow across the width). To stripe EVERY Table and DataTable in a service, set it ONCE in the theme — `:root { --table-row-striped-alpha: 100%; }` — instead of passing `striped` at each call site; `striped={false}` then opts one table out. Retint with `--table-row-striped-background`, never with a `rowClassName` utility or `:nth-child` page CSS (those count DOM rows, so an expanded detail row shifts every stripe after it).",
      "DO pass loading={isFetching} during data fetches — it renders a loading row in the table body and suppresses the empty state. Never show a spinner outside DataTable while the table is visible.",
      "DO NOT add a data.length===0 conditional around DataTable. When data is empty and loading is false, the built-in EmptyState renders automatically. Pass empty={<EmptyState title='...'/>} only when you need a custom message.",
      "SIX STATES, ZERO HAND-ROLLING: loading (`loading`), empty (automatic / `empty`), error (`error` + optional `onRetry`), denied (`denied`), pagination (`DataTable.Pagination`), row actions (`DataTable.RowActions`). Wire them straight off the query — `<DataTable loading={isPending} error={isError} denied={status === 403} onRetry={refetch} …/>` — and never branch the page around the table to render your own alert/empty/forbidden block. Precedence is loading > denied > error > empty > rows, so exactly one state ever shows.",
      "DO provide getRowId when selectable is true or when rows do not have a string/number 'id' field — the default falls back to row.id and silently returns '' for missing IDs, which breaks selection.",
      "DO use DataTable.Toolbar as the immediate child that wraps search/filter controls on the left and DataTable.DensityToggle/action buttons on the right. DataTable.BulkActions inside the toolbar auto-hides when selection count is 0; it accepts either plain ReactNode children (built-in 'N selected' status bar) or a (count)=>node render-prop (you own the whole bar).",
      "DO reach for the grid chrome (DataTable.Search, DataTable.ViewOptions, DataTable.Pagination pageSizeOptions) when you need global search, a column 'set view' picker, or numbered pagination — these are the merged former-DataGrid features, now on the one DataTable. Drive them client-side by default; pass the matching state + manual* flag for a server query.",
      "DataTable.Pagination OWNS ITS OWN INSET. The footer is a self-contained slot: it declares `padding-block` + `padding-inline` from `--table-pagination-padding-{y,x}` (block default = `--space-stack-sm`, inline default = `--table-cell-space-x`, so the 'rows per page' label lands on the same optical axis as the first column's text). Before the fix it declared `padding-top` only, so inside the documented flush container (`<Card><CardContent flush><DataTable/>`) the label and page-size Select sat flush against the container edge and border. DON'T ship a local `.ui-data-table-pagination { padding: … }` override in an app — retune the two tokens in your theme instead.",
      "RESPONSIVE APPROVAL / ACTION QUEUE: reach for `preset=\"action-collection\"` when a dense five-column queue (requester · target · reason · requested date · row actions) must stay readable at 390px, and give every column a `priority` on its ColumnDef — `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved FIRST so it can never be pushed off-screen, and which grows to its content when a text action + `…` menu does not fit the icon-sized token — gh#1067). Leave the free-text column unmarked; it takes the remaining space. This is the SAME contract, the SAME `--table-action-collection-*` tokens and the SAME container query as `Table preset=\"action-collection\"` — there is no separate DataTable family. Never add a consumer width, a hidden column, `hiddenOnMobile` or a page-local breakpoint to make a table fit: retune the tokens instead.",
      "DON'T set `width` on a column that also has a `priority` — an explicit width utility wins the cascade over the priority measure and re-opens the horizontal scroll. Under the preset, `pin: 'end'` is also redundant: nothing scrolls sideways, so the actions column is already in frame.",
      "The DataTable surface's narrow-viewport width floor is `--table-surface-min-inline-size` (default 640px, released at the `sm` viewport step). It used to be a hard-coded `min-w-[640px] sm:min-w-0` utility pair on the surface — the literal that forced the horizontal scroll at 390. Retune (or zero) the token in your theme; `preset=\"action-collection\"` already opts out of it.",
      "DO use ColumnDef.render for custom cell content (Badge, Link, RowActions). For plain string/number fields render can be omitted — DataTable falls back to String(row[key]).",
      "DO give every visually-empty column an accessible header via `ariaLabel` — a row-actions column (`header: ''`, `pin: 'end'`) sets `ariaLabel: t('actions')`, so screen readers announce the column and axe reports no `empty-table-header`. DataTable dev-warns any column that renders a `<th>` with neither visible text nor an `ariaLabel`. The selection column added by `selectable` is already named by its SelectAll checkbox — no `ariaLabel` needed there.",
      "COLUMN SEMANTICS + KEYBOARD: a `sortable` header renders as a real <button> inside the <th> with `aria-sort` (ascending/descending/none) on the <th>; it is Tab-reachable and toggles asc → desc → cleared on Enter/Space/click. A selection column exposes a header 'select all' Checkbox (indeterminate when a subset is selected) and a per-row Checkbox, each keyboard-operable with Space. An action column is visually empty but carries an `ariaLabel`; its per-row controls (kebab menu / buttons) own their own accessible names and keyboard behavior. Row click (`onRowClick`) is suppressed when the user activates an interactive descendant.",
      "DO NOT nest DataTable.Content in a conditional — it is already guarded internally. If you need to override the table body slot, drop exactly one <DataTable.Content /> in children; DataTable auto-detects it by displayName and skips the default."
    ],
    "useCases": [
      "Admin list pages (invoices, customers, orders, accounts) where rows are clickable for detail navigation via onRowClick.",
      "Bulk-action workflows (e.g. mark invoices paid, export selected rows) — use selectable + DataTable.BulkActions to show contextual action buttons only when something is selected.",
      "Server-side sorted tables: pass sort + onSortChange and update the data prop after the API call; DataTable renders asc/desc/neutral icons on the header automatically.",
      "Cursor-paginated lists: add DataTable.Pagination with cursor + hasMore + onChange inside children to get First/Next navigation without offset arithmetic. For page-size + numbered prev/next instead, use DataTable.Pagination with pageSizeOptions (no cursor/onChange) driven by the internal TanStack pagination.",
      "Server-paged table (antd `Table pagination`): `pagination={{ total, current, pageSize, onChange }}` with `data` = the rows of the current page — the table renders its own footer with the real Pagination (total + page numbers, bottom-end, sized from density). Add `showTotal: true` or `(total, [from, to]) => …` for the total label; `position: ['topEnd']` to move it.",
      "Full grid screens (global search + column 'set view' + numbered pagination): compose DataTable.Search, DataTable.ViewOptions, DataTable.DensityToggle in the toolbar and DataTable.Pagination pageSizeOptions={[…]} — client-side by default, or server-side by passing globalFilter/pagination/sort state with the matching manual* flag.",
      "Responsive admin tables where columns should drop at specific viewport steps — set hideBelow on each ColumnDef (sm/md/lg/xl, same ladder as Flex hideBelow); hiddenOnMobile: true remains an alias for hideBelow:\"md\". ColumnDef.priority is ONLY for preset=\"action-collection\" width allocation, not for hiding. When every column must stay DISCOVERABLE at 390 (an approval/action queue), use preset=\"action-collection\" + priority rather than hideBelow.",
      "Access-approval / action queues at 390px (SCR-105): preset=\"action-collection\" + a priority on each ColumnDef keeps requester · target · reason · requested date · row actions inside the initial narrow frame with no page-local CSS, no consumer width, no hidden column and no horizontal scroll — see the DataTable 'Approval queue' example page.",
      "Loading skeletons during initial page load or filter change: set loading={true} alongside an empty data={[]} to show the loading row without flashing an empty state."
    ]
  },
  {
    "example": "import { Card, CardHeader, CardTitle, CardContent } from \"@godxjp/ui/data-display\";\n\n<Card accent=\"success\">\n  <CardHeader><CardTitle>注文サマリー</CardTitle></CardHeader>\n  <CardContent>総売上: ¥1,234,567</CardContent>\n</Card>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Card",
    "props": [
      {
        "description": "Semantic accent TONE. Where it is drawn is `accentPlacement`'s job — by default a leading-edge stripe on `border-inline-start` at the `--card-accent-rail-width` measure (6px).",
        "name": "accent",
        "type": "\"primary\" | \"success\" | \"warning\" | \"info\" | \"attention\" | \"destructive\""
      },
      {
        "defaultValue": "\"edge\"",
        "description": "Where `accent` is drawn. \"edge\" is the classic leading rail. \"perimeter\" is the FULL attention border — the whole edge in the accent tone, at the same optical weight as `variant=\"featured\"` but tone-owned, so a card can read as \"action required\" (accent=\"attention\") or \"failed\" (accent=\"destructive\") without borrowing the brand colour. Inert without `accent`. LIMIT (gh#750): the perimeter ring is painted OUTSIDE the border box, and a scroll container clips it — a horizontal ScrollArea is `overflow: auto hidden`, and `overflow-clip-margin` is honoured only by `clip`, not by `auto`. A perimeter card flush against a scroller edge loses 1px of ring on that side; inside a scroller use the default \"edge\" placement (its rail is inside the box) or a tone fill.",
        "name": "accentPlacement",
        "type": "\"edge\" | \"perimeter\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Surface fill AND edge. `outline` is Ant Design's `outlined` — no fill, hairline kept. `borderless` is Ant Design's `variant=\"borderless\"` (its deprecated `bordered={false}`) — no hairline, fill kept. The two are mirror images and neither substitutes for the other. `featured` is the BRAND perimeter; its colour is the `--card-featured-border-color` knob rather than a hard-coded `--primary`. For a perimeter in a semantic tone use `accent` + `accentPlacement=\"perimeter\"` instead.",
        "name": "variant",
        "type": "\"default\" | \"muted\" | \"outline\" | \"borderless\" | \"featured\""
      },
      {
        "description": "Ant Design `hoverable` — the card lifts to `--card-hover-shadow` on hover and takes a pointer cursor. PRESENTATION ONLY: it announces nothing and binds no handler, so pair it with a real control (a Link/Button inside, or the whole card rendered as one via `asChild`). Never with a bare onClick on the Card div — a keyboard or screen-reader user cannot reach that. Composes with `accent`/`accentPlacement=\"perimeter\"`: the hover raises the shadow TOKEN, so the attention ring survives the hover.",
        "name": "hoverable",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Borrow the child's element for the card BOX instead of rendering a div — the standard Slot passthrough (Button, AspectRatio, ListRow). This is the other half of `hoverable`: `<Card asChild hoverable><a href=\"…\">` IS \"the whole card rendered as one control\" — one tab stop, announced as one link, with the focus ring on the card box (measured in Chromium: `<div>`, `<a>` and `<button>` identical on box, border, radius, fill, padding inset, shadow and cursor at 1440 and 390). For a click-only card hand it your router's Link component as the child. NESTING CAVEAT: the card is now ONE control, so it may not contain another — an <a>/<Button>/menu trigger inside a card-as-link or card-as-button is invalid HTML, and `tabList` (a strip of button triggers) is for the same reason not drawn under `asChild` and warns in development. A card that needs interactive children is not one control: drop `asChild` and put the Link/Button inside it, which is the other branch `hoverable` names. Exactly one child — two throws React.Children.only, the same error `Button asChild` throws.",
        "name": "asChild",
        "type": "boolean"
      },
      {
        "description": "Internal padding density (base 16 / tight 12 / cozy 20). This IS Ant Design's `size` axis; there is deliberately no `size` prop (removed 2026-08-24) — see docs/DESIGN-AUTHORITY.md, a capability this library already has keeps its own name.",
        "name": "density",
        "type": "\"tight\" | \"cozy\""
      },
      {
        "description": "Ant Design `tabList` — the tab strip that lives IN THE CARD'S HEAD: under the title, on the same surface, inside the same border, so the card and its tabs read as ONE object. The entry keeps antd's own field names (`key`/`tab`/`disabled`), NOT the Tabs component's `value`/`label`/`content` — a card tab carries only the trigger, because the panel is the card body. The Card's children become the selected tab's body; wrap them in <CardContent> (or <CardContent flush> for an edge-to-edge DataTable, which still reaches the card edge inside a tab).",
        "name": "tabList",
        "type": "{ key: string; tab: ReactNode; disabled?: boolean }[]"
      },
      {
        "description": "Ant Design `activeTabKey` — the CONTROLLED selection. With it set the card never moves itself; pair it with `onTabChange` and swap the children yourself, exactly as in antd.",
        "name": "activeTabKey",
        "type": "string"
      },
      {
        "description": "Ant Design `defaultActiveTabKey` — the uncontrolled initial selection. Without it the first selectable entry of `tabList` opens (antd's own fallback); a disabled tab is never the open one.",
        "name": "defaultActiveTabKey",
        "type": "string"
      },
      {
        "description": "Ant Design `onTabChange` — fires with the newly selected `key`, however the selection moved (pointer or keyboard).",
        "name": "onTabChange",
        "type": "(key: string) => void"
      },
      {
        "description": "Ant Design `tabBarExtraContent`, RENAMED to `extra` and made logical — the same precedent `Tabs.extra` already set in this package, and antd's `left`/`right` keys are `start`/`end` here so an RTL locale gets the slot on the correct edge. It rides the TAB BAR beside the strip, so it is inert without `tabList`; a header-level action is <CardAction> inside <CardHeader>.",
        "name": "extra",
        "type": "ReactNode | { start?: ReactNode; end?: ReactNode }"
      },
      {
        "description": "Ant Design `tabProps` — passed straight to the Tabs that draws the strip, so `variant`, `size`, `centered`, `overflow`, `indicator` and the rest are reachable. The fields the CARD owns are omitted rather than silently overwritten: `items` comes from `tabList`, `value`/`defaultValue`/`onValueChange` from `activeTabKey`/`defaultActiveTabKey`/`onTabChange`, and `extra` is the Card's own slot. antd drops the same fields — it writes its own over `tabProps` — so this is that behaviour made visible in the type.",
        "name": "tabProps",
        "type": "Omit<TabsProps, 'items' | 'value' | 'defaultValue' | 'onValueChange' | 'extra' | 'children'>"
      }
    ],
    "related": [
      "StatCard — use instead of a plain Card when rendering a KPI/metric tile (label + value + optional delta/hint). StatCard is a Card internally; do not re-wrap it in another Card.",
      "CardContent — mandatory inner wrapper for all body content inside Card. Provides the correct padding and supports flush/tight/solo variants. The only correct way to put padded content inside Card.",
      "Descriptions — use inside <CardContent> when body content is a label-value metadata list (e.g. entity details, invoice fields); do not hand-roll a dl/dt/dd grid.",
      "DataState / InfiniteQueryState — use instead of Card when the content is a TanStack Query-driven list that needs automatic skeleton, empty, and error states; Card does not manage loading lifecycle."
    ],
    "rules": [],
    "storyPath": "data-display/card/index.tsx",
    "subParts": [
      "CardAction",
      "CardCover",
      "CardDescription",
      "CardFooter",
      "CardHeader",
      "CardTitle"
    ],
    "tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab TOOL strip inside the card (composer or editor tools) use <CardBar extra={…}> — a positionable bar that auto-draws its separator from its position (top→bottom border, bottom→top border, middle→both) and pins `extra` content to the inline-end edge; place it as first/last child of the Card. List FILTERS (search, filter selects, filter chips) are NOT a CardBar: CardBar's main slot scrolls sideways, and a filter strip never does — they go in a FilterBar ABOVE the card, which wraps (cardinal rule #51, pattern `list-filters`).",
    "usage": [
      "DO always wrap body content in <CardContent> — the bare <Card> div has zero inner padding; content renders flush against card edges without it. Never add className=\"p-4\" directly on <Card> as a substitute.",
      "DO put titles/descriptions in <CardHeader>/<CardTitle>/<CardDescription>. Use <CardHeader banded> for a visually separated muted-background header band (mirrors <CardFooter separated>). Pair with <CardAction> inside a flex-row CardHeader for header-level action buttons.",
      "DO set <CardTitle level={n}> to keep a valid document outline (h1 → h2 → h3, no skipped levels): CardTitle renders <h3> by default, so a section card directly under a page <h1> needs level={2}. Pick the level by OUTLINE position, NEVER for visual size — the title size is fixed by tokens and does not change with level. When the card title is a styled label rather than a section heading, use <CardTitle as=\"p\"> so it is not announced as a heading.",
      "DO use <CardContent flush> for edge-to-edge children such as DataTable, Table, or a Tabs list — this removes horizontal padding. Combine with <CardContent tight> when there is no visual gap needed after the header, and <CardContent solo> when there is no CardHeader above (top padding matches the card shell).",
      "DO use <CardFooter separated> to render a top-bordered action band (Save/Cancel buttons, table summary row). Use <CardFooter flush> for a full-bleed footer bar.",
      "DO use <CardFooter actions> for Ant Design's `actions` row — N EQUAL-WIDTH cells split by vertical hairlines (複製 / 共有 / 削除 under a profile or entity card). It is a different band from `separated`, which packs children at the inline end at their natural widths: that is the right shape for a Save/Cancel pair and the wrong one for a divided strip. `actions` is self-sufficient — it draws its own top rule and full-bleed edges, so it needs neither `separated` nor `flush` beside it. The dividers are logical (border-inline-start), so the strip mirrors under RTL.",
      "DO use <CardCover> as the first child for full-bleed cover media — the header below it uses card-section top spacing, not the card shell.",
      "DO make the WHOLE card the control with `asChild` when the whole card is the click target: `<Card asChild hoverable><a href={href}><CardHeader><CardTitle level={2}>…</CardTitle></CardHeader><CardContent>…</CardContent></a></Card>` (or your router's Link as the child when there is no href, e.g. a nav.push handler). One tab stop, announced as one link, focus ring on the card box. DON'T wrap the card in a raw <button> (that is a `no-raw-button` error) and DON'T put a bare onClick on the Card div (unreachable by keyboard and screen reader — `hoverable`'s own docblock forbids it). NESTING CAVEAT: a card-as-link/button may not CONTAIN another control — an <a>, a <Button>, a DropdownMenu trigger or a `tabList` strip inside it is invalid HTML (`tabList` is dropped with a development warning). When the card needs interactive children it is not one control: drop `asChild` and use the other branch — `hoverable` plus a real Link/Button inside <CardHeader>/<CardAction>/<CardFooter>.",
      "ANT DESIGN PROPS THIS FAMILY ANSWERS BY COMPOSITION, not by a prop of the same name — do not ask for these to be added: `title` is <CardHeader> + <CardTitle> (and CardTitle.level emits a real heading, which antd does not); a header-level action is <CardAction> inside <CardHeader>. `cover` is <CardCover>. `actions` is <CardFooter actions>. `loading` is a Skeleton in the body — antd renders a Skeleton with paragraph rows and no title, so the equivalent is <CardContent solo><SkeletonRows rows={4} /></CardContent>. `type=\"inner\"` is variant=\"muted\" plus <CardHeader banded>. `size` is `density`. `Card.Grid` is <ResponsiveGrid>; `Card.Meta` is <ListRow leading title description trailing>.",
      "DO use `tabList` for a tab strip that belongs to the CARD — antd's card-head tabs, ported name for name (gh#570): `tabList={[{ key, tab, disabled? }]}` plus `activeTabKey`/`defaultActiveTabKey`/`onTabChange`, with `extra` for antd's `tabBarExtraContent` and `tabProps` for everything else on the Tabs underneath. The strip renders INSIDE the card head, under the title, on the same surface and inside the same border, and the Card's children become the selected tab's body (wrap them in <CardContent>, or <CardContent flush> for an edge-to-edge DataTable). DON'T hand-roll it as a <Tabs> parked on the page above the card (the strip floats off the card and the two read as two objects) or as a <Card> repeated inside each tab (the shell is copied per view). A <Tabs> INSIDE <CardContent tight flush> is still correct for a strip that belongs to the BODY rather than to the card head.",
      "DO reach for `accentPlacement=\"perimeter\"` when the whole card needs attention, not one edge: `<Card accent=\"attention\" accentPlacement=\"perimeter\">` is the semantic-tone equivalent of `variant=\"featured\"` (which is brand-toned by definition). Never hand-roll it with `className=\"border-2 border-[--attention]\"` or a page-local `.card--attention` rule — the placement owns the border weight, the outer ring AND the slot-padding compensation, so text stays on the same column as an unaccented sibling.",
      "DON'T hand-roll a stat/KPI tile with <Card> + raw divs — use <StatCard> (label, value, hint, delta, layout, inverse props) which is already a Card internally with correct token-driven layout.",
      "SPACING IS BORDER-AWARE & token-driven (theme via src/tokens/components/card.css, never hard-code padding on slots): `--card-space-inset` is the shared horizontal column every slot (header/content/footer) aligns to. A DIVIDED section — a `banded` header or a `separated` footer, i.e. one carrying a divider border — pads SYMMETRICALLY top+bottom from `--card-space-divided-y` (a band reads as its own region). A PLAIN header flows into the body instead: top `--card-space-shell-y`, no bottom, and the body supplies the gap via `--card-space-body-y`. THE TWO AXES ARE INDEPENDENT: `--card-space-inset` is inline-only, while `--card-space-shell-y` owns the BLOCK shell edges (plain-header top, `solo` body top, terminal slot bottom) and defaults to the inset — so a shell/theme can make a card SHORTER without narrowing its column by overriding `--card-space-shell-y` alone (this is how the auth CenteredShell's `--auth-shell-card-padding-block-compact` reaches CardContent). Never bridge it with a consumer selector on the card-content slot. Special case: `<CardContent flush>` zeroes BOTH of its block edges — for ANY full-bleed body, not only one containing a <Table>` gate left a flush file LIST floating 18px off its header while the flush table beside it sat at 0) — so the plain header above it supplies the gap from its own `--card-space-body-y` bottom padding instead. `tight` and `solo` still own that axis themselves. `--card-space-gap` is the in-slot stack gap (title↕description). Tune the band rhythm once at `--card-space-divided-y`; tune the accent stripe width at `--card-accent-rail-width` (default 6px)."
    ],
    "useCases": [
      "Dashboard KPI summary row: wrap each metric in <StatCard> (or a plain <Card density=\"tight\"> with <CardContent>) to render a uniform grid of labeled value tiles with optional trend deltas.",
      "Invoice or order detail panel: <Card accent=\"primary\"> with <CardHeader banded><CardTitle>, <CardContent> body rows (use <Descriptions> inside), and <CardFooter separated> holding approve/reject buttons.",
      "Section container on a settings or form page: a single <Card> wrapping a <CardHeader><CardTitle> plus <CardContent> containing <FormField> groups, with <CardFooter separated> for Save/Cancel.",
      "Data table with toolbar: <Card> + <CardHeader> (title + filter controls in <CardAction>) + <CardContent flush> containing <DataTable> — <CardContent flush> removes horizontal padding so the table header spans full width.",
      "Detail screen with views: <Card tabList={[{key,tab}]} activeTabKey onTabChange extra={<Button/>}> with <CardHeader><CardTitle> above the strip and <CardContent flush><DataTable/></CardContent> as the body — one card, tabs in its head, and a table that reaches the card edge inside the tab (docs/data-display/card/examples/tab-list.tsx).",
      "Featured announcement or alert card: <Card variant=\"featured\"> with an accent stripe (<accent=\"warning\">) to visually elevate a card above sibling cards on the page.",
      "Media/cover card (e.g. entity profile): <CardCover> first (full-bleed image), then <CardHeader> + <CardContent> below it for structured metadata."
    ]
  },
  {
    "example": "import { Card, CardContent, DataTable } from \"@godxjp/ui/data-display\";\n\n<Card>\n  <CardContent flush>\n    <DataTable data={rows} columns={columns} />\n  </CardContent>\n</Card>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "CardContent",
    "props": [
      {
        "description": "Remove horizontal padding for edge-to-edge tables / tabs lists.",
        "name": "flush",
        "type": "boolean"
      },
      {
        "description": "No top gap after header — pair with flush toolbars/tabs.",
        "name": "tight",
        "type": "boolean"
      },
      {
        "description": "No header above: top padding matches the card shell.",
        "name": "solo",
        "type": "boolean"
      }
    ],
    "related": [
      "Card — the parent container; CardContent is always a direct child of Card. Card itself has zero internal padding; every visible body padding comes from CardContent (or CardHeader/CardFooter). Never put content directly inside Card.",
      "StatCard — a self-contained KPI tile that IS already a Card; do not wrap it in <Card><CardContent>. Use StatCard directly inside a ResponsiveGrid.",
      "ScrollArea — place ScrollArea inside CardContent (non-flush) when the card body needs to scroll; do not put ScrollArea outside CardContent or you lose the card's internal padding.",
      "SkeletonStat — the loading placeholder for a StatCard tile; swap in SkeletonStat while KPI data is loading. For general Card loading shapes, use SkeletonTable or Skeleton primitives."
    ],
    "rules": [
      37,
      38
    ],
    "tagline": "Card body. flush = edge-to-edge (for DataTable/tabs); tight = no top gap; solo = no header above. NEVER put a Toolbar inside flush (it loses padding).",
    "usage": [
      "DO: Always wrap body content in <CardContent> — a bare <Card> has no internal padding, so any child placed directly inside it renders flush against the card edges.",
      "DO: Use <CardContent flush> for DataTable, Table, or Tabs — the flush prop removes horizontal padding so the content spans edge-to-edge inside the card border. Never add manual p-0 on the Card itself instead.",
      "DO: Use <CardContent tight> when placing a flush toolbar or a Tabs list directly below a CardHeader — tight removes the top gap so the header and the body connect without an awkward spacing gap.",
      "DO: Use <CardContent solo> when the card has no CardHeader above it — solo gives the top padding that matches the card shell, ensuring visual balance.",
      "DON'T: Nest a Toolbar inside <CardContent flush> — flush strips horizontal padding and Toolbar will lose its own padding. Put Toolbar outside the flush CardContent or in a separate non-flush CardContent above it.",
      "DON'T: Wrap a StatCard inside <Card><CardContent> — StatCard already renders its own Card border; double-wrapping produces a double border. Render StatCard directly in a ResponsiveGrid."
    ],
    "useCases": [
      "Wrapping a form body (Input, Select, Textarea fields) inside a Card that has a CardHeader title — ensures the form fields have correct internal padding.",
      "Hosting a DataTable inside a Card edge-to-edge: <CardContent flush><DataTable .../></CardContent> — the table occupies the full card width with the card's border acting as the table container.",
      "Dashboard detail panels where the card has no title — <CardContent solo> gives top padding equivalent to the card shell so the content doesn't sit too close to the top border.",
      "Placing a Descriptions or Timeline inside a card to display invoice/accounting details — <CardContent> provides the standard 16px (or density-adjusted) padding without needing manual className.",
      "Pairing with <CardHeader banded> and <CardFooter separated> in a multi-section layout such as a payment summary card — each section slot (header, content, footer) carries its own semantic spacing tokens.",
      "Putting a ScrollArea inside <CardContent> (not flush) to create a scrollable card body with consistent padding, e.g. a chat or log viewer panel."
    ]
  },
  {
    "example": "import { StatCard } from \"@godxjp/ui/data-display\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\n\n// ✅ StatCard sits directly in the grid — it draws its own card + border.\n<ResponsiveGrid columns={3}>\n  <StatCard label=\"総会員数\" value=\"12,450\" hint=\"先月比 +3%\" />\n  <StatCard label=\"月次売上\" value=\"¥8,200,000\" delta=\"+12%\" />\n  <StatCard label=\"利用率\" value=\"68.4%\" />\n</ResponsiveGrid>\n\n// ❌ Double border — do NOT wrap StatCard in a Card:\n// <Card><CardContent><StatCard label=\"x\" value=\"1\" /></CardContent></Card>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "StatCard",
    "props": [
      {
        "description": "Metric name.",
        "name": "label",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Metric value (string/number/ReactNode).",
        "name": "value",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Secondary context below the value.",
        "name": "hint",
        "type": "ReactNode"
      },
      {
        "description": "Optional leading icon rendered as a tinted medallion above the metric. Decorative (aria-hidden) — the label carries the meaning. Tint via the --stat-card-icon-{background,foreground} tokens (default a soft brand-primary wash).",
        "name": "icon",
        "type": "LucideIcon"
      },
      {
        "description": "Compact trend text beside the value. Sign-aware tone (+ green / - red).",
        "name": "delta",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"stacked\"",
        "description": "stacked = label over value; inline = label left / value right.",
        "name": "layout",
        "type": "\"stacked\" | \"inline\""
      },
      {
        "description": "Align the metric group.",
        "name": "align",
        "type": "\"start\" | \"end\""
      },
      {
        "defaultValue": "false",
        "description": "Flip delta tone semantics for metrics where lower is better (cost, error rate): '-' renders green, '+' renders red.",
        "name": "inverse",
        "type": "boolean"
      },
      {
        "description": "Semantic 3px leading-edge rail (forwarded to the underlying Card) — flags a KPI that needs attention (e.g. attention = backlog with a deadline, destructive = overdue).",
        "name": "accent",
        "type": "\"primary\" | \"success\" | \"warning\" | \"info\" | \"attention\" | \"destructive\""
      }
    ],
    "related": [
      "ResponsiveGrid — required layout wrapper for StatCard grids; controls column count and responsive breakpoints. Always pair them together.",
      "SkeletonStat — exact loading placeholder shaped like a StatCard tile; swap in while KPI data is fetching, then replace with the real StatCard.",
      "Descriptions — use instead when displaying multiple label/value metadata pairs on a detail page (not headline KPIs); Descriptions is not card-bordered and does not show delta/hint slots.",
      "Card + CardContent — use when you need a general-purpose content container with a header, footer, or arbitrary body; do NOT wrap StatCard inside these.",
      "LineChart / BarChart / PieChart (@godxjp/ui/charts) — when the trend or composition BEHIND a KPI matters, pair the StatCard headline number with a chart in the same dashboard grid: StatCard states the figure, the chart shows its shape over time or its breakdown."
    ],
    "rules": [],
    "storyPath": "data-display/stat-card.tsx",
    "tagline": "KPI tile. ⚠️ StatCard IS ALREADY a bordered Card — render it DIRECTLY in ResponsiveGrid. NEVER wrap it in <Card>/<CardContent> (that double-borders it → looks too thick). Use `accent` for a semantic leading-edge rail on a KPI needing attention.",
    "usage": [
      "DO place StatCard directly as a child of ResponsiveGrid — it renders its own bordered Card shell internally, so no wrapping <Card> or <CardContent> is needed or allowed. Wrapping creates a double border.",
      "DO pass `delta` as a sign-prefixed string (e.g. '+12%' or '-3%') to get automatic color tone: '+' renders text-success, '-' renders text-destructive. For metrics where a negative delta is good (e.g. cost reduction, error rate), pass `inverse` so the tone is flipped correctly.",
      "DO use `hint` for secondary context (e.g. '先月比 +3%', 'last 30 days'). In the default `stacked` layout hint renders below the value; in `inline` layout it renders beside the label.",
      "DO use `accent` (not a className border) for the semantic leading-edge rail — `attention` for a non-destructive backlog needing action, `destructive` only for overdue/exceeded states. Most tiles in a grid stay rail-less; an all-accented grid says nothing.",
      "DO NOT hand-roll a KPI tile using a plain <Card><CardContent>. StatCard is the correct primitive and token-aligns the label/value/hint/delta slots automatically.",
      "WHILE data is loading, replace each StatCard with a <SkeletonStat /> at the same grid position — never render an empty value string or a spinner inside StatCard itself."
    ],
    "useCases": [
      "Dashboard KPI row: monthly revenue, invoice count, overdue balance, and collection rate displayed side-by-side in a ResponsiveGrid with delta trend vs previous period.",
      "Accounting summary header: total debits, total credits, and net balance for a journal entry list page, each with a hint showing the date range in scope.",
      "Coupon/membership admin overview: active members, live coupons, monthly redemptions, and total discount amount — the canonical example in the catalog.",
      "Inline variant for a narrow sidebar or detail panel where space is constrained: label on the left, large value on the right (layout='inline'), e.g. contract value next to a deal record.",
      "Cost or error-rate metrics where a falling number is positive: pass `inverse` so a '-15%' delta shows green, preventing misleading red-for-good UI.",
      "Loading state for any KPI grid: render the same ResponsiveGrid columns filled with <SkeletonStat /> components while the query is in-flight, then replace with StatCard tiles once data resolves."
    ]
  },
  {
    "example": "import { Clock } from \"lucide-react\";\nimport {\n  ServiceCatalogCta,\n  ServiceLauncherCard,\n  ServiceLauncherCardSkeleton,\n} from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\n\n// ResponsiveGrid owns the canonical 3 → 2 → 1 ladder; the page writes no tracks.\n<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>\n  {loading\n    ? services.map((s) => <ServiceLauncherCardSkeleton key={s.id} label={t(\"loadingService\")} />)\n    : services.map((s) => (\n        <ServiceLauncherCard\n          key={s.id}\n          icon={Clock}\n          // The uploaded mark when there is one; the tile falls back to `icon` when there is not,\n          // and again if the file behind the URL has gone away.\n          logo={s.logoUrl ?? undefined}\n          title={s.name}\n          statusLabel={s.accessLabel}\n          statusTone={s.accessTone}\n          description={s.description}\n          metadata={s.hostnameAndPlan}\n          disabledReason={s.blockedReason}\n          action={\n            <Button asChild={s.canLaunch} disabled={!s.canLaunch}>\n              {s.canLaunch ? <a href={s.launchUrl}>{t(\"launch\")}</a> : t(\"launch\")}\n            </Button>\n          }\n        />\n      ))}\n  <ServiceCatalogCta\n    title={t(\"addFromCatalog\")}\n    action={<Button variant=\"outline\">{t(\"viewCatalog\")}</Button>}\n  />\n</ResponsiveGrid>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "ServiceLauncherCard",
    "props": [
      {
        "description": "Decorative glyph for the KIND of service, rendered in the canonical semantic icon surface. Stays required because it is also what `logo` falls back to when an upload is missing or broken.",
        "name": "icon",
        "required": true,
        "type": "LucideIcon"
      },
      {
        "description": "URL of the service's OWN uploaded mark (the PNG/WebP an administrator uploaded). When it loads it replaces `icon` in the medallion, in the glyph's exact box (--card-service-launcher-icon-glyph-size, object-fit: contain). It is a fallback chain, not a switch: the tile renders `icon` while the URL is in flight and KEEPS rendering `icon` if the URL 404s or is empty — a broken logo never reaches the DOM as an <img>. Decorative (alt=\"\", medallion aria-hidden), so there is no logoAlt prop: the service name is already beside it.",
        "name": "logo",
        "type": "string"
      },
      {
        "description": "Real downstream service display name.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "defaultValue": "2",
        "description": "Semantic heading level; visual styling remains token-owned.",
        "name": "titleLevel",
        "type": "1 | 2 | 3 | 4"
      },
      {
        "description": "Consumer-provided access/readiness label. The component never infers status.",
        "name": "statusLabel",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"neutral\"",
        "description": "Semantic tone corresponding to the real statusLabel.",
        "name": "statusTone",
        "type": "\"success\" | \"warning\" | \"destructive\" | \"info\" | \"neutral\" | \"muted\""
      },
      {
        "description": "Localized service summary.",
        "name": "description",
        "type": "ReactNode"
      },
      {
        "description": "Compact mono metadata such as real hostname and subscribed plan.",
        "name": "metadata",
        "type": "ReactNode"
      },
      {
        "description": "Real launch/detail action, normally a Button or Button asChild link.",
        "name": "action",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Localized prose reason accompanying a disabled/unavailable action. Rendered ABOVE the action (so assistive tech meets the explanation before the disabled control) and marks the tile data-unavailable, which mutes the medallion via --card-service-launcher-unavailable-icon-*. It never disables the action itself — that stays the consumer's Button prop.",
        "name": "disabledReason",
        "type": "ReactNode"
      }
    ],
    "related": [
      "ServiceLauncherCardSkeleton — shape-matched initial loading placeholder (required `label`, aria-busy, no live region).",
      "ServiceCatalogCta — dashed companion tile for an existing catalog/add route.",
      "ResponsiveGrid — OWNS the 3→2→1 layout around launcher tiles; the launcher never ships grid tracks of its own.",
      "Card — general-purpose surface; use ServiceLauncherCard instead for this established composite."
    ],
    "rules": [
      40
    ],
    "storyPath": "data-display/service-launcher-card.tsx",
    "subParts": [
      "ServiceLauncherCardSkeleton"
    ],
    "tagline": "Token-owned downstream-service launcher tile with an uploaded service logo (falling back to a semantic icon), status, metadata, action, disabled reason, matching skeleton, and companion catalog CTA.",
    "usage": [
      "DO provide status, hostname, plan, access state and action from the product's real API contract. ServiceLauncherCard deliberately performs no entitlement or URL inference — it has no href/entitlement/available prop at all.",
      "DO own the layout with ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }} — the canonical 3→2→1 launcher grid. ResponsiveGrid queries its OWN container (40/48/64rem), so never hand-write grid-template-columns or a media query in the page. The shorthand columns={3} also works but widens to 2 columns earlier (40rem).",
      "DO render ServiceLauncherCard directly as a grid child; it already owns its Card shell, its 36px medallion (--control-height-lg tier) and the canonical internal rhythm.",
      "DO replace it with ServiceLauncherCardSkeleton while loading (it carries a required `label` and aria-busy, and deliberately opens no live region). Use ServiceCatalogCta as the peer tile only when a real catalog/add route exists.",
      "DO keep `metadata` to machine identifiers (hostname · plan) — it is the only mono line. Sentences belong in `description` / `disabledReason`.",
      "DO pass BOTH `logo` and `icon` when a service may have an uploaded mark: `logo={s.logoUrl ?? undefined}` with `icon` as the kind glyph. `icon` is not optional and must not be treated as redundant — it is the tile a freshly created service, and a service whose logo file has been deleted, actually shows.",
      "DON'T wrap an <img> in a component to squeeze it through `icon`, and DON'T style the medallion to fit a picture. `logo` sizes the image to the glyph's own token, so a grid mixing logo tiles and glyph tiles keeps one optical weight.",
      "DON'T recreate launcher geometry with page-local CSS, utility padding, grid tracks, or a hand-built Card hierarchy. Retune it with the --card-service-launcher-* tokens instead.",
      "DON'T show LIVE, a hostname, subscribed plan, or launch action merely because a service is active in the global catalog."
    ],
    "useCases": [
      "Organization console launcher showing subscribed downstream applications with a real SSO launch action.",
      "Service picker where unavailable apps remain visible with a disabled action and permission/subscription reason.",
      "Responsive 3→2→1 launcher grid with a final ServiceCatalogCta tile linked to an existing catalog route."
    ]
  },
  {
    "example": "import { ServiceCatalogCta } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\n\n<ServiceCatalogCta\n  title={t(\"addFromCatalog\")}\n  action={\n    <Button asChild variant=\"outline\">\n      <a href={catalogUrl}>{t(\"viewCatalog\")}</a>\n    </Button>\n  }\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "ServiceCatalogCta",
    "props": [
      {
        "defaultValue": "Plus",
        "description": "Decorative glyph above the title, rendered aria-hidden. Override only when the route is not an add/browse action.",
        "name": "icon",
        "type": "LucideIcon"
      },
      {
        "description": "Localized invitation, e.g. the label of the catalog route this tile opens.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "The real navigation control — normally a Button, or Button asChild wrapping the catalog link. The tile itself is not clickable.",
        "name": "action",
        "required": true,
        "type": "ReactNode"
      }
    ],
    "related": [
      "ServiceLauncherCard — the real service tile this one accompanies; it carries status, metadata and the launch action.",
      "ServiceLauncherCardSkeleton — the loading placeholder for the launcher tiles, documented under ServiceLauncherCard.",
      "EmptyState — the zero-state primitive to reach for when the grid has NO services at all; this CTA is a companion to existing tiles, not an empty state."
    ],
    "rules": [
      40
    ],
    "tagline": "Dashed companion tile that closes a launcher grid with the real catalog/add route — same Card geometry as ServiceLauncherCard, no status or metadata.",
    "usage": [
      "DO render it as the LAST child of the same ResponsiveGrid that holds the ServiceLauncherCards, so it inherits the 3→2→1 ladder and lines up with the tiles it follows.",
      "DO give it a route that already exists. It is an invitation to a real catalog/add screen; a tile whose action goes nowhere reads as a broken service.",
      "DON'T use it as an empty state for a grid that has no services — an empty launcher grid needs Empty, which explains the absence, not a CTA that implies there is something to add.",
      "DON'T rebuild it as a Card with a dashed border and utility padding: the dashed surface, the medallion and the internal rhythm are token-owned (--card-service-launcher-*), same as the launcher tile."
    ],
    "useCases": [
      "Closing tile of an organization console launcher grid, linking to the service catalog.",
      "Add-a-service affordance beside the subscribed applications an admin already has."
    ]
  },
  {
    "example": "import { Badge } from \"@godxjp/ui/data-display\";\n\n<Badge variant=\"secondary\">A/B</Badge>\n<Badge status=\"active\">公開中</Badge>\n<Badge status=\"プレミアム\" tone=\"success\" icon={null}>プレミアム</Badge>\n<Badge shape=\"pill\" color=\"#4488c5\">処理中</Badge>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Badge",
    "props": [
      {
        "defaultValue": "\"div\"",
        "description": "Render element. Swaps the TAG only — the chip keeps its own inline-flex box, icon and label. Pass \"span\" when the chip sits in a PHRASING context where a <div> is invalid HTML: inside a TabsTrigger, a PopoverTrigger or a Button (each renders a <button>, whose content model is phrasing content only), inside a <label>, or inside a <p>.",
        "name": "as",
        "type": "\"div\" | \"span\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "STRUCTURAL emphasis only (fill/border style) — NOT colour. Use `tone` for semantic colour. `dashed` = dashed border.",
        "name": "variant",
        "type": "\"default\" | \"secondary\" | \"outline\" | \"dashed\""
      },
      {
        "description": "SEMANTIC colour intent (BadgeTone = ToneProp + `primary`). This is the colour knob — success/warning/destructive/info/etc. `primary` is a SOFT brand pill (tinted brand fill + brand text) for the dashboard role-pill pattern; for a SOLID brand fill use `variant=\"default\"`. Keep variant for structure, tone for meaning.",
        "name": "tone",
        "type": "\"default\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"muted\" | \"neutral\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Corner radius from the tokens — `default` (badge radius), `pill` (fully rounded), `sharp` (square). Use the prop instead of a `rounded-*` className.",
        "name": "shape",
        "type": "\"default\" | \"pill\" | \"sharp\""
      },
      {
        "defaultValue": "false",
        "description": "Lining, fixed-width figures — for a chip whose content is a COUNT sitting in a column with other counts (a queue length per row, an unread tally per tab). Proportional figures make a `1` narrower than a `0`, so the digits do not line up and the chips jitter. The same axis Text, TableCell and StatCard already carry. Off by default: tabular figures are wider, and a chip carrying WORDS should not pay for them. Prefer it over `<Badge><Text size=\"xs\" tabular>` — that composition works, but it asks the call site to know the chip's own type step.",
        "name": "tabular",
        "type": "boolean"
      },
      {
        "description": "The entity's OWN colour as a CSS colour — DATA, not a semantic tone: a status colour, an issue type, a tag, whatever a person picked in a settings screen. A third axis beside `variant` (structure) and `tone` (meaning), and it wins over both. The chip is WASHED rather than filled (`--badge-tint-fill` into `--badge-tint-surface`, label from `--badge-tint-foreground`) because no foreground clears WCAG AA against every colour a picker can produce — near-black and white measure equal at luminance 0.2029, both 4.15:1. Washed, the ratio is a function of the tokens instead: 8.52:1 worst case across the sRGB cube, both themes.",
        "name": "color",
        "type": "string"
      },
      {
        "description": "Lifecycle key. Known keys auto-map to tone + icon + i18n label; unknown keys fall back to neutral.",
        "name": "status",
        "type": "string"
      },
      {
        "description": "Leading icon override. Pass null to suppress the auto status icon.",
        "name": "icon",
        "type": "React.ComponentType<{ className?: string }> | null"
      },
      {
        "description": "Badge label. When omitted with status, Badge renders the translated lifecycle label or raw status. LONG LABEL CONTRACT (gh#1101): the chip is `max-inline-size: 100%` of its container and never wraps; the label ellipsizes and, when it was cut, opens the full text in the same tooltip as `Text ellipsis={{ tooltip: true }}` — on pointer hover of the label and on keyboard focus of the nearest focusable control around the chip (a row link, a treeitem, a cell button), only while it is actually cut; the chip never becomes a tab stop (gh#1105). A caller's own `title` on the Badge turns the tooltip off. So a Badge in a fixed-width DataTable cell or a narrow Card stays inside it with no extra props; `<Badge><Text ellipsis>` also works. Do not reach for `Text break=\"anywhere\"` to make a chip wrap.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "antd Tag `closable` + `onClose` — draws a × on the chip and calls this when the user activates it. Omit for a plain badge (no ×). Accessible name quotes the string label via `navigation.filterBar.removeFilter`.",
        "name": "onRemove",
        "type": "() => void"
      },
      {
        "description": "The × button's full accessible name, verbatim — antd 5.15+ `closable={{ 'aria-label' }}` (gh#706). Without it the name quotes the chip's label: a string `children`, or the label's RENDERED TEXT when `children` is a link or other node (so a row of saved-filter link chips never shares one bare \"Delete\"). Use it when the chip text alone doesn't say what removing does.",
        "name": "removeLabel",
        "type": "string"
      }
    ],
    "related": [
      "Button — use instead of Badge when the chip must be interactive (clickable, toggleable). Badge carries no button role or keyboard handler; a naked `onClick` on Badge is inaccessible."
    ],
    "rules": [
      35
    ],
    "storyPath": "data-display/badge.tsx",
    "subParts": [
      "StatusBadge"
    ],
    "tagline": "Plain or lifecycle badge. Use `variant` for static chips, or `status` to auto-map lifecycle keys to semantic tone + icon. Labels never wrap: a label wider than its container ellipsizes, and its full text opens in a tooltip on hover or on keyboard focus of the control around the chip (gh#1101, gh#1105).",
    "usage": [
      "DO pick the correct variant semantically: `success` (approved/paid), `warning` (pending/overdue), `destructive` (rejected/error), `secondary` (neutral category), `outline` (subtle label), `default` (primary accent). Never force a colour just for aesthetics — agents and screen readers read the variant as intent.",
      "DO use `status` for entity lifecycle statuses (active, draft, pending, cancelled, failed, scheduled, etc.) so the component resolves the correct tone, icon, and i18n label.",
      "DO pass `variant` explicitly for localized labels or categorical tiers, and pass `icon={null}` when a lifecycle glyph would be misleading.",
      "Badge renders as a `<div>` by default (HTMLAttributes<HTMLDivElement>) — pass `as=\"span\"` when it sits inside a <button>, <label> or <p>, where a <div> is invalid HTML. With `onRemove`, the chip draws its own × (antd Tag closable); without it the chip is non-interactive. Do not add a naked `onClick` without an accessible role.",
      "With `onRemove`, the × is part of the chip (antd Tag closable) — do not bolt a separate Button beside the label. Pass a string `children` label so the remover's accessible name quotes the chip.",
      "Without `onRemove`, Badge is a leaf — pass plain text or a short ReactNode as children. Do NOT nest another Badge or interactive controls inside it.",
      "Use semantic tokens for any className overrides (`text-muted-foreground`, `bg-destructive`) — never raw Tailwind palette classes like `bg-green-500`.",
      "DO pass `color` — never an inline `backgroundColor` — when the colour belongs to the RECORD rather than to its meaning (a status an administrator coloured, an issue type, a tag). A hand-filled chip has to choose a foreground, and no choice is readable for every colour a picker can produce; `color` moves the ground instead and keeps the label on the surface's own foreground."
    ],
    "useCases": [
      "Category or tier labels on table rows — e.g. plan tier (`<Badge variant=\"secondary\">Pro</Badge>`), document type (`<Badge variant=\"outline\">Invoice</Badge>`), or locale tag (`<Badge variant=\"secondary\">EN</Badge>`).",
      "Approval or review state in an accounting list where the value is not a lifecycle key in Badge's STATUS_MAP — e.g. a custom approval tier like `<Badge tone=\"success\">承認済</Badge>` or `<Badge tone=\"warning\">要確認</Badge>`.",
      "Inline count or highlight next to a heading or nav item — e.g. `<Badge variant=\"destructive\">3</Badge>` beside 'Overdue invoices' to draw attention to a non-zero count.",
      "Feature flags or experiment variant labels on admin records — e.g. `<Badge variant=\"outline\">A/B</Badge>` alongside a campaign row to indicate it is in a split test.",
      "Read-only metadata chips inside a Descriptions.Item or Card header where a lifecycle icon would be visually heavy — e.g. currency code, payment method, or region tag.",
      "A status or category an administrator coloured themselves, in an issue tracker or a CRM — `<Badge shape=\"pill\" color={status.color}>{status.name}</Badge>`. The colour comes out of a picker and is stored on the row, so it cannot be mapped to a tone."
    ]
  },
  {
    "example": "import { Legend } from \"@godxjp/ui/data-display\";\n\n<CardAction>\n  <Legend\n    items={[\n      { tone: \"destructive\", label: \"期限超過\" },\n      { tone: \"warning\", label: \"期限間近\" },\n      { tone: \"success\", label: \"対応済\" },\n    ]}\n  />\n</CardAction>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Legend",
    "props": [
      {
        "description": "The keys, in the order the marks they explain appear. `label` is required and cannot be omitted: colour alone never carries meaning (WCAG 1.4.1), so a wordless key would be the exact failure the component prevents. Each item is `{ tone, label }` or `{ color, label }`: `color` is a hex from data (user-chosen status colours, the same contract as Badge color and RangeTimelineRow color, gh#1189).",
        "name": "items",
        "required": true,
        "type": "{ tone: \"default\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"muted\" | \"neutral\"; label: ReactNode }[]"
      },
      {
        "description": "Root class. The swatch geometry lives in the --legend-* tokens, not here.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Progress — `segments` draws the breakdown this key explains; the two take the same tones.",
      "Badge — a Badge labels ONE thing in place; a Legend explains a colour used across many marks.",
      "Swatch — one read-only sample of an ARBITRARY colour (a brand's primary_color). Legend is a key over the closed semantic tones and always shows words; Swatch shows a value and takes its name from aria-label.",
      "CardAction — the header slot a Legend usually sits in, so the key lands opposite the CardTitle."
    ],
    "rules": [],
    "storyPath": "data-display/legend.tsx",
    "tagline": "The KEY for a colour-coded surface: which tone means what, spelled out in words once — a square swatch per tone with its label.",
    "usage": [
      "DO import from `@godxjp/ui/data-display`: `import { Legend } from \"@godxjp/ui/data-display\";`",
      "DO put it in the `CardAction` slot of the card whose bars or chart it explains, so the key sits on the same line as the card title and reads before the data.",
      "DO feed it the same tone order as the marks it explains — a key whose order differs from the bars forces the reader to map three colours by hand.",
      "DON'T build a key out of Badges. A Badge is a chip that reads as clickable and carries a tinted fill + border; a legend swatch is a SAMPLE of the exact colour the mark uses.",
      "DON'T hand-roll a coloured square: `<span className=\"w-[10px] h-[10px] rounded-[2px] bg-[#c0392f]\" />` is blocked by ui-audit three ways at once (no-arbitrary-size, no-arbitrary-radius, no-arbitrary-hex). For a colour the USER chose — a brand colour, a tag tint — reach for `Swatch`, which takes that value as a prop.",
      "DON'T give the swatch its own text or aria — it is aria-hidden on purpose, because it repeats the label beside it."
    ],
    "useCases": [
      "The key above a set of `Progress` breakdown bars — 期限超過 / 期限間近 / 対応済 — so the three tones are named once for the whole card instead of on every row.",
      "A chart card where the series colours need naming outside the chart's own runtime legend.",
      "A status column in a dense table: name the tones once in the card header rather than repeating a Badge in every cell.",
      "A calendar or heatmap whose cell tints encode a state (booked / held / free)."
    ]
  },
  {
    "example": "import { Swatch } from \"@godxjp/ui/data-display\";\nimport { Text } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n<Flex align=\"center\" gap=\"sm\">\n  <Swatch color={brand.primary_color} aria-label={`プライマリカラー: ${brand.primary_color}`} />\n  <Text weight=\"medium\">{brand.name}</Text>\n</Flex>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Swatch",
    "props": [
      {
        "description": "The colour to show, as any CSS colour value (#7C3AED, rgb(…), oklch(…)). It is DATA, the same axis as Badge's `color`: a value a person picked in a settings screen, so it arrives as a prop and never enters a stylesheet. A colour that MEANS something (success, overdue) is not this prop — use Badge `tone` or Legend, which name the meaning.",
        "name": "color",
        "required": true,
        "type": "string"
      },
      {
        "description": "The sample's accessible NAME, and the reason a swatch can be shown with no visible label: say what the colour is for AND what it is, e.g. `プライマリカラー: #7C3AED`. With it the mark is role=img and announces that sentence; WITHOUT it the mark is aria-hidden, which is correct only when a visible line beside it already states the colour. Colour is never the sole carrier of meaning either way (WCAG 1.4.1).",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "Legend — the KEY for a colour-coded surface: closed semantic tones, each with required words. Swatch is one sample of an arbitrary value and takes its name from aria-label.",
      "ColorPicker — the INPUT for the same value. Swatch displays; ColorPicker chooses.",
      "Badge — a chip that labels a thing (and can carry the entity's own `color` as a wash). Swatch is the colour itself, with no chip around it."
    ],
    "rules": [],
    "storyPath": "data-display/swatch.tsx",
    "tagline": "A READ-ONLY sample of ONE colour a person chose — a brand's primary_color, a calendar category, a tag tint. The colour is a VALUE on the element, never a token in a stylesheet.",
    "usage": [
      "DO import from `@godxjp/ui/data-display`: `import { Swatch } from \"@godxjp/ui/data-display\";`",
      "DO give it an aria-label whenever it is the only thing on screen saying what the colour is, and omit the label when the row's own text already says it — a swatch that repeats its neighbour announces the same thing twice.",
      "DO let the hairline do its job: it is built in so a white or near-white value is still a visible sample on a white card, and it survives forced-colors (the colour IS the content here, so it is not repainted).",
      "DON'T reach for it for a STATUS colour. success/warning/overdue are meanings, and a meaning belongs on Badge `tone` or in a Legend, which spell the meaning out in words.",
      "DON'T render a disabled ColorPicker to display a colour — that is an input that looks broken. ColorPicker is for CHOOSING the value; Swatch is for showing it.",
      "DON'T size it with a className. The mark's size, radius and hairline are the --swatch-* tokens, so a theme retunes every sample at once."
    ],
    "useCases": [
      "An organisation's brand card: primary_color and secondary_color beside the organisation name, read-only, with the hex in the accessible name.",
      "A list of calendar categories, label colours or tags where each row's own text already names the colour and the mark is purely visual.",
      "A settings screen showing the colour currently saved, next to the ColorPicker that changes it.",
      "A table cell whose column is 'colour' — one sample per row, named by the row header."
    ]
  },
  {
    "example": "import { TextDiff } from \"@godxjp/ui/lab\";\n\n<TextDiff before={row.previous_original} after={original} lang=\"ja\" />\n\n<TextDiff mode=\"split\" granularity=\"line\" before={previousBody} after={body} />",
    "group": "data-display",
    "importPath": "@godxjp/ui/lab",
    "name": "TextDiff",
    "props": [
      {
        "description": "The earlier text (the original a translation was written against, the saved version).",
        "name": "before",
        "required": true,
        "type": "string"
      },
      {
        "description": "The current text.",
        "name": "after",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "\"inline\"",
        "description": "inline — one flow with removals and additions in place (GitHub's unified view). split — two columns, before | after, with changed hunks aligned row by row and localized column headings.",
        "name": "mode",
        "type": "\"inline\" | \"split\""
      },
      {
        "defaultValue": "\"auto\"",
        "description": "The unit a change is measured in. auto = Intl.Segmenter words, with Han/Hiragana/Katakana/Hangul runs cut per character (no spaces to split on, and a dictionary word boundary moves when one particle changes). word = Intl.Segmenter words in `lang`. char = one grapheme cluster per token. line = whole lines.",
        "name": "granularity",
        "type": "\"auto\" | \"word\" | \"char\" | \"line\""
      },
      {
        "defaultValue": "true",
        "description": "Fold long unchanged runs of lines behind a localized 'show N unchanged lines' button.",
        "name": "collapseUnchanged",
        "type": "boolean"
      },
      {
        "defaultValue": "2",
        "description": "Unchanged lines kept visible beside each change when folding (git's -U<n>).",
        "name": "contextLines",
        "type": "number"
      },
      {
        "defaultValue": "4000000",
        "description": "LCS cost cap (tokens before × tokens after). Past it the changed middle is one removal plus one addition instead of freezing the tab.",
        "name": "maxCells",
        "type": "number"
      },
      {
        "description": "Heading of the before column in split mode. Default: localized 'Before'.",
        "name": "beforeLabel",
        "type": "ReactNode"
      },
      {
        "description": "Heading of the after column in split mode. Default: localized 'After'.",
        "name": "afterLabel",
        "type": "ReactNode"
      },
      {
        "description": "BCP-47 language of the text — set on the element (screen-reader pronunciation, CJK font selection) and used as the Intl.Segmenter locale.",
        "name": "lang",
        "type": "string"
      }
    ],
    "related": [
      "CodeBlock — shows ONE preformatted text; TextDiff shows how two versions differ.",
      "Text — `delete` / `mark` style a run you already chose; TextDiff decides the runs and announces them.",
      "Timeline — the list of revisions; TextDiff is what one revision changed."
    ],
    "rules": [],
    "storyPath": "data-display/text-diff.tsx",
    "tagline": "What changed between two versions of a text — removed runs in <del>, added runs in <ins>, inline or side by side, per line / word / character, with CJK cut per character and long unchanged runs folded.",
    "tier": "lab",
    "usage": [
      "DO import from `@godxjp/ui/lab` (the opt-in lab subpath): `import { TextDiff } from \"@godxjp/ui/lab\";`",
      "DO pass the two raw strings — the component tokenizes, diffs (LCS) and renders. Use the exported `diffText(before, after, { granularity, lang })` only when you need the runs themselves (a change count, a plain-text export).",
      "DO pass `lang` for the text's language, not the UI's: a Japanese original in an English UI still needs lang=\"ja\".",
      "DO use mode=\"split\" when there is room for two columns and the texts are long; inline for a drawer, a list row or a narrow panel.",
      "DON'T hand-roll a diff from `Text delete` / `Text mark` — those carry no spoken start/end label, so a screen reader reads the old and new words run together.",
      "DON'T colour a run without its mark: removals are struck through and additions underlined, plus a visually hidden start/end label — colour is never the only carrier (WCAG 1.4.1)."
    ],
    "useCases": [
      "A translation drawer: what changed in the original since this translation was written (old original → current original).",
      "A document or wiki page revision history: one revision against the previous one.",
      "A settings or template audit log: the previous value of a long text field against the new one."
    ]
  },
  {
    "docPath": "data-display/feature-list.tsx",
    "example": "import { FeatureList } from \"@godxjp/ui/data-display\";\n\n<FeatureList\n  items={[\n    { state: \"included\", label: \"SSO\", description: \"SAML と OIDC\" },\n    { state: \"limited\", label: \"API 呼び出し\", description: \"月 10,000 回まで\" },\n    { state: \"excluded\", label: \"監査ログのエクスポート\" },\n  ]}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "FeatureList",
    "props": [
      {
        "description": "The lines, in reading order. `state` drives the glyph, its mark colour and a localized sr-only word, so colour is never the only carrier (WCAG 1.4.1). `description` is a muted second line that WRAPS.",
        "name": "items",
        "required": true,
        "type": "{ state: \"included\" | \"excluded\" | \"limited\"; label: ReactNode; description?: ReactNode }[]"
      },
      {
        "description": "Root class. The rhythm and the glyph metric live in the --feature-list-* tokens, not here.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "ListRow — a single-line entity row (session, token, passkey) with a trailing action; it truncates and it rules a line between rows.",
      "Timeline — the same glyph rail, but ordered in TIME, with a connector and done/current/pending.",
      "Descriptions — a term/value grid. A feature line has no value column; the state IS the value, and it is drawn.",
      "Legend — the key that says what a COLOUR means across other marks; a FeatureList makes statements of its own."
    ],
    "rules": [],
    "storyPath": "data-display/feature-list.tsx",
    "tagline": "A list of STATEMENTS, each with a leading state glyph (included ✓ / limited − / excluded ✗), an optional wrapping description, and the glyph aligned to the first text line.",
    "usage": [
      "DO import from `@godxjp/ui/data-display`: `import { FeatureList } from \"@godxjp/ui/data-display\";`",
      "DO put a quantity INSIDE the label — there is no prop for it, because `<Text tone=\"muted\" tabular>` in the label is already legal and already audit-clean.",
      "DO leave `excluded` lines un-emphasised. The component mutes the label and draws a plain ✗; a red cross per omission reads as a list of errors instead of a list of facts.",
      "DON'T hand-roll it as a `<ul className=\"flex flex-col gap-2\">` with an `mt-0.5` icon nudge — that is three ui-audit errors (no-utility-layout, no-utility-spacing ×2), and the 2px is below --space-1 so no token step spells it.",
      "DON'T reach for ListRow: it is a single-line ENTITY row with a divider between every row and a trailing action slot, and it truncates where a statement has to wrap.",
      "DON'T reach for Timeline: it is an ORDERED event rail with a connector line and done/current/pending statuses — that is time, not inclusion."
    ],
    "useCases": [
      "A pricing-plan card listing what the plan includes, what it limits and what it leaves out.",
      "A comparison surface where each tier repeats the same rows with different states.",
      "A submission review: which requirements the upload met, which it met partially, which it missed.",
      "A capability or compatibility list — supported / partially supported / unsupported."
    ]
  },
  {
    "example": "import { Card, CardContent, CardHeader, CardTitle, ListRow, Badge } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Smartphone } from \"lucide-react\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n// The three scenarios below are three CARDS on one page, so they are wrapped in a Flex — the gap\n// between sibling cards belongs to the stack, never to the cards (audit: sibling-cards-need-flex).\n<Flex direction=\"col\" gap=\"lg\">\n<Card>\n  <CardHeader>\n    <CardTitle>アクティブなセッション</CardTitle>\n  </CardHeader>\n  <CardContent flush>\n    <ListRow\n      leading={<Smartphone aria-hidden=\"true\" className=\"size-4\" />}\n      title=\"iPhone 15 · Tokyo\"\n      description=\"最終アクセス 2分前\"\n      trailing={<Badge status=\"active\" />}\n    />\n    <ListRow\n      leading={<Smartphone aria-hidden=\"true\" className=\"size-4\" />}\n      title=\"MacBook Pro · Osaka\"\n      description=\"最終アクセス 3日前\"\n      trailing={<Button size=\"xs\" variant=\"outline\">ログアウト</Button>}\n    />\n  </CardContent>\n</Card>\n\n// Notifications — unread dot + emphasized surface, wrapping title, two inline actions\n<Card>\n  <CardContent flush>\n    <ListRow\n      unread\n      align=\"start\"\n      overflow=\"wrap\"\n      title=\"組織「グローバル・トランスフォーメーション推進本部」への招待が届いています\"\n      description=\"2026-07-30 09:12 JST\"\n      trailing={\n        <>\n          <Button size=\"xs\" variant=\"ghost\">既読にする</Button>\n          <Button size=\"xs\" variant=\"outline\">開く</Button>\n        </>\n      }\n    />\n    <ListRow unread={false} align=\"start\" overflow=\"wrap\" title=\"請求書が発行されました\" description=\"2026-07-28 18:40 JST\" />\n  </CardContent>\n</Card>\n\n{/* A list of LINKS — `as=\"li\"` gives the list item, `asChild` gives the whole-row link, and the\n   item carries the divider. Never wrap the row in your own li, and never reach for a raw ul:\n   `marker=\"none\"` is the semantic list container (no bullet, no indent, gap token). */}\n<Card>\n  <CardContent flush>\n    <Flex as=\"ul\" marker=\"none\" direction=\"col\" gap=\"none\">\n      {projects.map((project) => (\n        <ListRow key={project.key} as=\"li\" asChild title={project.name} description={project.key}>\n          <Link href={`/projects/${project.key}`} />\n        </ListRow>\n      ))}\n    </Flex>\n  </CardContent>\n</Card>\n</Flex>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "ListRow",
    "props": [
      {
        "defaultValue": "false",
        "description": "Supply a link child to make the entire row a native link. Use aria-current=page for the current destination; never nest interactive trailing controls. Combine with `as=\"li\"` inside a `<Flex as=\"ul\" marker=\"none\">` — the row stays the link and `as` supplies the list item around it.",
        "name": "asChild",
        "type": "boolean"
      },
      {
        "description": "Primary line — rendered in medium weight; truncates to one line.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Secondary line under the title (muted, xs); truncates to one line. ONE line, deliberately: a row that wants two (an endpoint, then its events) is asking for a different surface — antd `List.Item.Meta` has the same single `description`, so this is NOT an antd parity gap. Put the second line in `trailing` (a Badge, a count) or move the row to `Descriptions` / a detail panel (gh#714 §3).",
        "name": "description",
        "type": "ReactNode"
      },
      {
        "description": "Leading slot — a decorative icon or an Avatar. Mark a purely decorative icon `aria-hidden`.",
        "name": "leading",
        "type": "ReactNode"
      },
      {
        "description": "Trailing slot — the row action(s): a Button / DropdownMenu trigger, a Badge, or a Switch.",
        "name": "trailing",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"center\"",
        "description": "Cross-axis alignment of the columns; `start` for multi-line content.",
        "name": "align",
        "type": "\"center\" | \"start\""
      },
      {
        "defaultValue": "\"div\"",
        "description": "Render element — `li` when the parent is a semantic list (`<Flex as=\"ul\" marker=\"none\">` — a raw `<ul>` carries no gap token and, without `marker=\"none\"`, the bullet rule `.ui-flex[data-list] > li { display: list-item }` outranks `display: flex` on the row itself). WITH `asChild` the child owns the row element, so `as` becomes the list ITEM around it: `<li data-slot=\"list-row-item\"><a data-slot=\"list-row\">`. Use both for a list of links — do not wrap the row in an `<li>` (or a `role=\"listitem\"` div) yourself, because that makes every row an only child and the row-to-row divider, keyed on `:not(:last-child)` among siblings, stops matching on every row.",
        "name": "as",
        "type": "\"div\" | \"li\""
      },
      {
        "defaultValue": "\"truncate\"",
        "description": "How title/description resolve content longer than the row — `truncate` (one line + ellipsis) or `wrap` (multi-line; long unbroken tokens break via `overflow-wrap: anywhere`). Either way the content column may shrink below its intrinsic width, so the row never widens the page root.",
        "name": "overflow",
        "type": "\"truncate\" | \"wrap\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Row geometry. It only lowers thresholds — the row and its trailing cluster still wrap, so a cluster that cannot fit drops to its own line rather than widening the page root.",
        "name": "density",
        "type": "\"default\" | \"compact\""
      },
      {
        "description": "Read/unread state for notification rows — renders the indicator dot (with localized `sr-only` text, never colour alone) plus the tokenized `--list-row-unread-background`. OMIT the prop for rows with no read state; pass `false` for a read row so its title keeps the same optical axis as the unread ones.",
        "name": "unread",
        "type": "boolean"
      }
    ],
    "related": [
      "DataTable — use instead when the list is long or needs sorting/selection/pagination; ListRow is for short, chrome-light lists.",
      "Card — ListRow is designed to live inside `<CardContent flush>`; the Card supplies the outer surface and the closing border.",
      "Descriptions — for a key/value metadata grid on a detail page (no per-row action); ListRow is for actionable entity rows."
    ],
    "rules": [
      42,
      44
    ],
    "storyPath": "data-display/list-row.tsx",
    "tagline": "Single-line entity row (leading · title/description · trailing action) for SHORT lists inside a Card — sessions, API tokens, linked accounts, passkeys, MFA factors, invitations.",
    "usage": [
      "DO rely on ListRow for a cut title (gh#1298): a title or description that is actually truncated gets a `title` with its text (a `Text ellipsis` inside it, never a badge beside it) and unfolds while the row holds keyboard focus; when nothing in the row takes focus, the cut line gets its own tab stop. Do not add a hand-rolled Tooltip for that.",
      "DO use ListRow for a SHORT (≈2–8 item) list of entities inside a Card where each row is one line with an action — account sessions, API keys, linked identities, passkeys. Stack rows in a `<Card><CardContent flush>` so the rows draw their own quiet dividers edge-to-edge.",
      "DON'T reach for DataTable here — it carries sorting/selection/pagination chrome that a 3-item list doesn't need. DON'T nest a Card per row either (card-in-card). ListRow is the in-between surface.",
      "DO host ListRow in a DataTable column when the list needs ROW SELECTION (a mail/notification inbox with bulk actions) — DataTable is the only primitive that selects rows. Mark that column `flush: true` so the cell drops its own padding and the ListRow owns the inset (gh#1016); without it the cell padding stacks on the row's (a 32px dead gap after the checkbox, taller rows) and the unread band stops at the checkbox cell. DON'T zero the cell with a className.",
      "DO write a list of LINKS as `<Flex as=\"ul\" marker=\"none\" direction=\"col\" gap=\"none\">` + `<ListRow as=\"li\" asChild><Link/></ListRow>` — one call gives the list item, the whole-row link and the divider. DON'T wrap the row in your own `<li>` or `role=\"listitem\"` element to get list semantics back: the divider rule reads `:not(:last-child)` among the rows themselves, so a wrapper per row makes each one an only child and EVERY divider disappears — silently, which is why `ui-audit` now flags it as `no-hand-rolled-list`.",
      "DON'T hand-roll `<div className=\"flex items-center justify-between border-b py-3\">` — that is exactly the repeated pattern ListRow replaces (border/radius/padding are tokenized via `--list-row-*`).",
      "DO put the row's action in `trailing` (a `ghost`/`outline` Button, a DropdownMenu trigger, a Switch, or a status Badge). DO pass `as=\"li\"` when the rows live inside a semantic list, and build that list as `<Flex as=\"ul\" marker=\"none\">` — `ui-audit` flags a raw `<ul>`/`role=\"list\"` as `no-hand-rolled-list`.",
      "DO use `unread` for a notification list — the dot is a SHAPE with localized `sr-only` text (\"Unread\"/\"Read\"), so it never reads as colour alone, and the row surface reads `--list-row-unread-background` (default `hsl(var(--muted))` — chosen so the xs muted description line stays WCAG AA on the emphasized surface; `--accent` would drop it to 4.23:1). DON'T substitute a `Badge` — that renders a labelled pill, not a compact status dot.",
      "DO pass `density=\"compact\"` for the canonical invitation / history row — an Avatar, a title (+ description) and one or two small trailing Buttons that must read as ONE line inside a narrow card (≈326px content at 390px), or a history row whose status Badge + ISO-8601 date belongs beside the title. Measured at 390px: 62px tall vs 126px at the default density (where the actions wrapped), history row 41px vs 114px. DON'T reach for it just to \"make things tighter\" on a roomy page — the default density is the entity-row measure.",
      "DON'T add one-off `min-width`/wrapping CSS in the consumer app for a long title + two trailing Buttons. The row already shrinks and WRAPS: the content column keeps only `min(var(--list-row-body-min-width), 100%)` and the trailing actions drop onto their own line below the threshold. Retune the threshold with `--list-row-body-min-width` (default 12rem) and the action gap with `--list-row-trailing-gap`; pass `overflow=\"wrap\"` (usually with `align=\"start\"`) when the title must stay fully readable at 390px instead of truncating.",
      "DO expect rows ANYWHERE inside a padded (non-flush) `CardContent` — directly, in a `Flex as=\"ul\"`, or wrapped (`DataState` children in a div > `Flex as=\"ul\"` > `ListRow as=\"li\" asChild` > link, gh#1325) — to drop their own inline padding: the card already holds the inset, so the row text lines up with the CardTitle instead of sitting 16px inside it. A row keeps `--list-row-padding-x` inside `CardContent flush` (the documented home, with edge-to-edge dividers) — including a flush CardContent nested in a padded one — and inside a table cell."
    ],
    "useCases": [
      "Account security page — a list of active sessions (device + last-seen as title/description, a destructive 'Revoke' Button in trailing).",
      "Developer settings — API tokens or passkeys, each row showing the name + created date and a DropdownMenu of actions.",
      "Linked accounts / SSO — an IdP icon in leading, the provider name + connected email, and a Switch or 'Disconnect' Button trailing.",
      "Notifications inbox — `unread` rows carry the dot + emphasized surface, `overflow=\"wrap\"` keeps a long JA/EN/VI title and its ISO-8601 timestamp readable, and two inline trailing Buttons (Mark as read / Open) wrap under the text at 390px.",
      "Pending invitations — Avatar in leading, the organization/invitation name as title, and Accept + Decline Buttons in trailing that stack at narrow widths without a horizontal page scrollbar."
    ]
  },
  {
    "example": "import { CredentialReveal } from \"@godxjp/ui/data-display\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@godxjp/ui/feedback\";\n\n<Dialog open={open} onOpenChange={setOpen}>\n  <DialogContent>\n    <DialogHeader>\n      <DialogTitle>APIキーを発行しました</DialogTitle>\n    </DialogHeader>\n    <CredentialReveal\n      label=\"APIキー\"\n      secret=\"gxp_live_8Fh2kQ9wR7nZ1xV4bT6mL0cD\"\n      downloadable\n      onAcknowledge={() => setOpen(false)}\n    />\n  </DialogContent>\n</Dialog>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "CredentialReveal",
    "props": [
      {
        "description": "The one-time secret value.",
        "name": "secret",
        "required": true,
        "type": "string"
      },
      {
        "description": "Accessible name / caption for the secret (e.g. 'API key').",
        "name": "label",
        "type": "string"
      },
      {
        "description": "Caution banner copy; defaults to a localized 'shown only once' warning. Pass null to suppress the banner.",
        "name": "warning",
        "type": "React.ReactNode | null"
      },
      {
        "description": "Controlled reveal state (with defaultRevealed / onRevealedChange).",
        "name": "revealed",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Uncontrolled initial reveal state.",
        "name": "defaultRevealed",
        "type": "boolean"
      },
      {
        "description": "Reveal toggle handler.",
        "name": "onRevealedChange",
        "type": "(revealed: boolean) => void"
      },
      {
        "description": "Called after the secret is written to the clipboard.",
        "name": "onCopy",
        "type": "(secret: string) => void"
      },
      {
        "description": "Renders a confirm button; wire it to the Dialog's onOpenChange(false).",
        "name": "onAcknowledge",
        "type": "() => void"
      },
      {
        "description": "Copy on the button `onAcknowledge` creates. Defaults to a localized \"I've saved it\" — override it when the confirmation claims something more specific than having read the secret (\"保管しました\", \"Stored in 1Password\"). Consumer-owned wording: route it through t().",
        "name": "acknowledgeLabel",
        "type": "React.ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "Offer a download-as-file button.",
        "name": "downloadable",
        "type": "boolean"
      },
      {
        "defaultValue": "\"credential.txt\"",
        "description": "Name of the file `downloadable` writes. The default is deliberately anonymous; set it when the user will hold several at once (`api-key-prod.txt`) so the saved files are still telling apart in a downloads folder.",
        "name": "downloadFileName",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Action button size tier.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "\"warning\"",
        "description": "Caution banner severity.",
        "name": "tone",
        "type": "\"warning\" | \"destructive\" | \"info\""
      },
      {
        "description": "DOM id on the root. Useful here because the surface is usually inside a Dialog: it is what a `aria-describedby` on the dialog, or a deep link back to the issued credential, can point at.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Accessible name for the credential region when `label` is not set or is a non-string node. `label` is the VISIBLE caption and already names the region when it is a plain string, so reach for this only when the caption is rich content or when the surrounding dialog title is the only thing saying which secret this is.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "PasswordInput — editable password/secret ENTRY with a show/hide toggle (data-entry); CredentialReveal is read-only DISPLAY of an issued secret.",
      "Dialog — the modal CredentialReveal is designed to live inside.",
      "Alert — the caution banner CredentialReveal composes internally."
    ],
    "rules": [
      3,
      6,
      23
    ],
    "storyPath": "data-display/credential-reveal.tsx",
    "tagline": "One-time secret surface — masked-by-default value with a show/hide toggle, a copy button that confirms the copy, optional download, and an optional acknowledge action to pair with Dialog. The GitHub/Stripe token-reveal pattern as a real primitive so consumers stop hand-rolling it.",
    "usage": [
      "DO use for a secret shown exactly once after creation (device credential, API key, service-account secret) — it masks by default and confirms the copy.",
      "DO pair it inside a Dialog and reset via controlled `revealed`/`onRevealedChange` (or let it re-blur automatically when the `secret` prop changes) so a reopened dialog starts masked.",
      "DO pass `onAcknowledge` to gate the dialog close behind an explicit 'I've saved it' confirmation.",
      "DON'T use it for an editable password field — that's PasswordInput. CredentialReveal is read-only display of an issued secret.",
      "DON'T hand-roll the copy button + copied-state + aria-live announcement; it's built in."
    ],
    "useCases": [
      "Device credential issued after enrollment",
      "API key / personal access token shown once on creation",
      "Service-account secret / client secret reveal",
      "Recovery code or one-time bootstrap password"
    ]
  },
  {
    "example": "import { CredentialReveal, QrCode } from \"@godxjp/ui/data-display\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n<Flex direction=\"col\" gap=\"md\">\n  <QrCode\n    value={security.qr_code_url}\n    label=\"二要素認証の登録用QRコード\"\n    size=\"lg\"\n  />\n  <CredentialReveal\n    secret={security.secret}\n    label=\"手動設定キー\"\n    warning={null}\n    defaultRevealed\n  />\n</Flex>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "QrCode",
    "props": [
      {
        "description": "Value encoded in-process. It is never used as accessible text or a URL.",
        "name": "value",
        "required": true,
        "type": "string"
      },
      {
        "description": "Localized, purpose-specific accessible name for the QR image.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Natural display-size tier; remains bounded by the available inline space.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Root SVG class override.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Root SVG element id.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "CredentialReveal — pair it with QrCode for the manual setup-key fallback without exposing the full encoded URI.",
      "Card — supplies the one outer enrollment surface; QrCode intentionally adds no nested border or card chrome.",
      "Text — provides concise localized scan/manual guidance around the non-text QR image."
    ],
    "rules": [
      6,
      7,
      23,
      24
    ],
    "storyPath": "data-display/qr-code.tsx",
    "tagline": "Local-only SVG QR renderer for enrollment links, device pairing and other values that must never be sent to a third-party image service.",
    "usage": [
      "DO use QrCode when the encoded payload must remain inside the browser, especially an otpauth enrollment URI containing a TOTP secret.",
      "DO provide a localized label that describes the job, such as 'Two-factor authentication setup code'; the encoded value is intentionally excluded from the accessibility tree.",
      "DO compose an adjacent CredentialReveal with the manual key when users need a non-camera fallback. Keep both inside one parent Card rather than adding a bordered QR card.",
      "DON'T create an image URL with the payload, call a remote QR service, add a logo/image overlay, or expose raw colour/size props. The local encoder, four-module quiet zone and scanner-safe colours are security and reliability invariants.",
      "DON'T encode the manual secret alone for TOTP. Encode the canonical otpauth URI returned by the backend so issuer, account, algorithm, digits and period stay intact."
    ],
    "useCases": [
      "TOTP authenticator enrollment",
      "Device pairing or application handoff",
      "Locally rendered invitation or deep link",
      "Payment or ticket payload that must not reach a third-party renderer"
    ]
  },
  {
    "example": "import { Descriptions } from \"@godxjp/ui/data-display\";\n\n<Descriptions columns={2}>\n  <Descriptions.Item label=\"会員ID\" mono>{member.id}</Descriptions.Item>\n  <Descriptions.Item label=\"プラン\">{member.plan}</Descriptions.Item>\n  <Descriptions.Item label=\"メモ\" span={2}>{member.note}</Descriptions.Item>\n</Descriptions>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Descriptions",
    "props": [
      {
        "defaultValue": "2",
        "description": "Column count (antd `column`, in this library's plural spelling). A plain 1 | 2 | 3 keeps the mobile-first ladder it has always painted (1 → sm:2 → lg:3). Any other number, or the responsive object, publishes --descriptions-column-count per breakpoint instead, so a 4- or 6-column reference grid — and a per-breakpoint one — is expressible without a class-name fork.",
        "name": "columns",
        "type": "number | { sm?: number; md?: number; lg?: number; xl?: number }"
      },
      {
        "defaultValue": "false",
        "description": "Draw the grid as a bordered table with a rule between every cell and a shaded label cell (antd `bordered`) — the read-only counterpart of a dense data grid. Every cell draws its own block-start/inline-start rule and the container closes the other two edges, so the frame is complete at any column count, including a responsive one. Colour, radius and cell inset come from the --descriptions-* tokens.",
        "name": "bordered",
        "type": "boolean"
      },
      {
        "defaultValue": "\"vertical\"",
        "description": "Label placement within each item — `vertical` stacks the label over the value (default); `horizontal` puts the label BESIDE the value in a token-aligned column (mirrors `<Form layout>`). Tune the horizontal label-column width via `--descriptions-label-width`.",
        "name": "layout",
        "type": "\"vertical\" | \"horizontal\""
      },
      {
        "defaultValue": "\"start\"",
        "description": "Applies only in layout=\"horizontal\" — a vertical label sits above its value and end-aligning it there would read as a mistake, the same contract `Form` keeps.",
        "name": "labelAlign",
        "type": "\"start\" | \"end\""
      },
      {
        "description": "Descriptions.Item elements.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Declarative items (antd `items`) — the alternative to composing `Descriptions.Item` children. `children` is antd's name for the value and `value` is this library's older one; either works. Both APIs can be mixed: items render first, then any children. On an item, span accepts antd's 'filled' (take the whole remaining row) and the responsive object as well as the 2 | 3 the compound form has always taken.",
        "name": "items",
        "type": "{ key?: React.Key; label: ReactNode; children?: ReactNode; value?: ReactNode; mono?: boolean; span?: number | 'filled' | { sm?, md?, lg?, xl? }; className?: string }[]"
      }
    ],
    "related": [
      "Card / CardContent — Descriptions provides the internal grid layout; Card/CardContent provides the outer container, padding, and border. Always wrap Descriptions in CardContent (never add p-4 directly on Descriptions). Use Card when you need the visual surface; use Descriptions inside it for the label/value structure.",
      "DataTable — use DataTable when you have multiple rows of the same entity type that need sorting, filtering, or pagination. Use Descriptions when you have a single entity's fields laid out as labelled metadata (one row per field, not one row per record).",
      "Table — use Table (the lower-level primitive) for tabular data with explicit column headers and multiple data rows. Use Descriptions when the data is inherently label→value (no column headers needed, each field is its own row/cell).",
      "Flex — use Flex for arbitrary vertical/horizontal layout of heterogeneous UI elements. Use Descriptions when every item follows the label-on-top / value-below pattern and you want responsive multi-column alignment for free."
    ],
    "rules": [],
    "storyPath": "data-display/descriptions.tsx",
    "tagline": "Responsive definition grid for detail-page metadata. COMPOUND — value goes in Descriptions.Item children.",
    "usage": [
      "DO use Descriptions.Item as the ONLY direct child — never raw <div>, <dt>/<dd>, or plain text nodes. Every label/value pair must be wrapped in <Descriptions.Item label=\"…\">value</Descriptions.Item>.",
      "DO pass span={2} or span={3} on an Item when its value is long (e.g. a full address, a memo field, a JSON blob) — span={2} applies sm:col-span-2 and span={3} applies lg:col-span-3, keeping the grid aligned across breakpoints.",
      "DO pass mono on Item for machine-readable values: IDs, UUIDs, file paths, currency codes, JSON snippets. This sets font-mono + break-all so long strings wrap rather than overflow.",
      "DO embed any ReactNode as the Item child — Badge, Badge, formatDate output, a Tooltip-wrapped value, or a plain string all work. The value slot is not text-only.",
      "DON'T use Descriptions as a hand-rolled <dl>/<dt>/<dd> replacement for prose or running text — it is for structured metadata on detail/show pages only. For flowing key→value prose, use a plain <dl>.",
      "DON'T add className padding or margin to the root Descriptions to simulate a Card — wrap it in CardContent instead. Descriptions provides only grid layout (gap-x-6 gap-y-3); outer spacing is the Card/CardContent concern."
    ],
    "useCases": [
      "Detail/show page header block — displaying entity metadata such as invoice number, status, due date, vendor name, and payment method in a 2- or 3-column grid before the line-item DataTable.",
      "Account or member profile panel — showing user ID (mono), plan, registered date, email, and a status Badge in one scannable block instead of a vertical stack of FormField-looking rows.",
      "Accounting journal entry detail — date, reference code (mono), debit account, credit account, amount, and memo (span={2}) grouped in a compact grid alongside a Timeline of audit events.",
      "Read-only summary step in a multi-step form or wizard — displaying the values the user entered before final submission (Steps + Descriptions), without any input controls.",
      "Sidebar or Sheet detail pane — a narrow 1-column Descriptions inside a Sheet presenting the selected row's metadata while the main DataTable stays visible.",
      "API / webhook event inspector — showing event ID (mono, span={2}), event type, timestamp, HTTP status, and payload size in a grid, with a Badge for the status code."
    ]
  },
  {
    "example": "import { EmptyState } from \"@godxjp/ui/data-display\";\n\n<EmptyState title=\"該当データがありません\" description=\"検索条件を変更してください。\" />",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "EmptyState",
    "props": [
      {
        "description": "Primary empty message.",
        "name": "title",
        "required": true,
        "type": "string"
      },
      {
        "description": "Secondary helper text.",
        "name": "description",
        "type": "string"
      },
      {
        "description": "Icon above the title.",
        "name": "icon",
        "type": "LucideIcon"
      },
      {
        "description": "CTA element (e.g. a Button).",
        "name": "action",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"page\"",
        "description": "Contextual visual weight. Compact omits the icon medallion.",
        "name": "variant",
        "type": "\"page\" | \"section\" | \"compact\""
      },
      {
        "defaultValue": "\"muted\"",
        "description": "Medallion colour intent (a subset of the shared tone vocabulary; `destructive` is the DS name for a danger state). Recolours BOTH the icon GLYPH and the medallion fill from the matching role token — set `success` for a confirmation zero-state (e.g. device approved) instead of hand-rolling a `.ui-success-state` class. The glyph inherits `--empty-state-icon-foreground`; never pass a `text-*` colour utility on your icon, it would out-specify the token and pin every tone to muted.",
        "name": "tone",
        "type": "\"muted\" | \"success\" | \"warning\" | \"destructive\" | \"info\""
      },
      {
        "defaultValue": "3",
        "description": "Semantic heading level of the title. A page/onboarding empty state directly under the page h1 uses titleLevel={2}; one nested in an already-h2 section keeps the default 3.",
        "name": "titleLevel",
        "type": "1 | 2 | 3 | 4"
      },
      {
        "description": "Render the title as a non-heading element (p/div) instead of a heading — for a compact/section empty state inside a section that already owns its heading, so the message is not announced as a heading and cannot skip an outline level. Overrides titleLevel.",
        "name": "titleAs",
        "type": "\"h1\" | \"h2\" | \"h3\" | \"h4\" | \"p\" | \"div\""
      },
      {
        "defaultValue": "false",
        "description": "Announce the empty state as a status message (`role=\"status\"`). Default false since gh#1271: a static empty state (\"No invoices yet\") is content, not an update. Rule of thumb — on the page when it loads → leave it off; APPEARS because the user just searched or filtered and nothing matched → `live`.",
        "name": "live",
        "type": "boolean"
      }
    ],
    "related": [
      "DataTable — already embeds an EmptyState automatically when `data` is empty; customise via the `empty=` prop. Do NOT wrap DataTable in a `data.length === 0` guard that renders EmptyState separately.",
      "DataState — TanStack Query lifecycle widget (`@godxjp/ui/query`). Pass `<EmptyState />` to its `empty=` prop for zero-items; DataState itself covers loading/error — do not use EmptyState for those states.",
      "InfiniteQueryState — same pattern as DataState but for `useInfiniteQuery`; pass EmptyState to `empty=` when the flattened list is empty.",
      "SkeletonTable — use for the loading skeleton before data arrives (pass to DataState's `skeleton=` or DataTable's `loading=`). EmptyState is for after data arrives and is empty, not while loading."
    ],
    "rules": [],
    "storyPath": "data-display/empty-state.tsx",
    "tagline": "Centred empty placeholder with icon, title, description, and optional CTA.",
    "usage": [
      "DO decide whether the empty state is a STATUS MESSAGE (gh#1271). It is not a live region by default: a static \"No records yet\" that is there when the page loads would otherwise be announced as an update on every render (WCAG 4.1.3 covers content that CHANGES). Pass `live` (→ `role=\"status\"`) only when it appears as the result of a user action — a search or filter that matched nothing: `<EmptyState live icon={SearchIcon} title=\"No matching invoices\" />`. Inside `DataTable` do neither — its empty cell already carries `aria-live=\"polite\"`.",
      "DO always pass `title` — it is the only required prop and renders a heading (`<h3>` by default); omitting it causes a blank silent render with no visible error.",
      "DO set `titleLevel` to match the page outline (page h1 → section h2 → nested h3) so the empty state does not trigger a heading-order violation. Choose the level for OUTLINE position, never for visual size — the size never changes with the level. When the empty state sits in a section that already has its own heading, use `titleAs=\"p\"` so the message is not a heading at all.",
      "DO use `tone=\"success\"` (or warning/destructive/info) for a semantic confirmation/alert zero-state — it recolours the icon medallion from the role token; do NOT hand-roll a `.ui-success-state` class that scopes `--empty-state-icon-*`.",
      "DO use the `icon` prop (a Lucide icon component, not a JSX element) to give visual context — e.g. `icon={InboxIcon}` for empty inboxes, `icon={SearchIcon}` after a failed search. Pass the component reference, not `<InboxIcon />`.",
      "DO use `action` (a `ReactNode`, typically a `<Button>`) for actionable zero-states — e.g. 'Create first invoice' — so users have a clear next step instead of a dead end.",
      "DO NOT hand-roll a `data.length === 0 ? <EmptyState /> : <DataTable />` conditional — `DataTable` already embeds an `EmptyState` in its body when `data` is empty. Use the `empty=` prop on `DataTable` to customise it, not a wrapper conditional.",
      "DO NOT use EmptyState inside a `DataState` or `InfiniteQueryState` for the loading or error states — those widgets handle skeleton/error themselves; pass `EmptyState` only to their `empty=` prop for the zero-items case.",
      "DO NOT add padding directly on `EmptyState` via `className` when placing it inside a `Card` — wrap it in `<CardContent>` first; EmptyState is a self-contained block with its own internal spacing via `ui-empty-state` styles.",
      "DO omit optional secondary sections when absence has no user value. Otherwise use variant='compact' or 'section'; reserve page for the primary page job.",
      "DO match empty-state visual weight to the section's importance and expected content density — a low-priority 'no received invitations' block uses variant='compact' (no medallion, minimal padding), not the full page treatment that would outweigh real content.",
      "DO NOT wrap every empty condition in its own bordered Card. A compact/section empty state sits directly in the existing CardContent / section it belongs to; a dedicated bordered Card is only for a page-level or standalone zero-state.",
      "DO pair `variant='page'` (the default) with `<PageContainer fill>` when the zero-state IS the whole page body — `fill` hands the body the shell's remaining height and the page zero-state then takes that height and centres in it. That is ONE fact (this page's body is its empty state), not a second prop to keep in lockstep: no `fill` on the empty state, no `h-full`/`min-h-screen`/`grid place-items-center` wrapper, no spacer div. Measured on a consumer dashboard at 1440x805: without it the block top-packed at y=240 with 348px of white below; with `fill` alone the body grew to 541px and the void merely moved; with both the block resolves to 525px and centres. Under an ordinary auto-height body nothing changes, so leave `fill` off on a page that has real content beneath the header."
    ],
    "useCases": [
      "Zero-row admin list pages (invoices, accounts, transactions) that are NOT backed by a `DataTable` — e.g. a card-grid or custom list layout where DataTable's built-in empty state doesn't apply.",
      "Post-filter / post-search zero results — show `icon={SearchIcon}` + a `description` explaining what was searched and an `action` to clear filters.",
      "First-run onboarding screens where no data has been created yet — e.g. 'No entities added yet' with an action button to create the first legal entity.",
      "Passed as the `empty=` prop inside `DataState` or `InfiniteQueryState` to satisfy the TanStack Query lifecycle widget's zero-items slot without hand-rolling markup.",
      "Standalone section within a `CardContent` to indicate a sub-section (e.g. attachments, comments, related records) has no entries yet, separate from the page-level list.",
      "Error-adjacent zero states where the page loaded successfully but the filtered result set is empty — distinct from an error state handled by `DataState`/`AlertMutationFeedback`."
    ]
  },
  {
    "example": "import { Progress } from \"@godxjp/ui/data-display\";\n\n<Progress value={pct} label={pct + \"% 使用中\"} tone={pct >= 80 ? \"warning\" : \"success\"} />\n<Progress value={252} over label=\"252% 積載\" />\n<Progress\n  segments={[\n    { value: 2, tone: \"destructive\", label: \"期限超過\" },\n    { value: 3, tone: \"warning\", label: \"期限間近\" },\n    { value: 12, tone: \"success\", label: \"対応済\" },\n  ]}\n  aria-labelledby={companyNameId}\n/>\n<Progress\n  segments={[\n    { value: 3, tone: \"success\", label: \"合格\" },\n    { value: 1, tone: \"destructive\", label: \"失敗\" },\n  ]}\n  total={400}\n  remainderLabel=\"未実施\"\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Progress",
    "props": [
      {
        "description": "METER mode: progress percentage 0–100 (clamped unless `over`). Required unless you pass `segments` — the two modes are a discriminated union, so `value` and `segments` can never appear together.",
        "name": "value",
        "required": true,
        "type": "number"
      },
      {
        "description": "BREAKDOWN mode: one total split into slices. Pass ABSOLUTE amounts in one unit (counts, bytes, yen) — the bar computes each share, so three numbers never have to be rounded into 100. Renders role=\"img\" named from every slice (a partition is not three progressbars), on a taller track (--progress-breakdown-block-size 1.375rem, --progress-breakdown-radius var(--radius)) because three abutting fills on the meter's 0.5rem pill read as a coloured hairline. `label` on each slice is REQUIRED — colour alone never carries meaning (WCAG 1.4.1). Mutually exclusive with value/tone/over. Slice tones add `info` and `neutral` to the three meter tones (gh#1302) — the same fills as the matching `Legend` swatches, so a four-state partition (done · awaiting · in progress · not started) stays distinguishable and keys one-to-one.",
        "name": "segments",
        "type": "{ value: number; tone: \"success\" | \"warning\" | \"destructive\" | \"info\" | \"neutral\"; label: string }[]"
      },
      {
        "description": "BREAKDOWN mode: the WHOLE the slices are drawn against, in the same unit as their values. Omit it and the whole is the sum of the slices (the bar is always full). Pass it when part of the whole has no state yet — 3 passed of 400 test cases — and the rest stays the neutral TRACK, the antd `percent` model where the unfilled track is the remainder. The remainder is spoken in the role=\"img\" name, Intl-formatted in the active locale. A total below the slice sum is ignored (the sum wins), so slices never overflow.",
        "name": "total",
        "type": "number"
      },
      {
        "description": "BREAKDOWN mode, with `total`: what the remainder is called in the spoken breakdown (e.g. 未実施). Defaults to the catalogue's Remaining / 残り / Còn lại.",
        "name": "remainderLabel",
        "type": "string"
      },
      {
        "description": "VISIBLE caption beside/below the bar; it also becomes the accessible name. (Unlike Anchor `label`, which is screen-reader-only: there the name is never drawn.) Pass `aria-labelledby` instead when the name is ALREADY on screen (a row's company name, a card heading) — the bar then borrows it rather than repeating it.",
        "name": "label",
        "type": "string"
      },
      {
        "defaultValue": "\"primary\"",
        "description": "Bar colour tone. `primary` (default since gh#1308) is the brand role for ordinary progress — a reading position, an upload; pass a status tone only when the bar reports that status (ADR C09: `success` for a completed/healthy state, `warning`/`destructive` for limits). Over-capacity defaults to destructive.",
        "name": "tone",
        "type": "\"primary\" | \"success\" | \"warning\" | \"destructive\""
      },
      {
        "defaultValue": "false",
        "description": "Allow value > 100 to render an over-capacity fill: bar caps at 100% width but gets a diagonal hatch + destructive tone (e.g. 252%). aria-valuetext reports the real ratio. Off by default (clamps to 100). On shape='ring' there is no hatch — diagonal stripes are a rectangle drawing — so the destructive tone and aria-valuetext carry it.",
        "name": "over",
        "type": "boolean"
      },
      {
        "defaultValue": "\"bar\"",
        "description": "METER geometry — the SAME measurement drawn as a full-width bar or as an arc. Same value, tone, size and ARIA either way; `label` moves INSIDE the ring, which is the point of it: a phone app bar showing '18 of 42 done' beside a title has one square of space, and a bar plus its caption needs two stacked rows. Reach for it when the SPACE is square, not when the number is important. It is meter-only: a ring around a `segments` breakdown is a pie chart, which is `PieChart donut` in the charts entry point — a part-to-whole across CATEGORIES with a legend, announced as an image rather than as a progressbar. The arc reads the MARK token tier, exactly as the bar does, so a warning ring and a warning bar on one screen are one colour.",
        "name": "shape",
        "type": "\"bar\" | \"ring\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Bar THICKNESS, on both modes. `md` is what each mode was designed at and stays the default. `sm` is for a bar that annotates a row rather than being the subject of the screen — an in-table capacity column, where a 1.375rem breakdown outweighs the row and sets the height of the whole table. Thickness only: the ARIA, the spoken breakdown and the corner are untouched. Never hand-write CSS to thin a bar — ui-audit blocks that in a consumer, and the two steps come from --progress-meter-block-size(-sm) / --progress-breakdown-block-size(-sm) so a theme retunes the scale instead of one call site.",
        "name": "size",
        "type": "\"sm\" | \"md\""
      }
    ],
    "related": [
      "Slider — use Slider when the user must drag or set a bounded numeric value (volume, priority, price range); use Progress when the value is read-only and must not be interacted with.",
      "Steps — use Steps for a discrete, named sequence of phases (onboarding wizard, checkout flow) where each step has a label and a clear current/done/pending state; use Progress for a continuous 0–100 fill.",
      "Badge / Badge — use Badge or Badge to communicate a categorical status label (e.g. \"Paid\", \"Overdue\") without a fill metaphor; use Progress when the numeric proportion itself is the information.",
      "StatCard — use StatCard to headline a single KPI metric with a title; compose Progress inside or alongside StatCard when a visual fill adds meaning to the number.",
      "Legend — the key for a breakdown: which tone means what, spelled out in words once for the whole card instead of on every bar.",
      "BarChart / PieChart / LineChart (@godxjp/ui/charts) — a Progress breakdown handles ONE total split into a few named states, inline and without a charting runtime; move up to a chart when you have several series, many categories, or a value changing over time."
    ],
    "rules": [],
    "storyPath": "data-display/progress.tsx",
    "tagline": "Progress in two modes: a METER (`value` 0–100, optional tone, over-capacity striped state, drawn as a bar or — with `shape='ring'` — as an arc with the readout inside it) and a BREAKDOWN (`segments` — one total split into tone-coloured slices on a taller track).",
    "usage": [
      "DO import from `@godxjp/ui/data-display`, not from a generic UI path: `import { Progress } from \"@godxjp/ui/data-display\";`",
      "DO pass `value` as a 0–100 number — the component clamps it internally via `Math.max(0, Math.min(100, value))`, so out-of-range values are safe but misleading; compute the real percentage before passing it.",
      "DO drive `tone` dynamically from business logic — e.g. `variant={pct >= 80 ? \"warning\" : \"success\"}` — to communicate threshold status semantically rather than with raw colour classes.",
      "DON'T use a `disabled` Slider as a read-only progress bar — Slider is semantically an interactive control even when disabled, which pollutes the a11y tree and exposes the wrong ARIA role (`slider` vs `progressbar`). Progress renders the correct read-only indicator.",
      "DON'T pass children or sub-components — Progress is a single self-contained element (track + bar + label). The `label` prop is the only text injection point; don't wrap it in a custom parent div to add a label alongside it.",
      "DON'T hand-roll a stacked bar out of three divs to show a part-to-whole split — pass `segments`. Hand-rolled slices need a hex fill, an arbitrary height and an arbitrary radius, which ui-audit blocks three ways (no-arbitrary-hex, no-arbitrary-size, no-arbitrary-radius), and they leave the picture with no accessible name at all.",
      "DON'T convert segment amounts to percentages yourself — pass the raw counts. The component divides by the total, so the slices always sum to the whole; pre-rounded percentages do not.",
      "DO pass `total` when part of the whole has no state yet (a test run: 合格 3 · 失敗 1 of 400) — the rest stays track and is read out as `remainderLabel` (e.g. \"未実施\"). Leaving it out makes 3 passed of 400 fill the whole bar green.",
      "DO pair a breakdown with `Legend` so each tone is spelled out in words once, instead of repeating the labels on every bar.",
      "DON'T use Progress for editable numeric input or range selection — it has no callbacks, no interactivity, and no form `name` prop. Use Slider (bounded range input) or Input (free-form number) for data-entry scenarios."
    ],
    "useCases": [
      "Budget utilisation in an accounting dashboard — show how much of a monthly budget has been consumed, switching to `tone=\"warning\"` when the figure crosses 80%.",
      "Invoice payment progress — display the proportion of an invoice total that has been settled (e.g. partial payments), with a label like `\"¥45,000 / ¥60,000 支払済\"` computed before passing `value`.",
      "Storage or quota indicator in an admin panel — visualise disk usage, API quota, or seat licence consumption against a fixed limit.",
      "Sync / import job completion feedback — surface the completion percentage of a long-running background job (polling the server) without giving the user an interactive control.",
      "StatCard companion — pair with a `StatCard` metric to add a visual fill below the KPI number, reinforcing how close a target is to being met.",
      "Multi-step onboarding or setup checklist — render one Progress per section (e.g. 3/5 steps complete = 60%) to give users a quick scan of overall progress across areas.",
      "Over-capacity meter — an air-cargo weight/volume load or an over-booked resource pushed past its limit (e.g. 252%): pass `over` with the real ratio to get a red diagonal-hatched bar that reads unmistakably as over-limit, not merely full."
    ]
  },
  {
    "example": "import { CodeBlock } from \"@godxjp/ui/data-display\";\n\n<CodeBlock maxHeight=\"sm\" language=\"json\" aria-label=\"Response body\">{body}</CodeBlock>\n<CodeBlock size=\"xs\" maxHeight=\"md\" aria-label=\"Console\">{consoleText}</CodeBlock>\n<CodeBlock copyable language=\"bash\">pnpm add @godxjp/ui</CodeBlock>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "CodeBlock",
    "props": [
      {
        "description": "The text. Pass a string, or a highlighter's spans tagged `data-code-token` (the twelve Shiki createCssVariablesTheme token names) — the package colours them from the `--code-block-token-*` knobs, so the consumer never writes a colour.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "defaultValue": "true",
        "description": "Soft-wrap long lines (`white-space: pre-wrap; overflow-wrap: anywhere`). `false` keeps lines intact and scrolls horizontally.",
        "name": "wrap",
        "type": "boolean"
      },
      {
        "description": "Token presets, or one explicit CSS length such as {value:\"16rem\"}. Still keyboard scrollable.",
        "name": "maxHeight",
        "type": "\"sm\" | \"md\" | \"lg\" | \"none\" | { value: string }"
      },
      {
        "defaultValue": "\"sm\"",
        "description": "Type size: `sm` for bodies and snippets, `xs` for dense logs.",
        "name": "size",
        "type": "\"xs\" | \"sm\""
      },
      {
        "description": "Lands on `data-language`. No highlighter is bundled — bring your own and tag its output with `data-code-token` (gh#784).",
        "name": "language",
        "type": "string"
      },
      {
        "description": "antd Typography's `copyable`, same name and semantics (gh#1032): a copy button in the block's inline-end corner. `true` copies the block's text content (highlighter spans included); `text` overrides it. The block reserves the button's column, the confirmed state is announced politely, and `onCopy` fires only after the clipboard write succeeds.",
        "name": "copyable",
        "type": "boolean | { text?: string | (() => string | Promise<string>); onCopy?: (event) => void; tooltips?: ReactNode | [ReactNode, ReactNode] | false; icon?: ReactNode | [ReactNode, ReactNode]; format?: \"text/plain\" | \"text/html\"; tabIndex?: number }"
      },
      {
        "description": "Extra classes on the `pre`.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Prose - renders whole documents (Markdown, CMS bodies) and delegates its `pre` treatment to the same tokens; use Prose when the block sits inside rendered content, CodeBlock for a standalone block.",
      "Text - `as=\"code\"` is inline monospace for an identifier in a sentence, not a block.",
      "Descriptions - `Descriptions.Item mono` is a mono VALUE in a label/value row.",
      "CredentialReveal - a masked secret with copy; not for arbitrary text."
    ],
    "rules": [],
    "storyPath": "data-display/code-block.tsx",
    "tagline": "A block of preformatted text (request and response bodies, console output, snippets): mono, muted surface, soft-wrapped long lines by default, and an optional height cap that scrolls inside the block instead of pushing the page.",
    "usage": [
      "DO import from `@godxjp/ui/data-display`: `import { CodeBlock } from \"@godxjp/ui/data-display\";`",
      "DO give a block that can scroll (`maxHeight` or `wrap={false}`) an `aria-label`: it becomes a keyboard tab stop so the content is reachable without a mouse.",
      "DO cap the height of anything that can be large (a 64 KB response body, a console dump) with `maxHeight`; the page keeps its rhythm and the block scrolls.",
      "DON'T hand-roll `<pre className=\"max-h-64 overflow-auto rounded bg-muted p-2 whitespace-pre-wrap\">`: every one of those values is a copy of a token this component reads.",
      "DON'T reach for `Text as=\"code\"` for a block: that is inline monospace with no wrapping axis. Use `Text as=\"code\"` for an identifier inside a sentence, CodeBlock for a block.",
      "DO use `copyable` for a command or snippet the reader will paste (gh#1032). DON'T hand-roll a copy Button next to or over a CodeBlock: `copyable` owns the clipboard call, the confirmed state, the placement and the polite announcement.",
      "DON'T use CodeBlock for a single value in a Descriptions row: `Descriptions.Item mono` owns that (it breaks the value, not the row).",
      "SYNTAX COLOUR (gh#784): CodeBlock does not highlight, but it DOES own the palette. Tag each span from your highlighter with `data-code-token` — the twelve names are Shiki createCssVariablesTheme's verbatim (comment, keyword, string, string-expression, function, constant, parameter, punctuation, link, inserted, deleted, changed, plus the block foreground) — and the package colours them. DON'T put `style={{ color }}` or a palette `className` on the spans: both are visual overrides, and the colour is not the consumer's to choose. Retheme with the `--code-block-token-*-color` knobs, which are role-mirrors, so light and dark follow the theme with no second palette."
    ],
    "useCases": [
      "Request and response bodies of an HTTP trace (HAR) in a bug-report inbox: JSON up to 64 KB, lines up to 240 characters, wrapped and capped at `maxHeight=\"sm\"`.",
      "Console output attached to a bug report: `size=\"xs\"` for density, `maxHeight=\"md\"`.",
      "An API example in developer documentation: `language=\"bash\"` or `\"json\"`, no height cap.",
      "A stack trace in an error detail sheet: `wrap={false}` so frames stay on one line, scrolled horizontally."
    ]
  },
  {
    "docPath": "docs/data-display/markdown.tsx",
    "example": "import { Prose } from \"@godxjp/ui/data-display\";\nimport { Markdown } from \"@godxjp/markdown\";\n\n<Prose measure=\"narrow\">\n  <Markdown resolveUrl={(url) => (url.startsWith(\"asset:\") ? assetUrl(url) : undefined)}>\n    {page.body}\n  </Markdown>\n</Prose>",
    "group": "data-display",
    "importPath": "@godxjp/markdown",
    "name": "Markdown",
    "props": [
      {
        "description": "The Markdown source.",
        "name": "children",
        "required": true,
        "type": "string"
      },
      {
        "description": "Host markers (callouts, embeds), run after GFM. Their output is still sanitised — allow marker attributes with `schema`.",
        "name": "remarkPlugins",
        "type": "PluggableList"
      },
      {
        "description": "Tags / attributes a host plugin needs. A rule for an attribute the base already constrains is MERGED into it (e.g. `code` `className` + `math-inline`), gh#1116. It can never widen the URL policy or bring raw HTML back.",
        "name": "schema",
        "type": "{ tagNames?: string[]; attributes?: Record<string, …> }"
      },
      {
        "description": "Map a host scheme (`asset:…`, wiki links) to a real URL. Runs BEFORE the sanitiser, which then judges the result.",
        "name": "resolveUrl",
        "type": "(url, key) => string | undefined"
      },
      {
        "description": "Server-assigned anchors instead of GitHub slugs.",
        "name": "headingId",
        "type": "({ depth, text, index }) => string | undefined"
      },
      {
        "description": "Element overrides (react-markdown). Mermaid fences are drawn through the gate BEFORE a host `pre` is asked, so a host code renderer keeps Mermaid (gh#1116).",
        "name": "components",
        "type": "Components"
      },
      {
        "description": "Run AFTER the sanitiser — presentation only.",
        "name": "rehypePlugins",
        "type": "PluggableList"
      },
      {
        "description": "Narrow the elements a body may produce (with `unwrapDisallowed` to keep their text) — an activity feed showing only strong / em / del / code. Only removes; never allows what the schema strips (gh#1116).",
        "name": "allowedElements",
        "type": "string[]"
      },
      {
        "defaultValue": "24",
        "description": "A table cell whose trimmed text has no whitespace and at most this many characters (grapheme clusters) — a code, an id, a date, 優先度 — is stamped data-short, and Prose keeps it on one line (CJK otherwise breaks between ideographs in a narrow column). Sentences still wrap. false stamps nothing (gh#1150). Images in Prose table cells are capped at --prose-cell-image-max-inline-size (20rem) so a narrow table scrolls instead of squashing them.",
        "name": "shortCellLength",
        "type": "number | false"
      },
      {
        "description": "Default 'same-tab'. Renderer policy for links to another origin (an absolute http(s) URL whose origin differs from `origin`). 'new-tab' adds target=\"_blank\" rel=\"noopener noreferrer\"; relative links, #anchors, resolved wikilinks and mailto: stay in the tab. Markdown cannot store target, so this replaces it.",
        "name": "externalLinks",
        "type": "'same-tab' | 'new-tab'"
      },
      {
        "description": "The page origin externalLinks compares against; defaults to location.origin. Pass it for server rendering, or every absolute link counts as external there.",
        "name": "origin",
        "type": "string"
      },
      {
        "description": "Visible titles for GitHub-alert callouts (`> [!WARNING]`) that carry none of their own. English by default (the package has no i18n); pass localized strings. Grammar v1 (gh#1156): callouts, `:::toggle[summary]`, `::::columns`/`:::column{width=N}`, CJK-friendly emphasis — the same syntax the pure codec at `@godxjp/markdown/codec` (CODEC_ID kit-md@1, parse/serialize/normalize, DOM-free) and @godxjp/block-editor use.",
        "name": "calloutTitles",
        "type": "Partial<Record<'note' | 'tip' | 'important' | 'warning' | 'caution', string>>"
      },
      {
        "defaultValue": "true",
        "description": "`false` keeps ```mermaid fences as code. Diagrams need the optional peer `mermaid`.",
        "name": "mermaid",
        "type": "boolean"
      }
    ],
    "related": [
      "Prose — the typography wrapper this renders into.",
      "MarkdownEditor — editing; its preview is this renderer.",
      "CodeBlock — a single block of preformatted text, not a document."
    ],
    "rules": [],
    "storyPath": "data-display/markdown.tsx",
    "tagline": "The one Markdown renderer for GoDX apps (sibling package `@godxjp/markdown`, versioned with the kit): GFM + GitHub heading anchors, one sanitiser schema (no raw HTML; http/https/mailto/relative URLs only), and ```mermaid drawn only after its SVG passes a fail-closed gate (gh#1108). Wrap it in `Prose`.",
    "usage": [
      "DO let the document language do Japanese line breaking: under `:lang(ja)` (AppProvider sets `<html lang>`) every prose paragraph and list item breaks between PHRASES — `word-break: auto-phrase` + `line-break: strict`, the `Text break=\"phrase\"` behaviour (gh#1310) — so 「新しく参加」 is not cut 「新し｜く参加」. Latin and Vietnamese wrap unchanged; a phrase longer than the line still breaks inside itself. A browser without `auto-phrase` keeps its own CJK breaking.",
      "Table of contents: outline(markdown) from @godxjp/markdown/codec returns { depth, text, id }[] with exactly the ids <Markdown> writes — feed it to the kit Anchor. YAML front matter is kept verbatim and never rendered; frontmatterOf(markdown) returns it (gh#1163).",
      "DO `<Prose><Markdown>{body}</Markdown></Prose>` for every rendered body (wiki page, issue, mail).",
      "DO record `MARKDOWN_FORMAT` and `RENDERER_VERSION` on stored versions.",
      "DON'T assemble react-markdown + rehype-sanitize per app, add rehype-raw, or render Mermaid yourself — `checkMermaidSvg` is the gate."
    ]
  },
  {
    "docPath": "docs/data-entry/markdown-editor.tsx",
    "example": "import { MarkdownEditor } from \"@godxjp/editor\";\nimport { FormField } from \"@godxjp/ui/data-entry\";\n\n<FormField id=\"body\" label=\"本文\">\n  <MarkdownEditor\n    value={body}\n    onValueChange={setBody}\n    upload={async (file) => ({ url: await storage.put(file), name: file.name })}\n  />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/editor",
    "name": "MarkdownEditor",
    "props": [
      {
        "description": "Controlled Markdown text (with `onValueChange`).",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial text.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Every change: typing, toolbar, uploads, undo.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Controlled view (with `onModeChange`); `defaultMode` for uncontrolled. Default `write`.",
        "name": "mode",
        "type": "\"write\" | \"preview\" | \"split\""
      },
      {
        "description": "Stores a pasted / dropped / attached file; the HOST decides where. A placeholder becomes `![name](url)` (images) or `[name](url)`; a failure removes it and names the file.",
        "name": "upload",
        "type": "(file: File) => Promise<{ url: string; name?: string }>"
      },
      {
        "description": "Refuses files with this message, with or without `upload`; the attach button carries the reason and shows it instead of opening the picker (gh#1114).",
        "name": "uploadBlockedReason",
        "type": "string | null"
      },
      {
        "description": "Replace the preview (an app with its own embeds).",
        "name": "renderPreview",
        "type": "(value: string) => ReactNode"
      },
      {
        "description": "Extra toolbar actions — the extension point for new block types (`api`: value, selection, edit, insert, focus).",
        "name": "actions",
        "type": "{ key, label, icon, run(api) }[]"
      },
      {
        "description": "Override any string (defaults: the kit's ja / en / vi catalogue).",
        "name": "labels",
        "type": "Partial<MarkdownEditorLabels>"
      }
    ],
    "related": [
      "Textarea — plain multi-line text; this is Textarea plus Markdown tooling.",
      "Markdown — rendering a stored body."
    ],
    "rules": [],
    "storyPath": "data-entry/markdown-editor.tsx",
    "tagline": "Markdown editor (sibling package `@godxjp/editor`, versioned with the kit): the kit's Textarea with a WAI-ARIA formatting toolbar, write / preview / side by side (preview via `@godxjp/markdown`), IME-safe ⌘/Ctrl+B·I·K, one undo step per toolbar edit, and paste / drop / attach through a host `upload` (gh#1109).",
    "usage": [
      "DO put it inside `FormField` — `id` / `aria-*` reach the textarea.",
      "DO keep a host autocomplete (ChatSuggestion) by passing its `onKeyDown`: it runs first, and `preventDefault()` skips the editor's shortcut.",
      "DON'T build a toolbar over a bare Textarea, or a rich-text surface — this is the shared editor."
    ]
  },
  {
    "docPath": "docs/data-entry/block-editor.tsx",
    "example": "import { BlockEditor } from \"@godxjp/block-editor\";\nimport { FormField } from \"@godxjp/ui/data-entry\";\n\n<FormField id=\"body\" label=\"本文\">\n  <BlockEditor\n    value={body}\n    onValueChange={setBody}\n    upload={async (file, { signal }) => ({ url: await media.put(file, { signal }), name: file.name })}\n    suggestWikilinks={(q) => pages.search(q)}\n  />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/block-editor",
    "name": "BlockEditor",
    "props": [
      {
        "description": "Controlled Markdown body (with `onValueChange`).",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial body.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Canonical Markdown after each edit — `serialize(editor.getJSON())`, identical to `normalize(body)` from `@godxjp/markdown/codec` (CODEC_ID kit-md@1).",
        "name": "onValueChange",
        "type": "(markdown: string) => void"
      },
      {
        "description": "Stores a pasted / dropped / chosen file (return `asset:<id>` if you like). An inline placeholder shows progress; failure offers Retry / Remove; deleting the placeholder aborts `signal`. Images become `![name](url)`, other files a link.",
        "name": "upload",
        "type": "(file: File, options: { signal: AbortSignal }) => Promise<{ url: string; name?: string }>"
      },
      {
        "description": "The host's media library; the `/` Image row opens it instead of a file picker.",
        "name": "pickMedia",
        "type": "() => Promise<{ url: string; name?: string } | null>"
      },
      {
        "description": "Refuse files with this message.",
        "name": "uploadBlockedReason",
        "type": "string | null"
      },
      {
        "description": "`[[` suggestions from the host's page index; picking inserts `[[target|label]]`.",
        "name": "suggestWikilinks",
        "type": "(query: string) => WikilinkSuggestion[] | Promise<WikilinkSuggestion[]>"
      },
      {
        "description": "Renders a block embed `![[target]]`; default is a labelled placeholder.",
        "name": "renderEmbed",
        "type": "(target: string) => ReactNode"
      },
      {
        "description": "Resolves a stored image URL (`asset:<id>`) for display; the body keeps the original.",
        "name": "resolveUrl",
        "type": "(url: string) => string | undefined"
      },
      {
        "defaultValue": "\"framed\"",
        "description": "framed: a field's border, padding and focus colour, so the editor reads as an editor inside a form; plain: the borderless Notion-style canvas for a full-page editor.",
        "name": "appearance",
        "type": "\"framed\" | \"plain\""
      },
      {
        "description": "Extra `/` rows ({ key, label, icon, keywords?, run(api) }) — `api.insertMarkdown` inserts through the codec.",
        "name": "actions",
        "type": "BlockEditorAction[]"
      },
      {
        "description": "Override any string (defaults: kit ja / en / vi).",
        "name": "labels",
        "type": "Partial<BlockEditorLabels>"
      },
      {
        "description": "Not editable; the body still renders as Prose.",
        "name": "disabled / readOnly",
        "type": "boolean"
      }
    ],
    "related": [
      "MarkdownEditor — a Textarea with Markdown tooling, for raw-Markdown editing.",
      "Markdown — rendering a stored body (same grammar, renderer v4)."
    ],
    "rules": [],
    "storyPath": "data-entry/block-editor.tsx",
    "tagline": "Notion / note.com style block editor (sibling package `@godxjp/block-editor`, versioned with the kit) that reads and writes MARKDOWN through `@godxjp/markdown/codec`: what onValueChange returns is byte-for-byte normalize(body). `/` block menu at the caret, ⋮⋮ handle (drag, turn into, duplicate, move, delete), floating format toolbar, `[[` wikilink suggestions, paste / drop uploads through the host, IME-safe, ja / en / vi (gh#1156).",
    "usage": [
      "From 40rem up the + / ⋮⋮ handles sit in a start gutter INSIDE the frame, `--block-editor-gutter-inline-size` (default 2 × 24px targets + 8px = 56px); it tunes that width but cannot put the text on a sibling Input's keyline, because the handles live in it (DESIGN.md Known Gaps, gh#1310). Below 40rem the handle gutter collapses to `--block-editor-compact-gutter-inline-size` (the frame inset, 12px) and the + / ⋮⋮ handle rises above the caret's block at its inline end (ADR C05): first line 56px → 12px from the inner edge at 390px. Touch: tap a block to show its handle; ⋮⋮ opens Move up / Move down / Duplicate / Delete. Keyboard: Mod+/ opens the same menu, Alt+Shift+↑↓ moves the block.",
      "Clicking a link (or Mod+K) opens a settings drawer: text, URL, title, open / remove. Clicking an image opens one for src, alt, title, replace (pickMedia / upload) and remove. Markdown stores no link target, so there is no target field. A link in the editor never navigates mid-edit.",
      "YAML front matter shows as a Properties block at the top and round-trips verbatim; read it with frontmatterOf() from @godxjp/markdown/codec.",
      "DO store the Markdown it emits — it is already canonical; compute `normalize()` server-side from the same codec to verify.",
      "DO give it a name: wrap in `FormField` or pass `aria-label` / `aria-labelledby` (the editable is a named `textbox`).",
      "DO pass `upload` (and `pickMedia` for a library) instead of handling paste yourself.",
      "DON'T render Markdown into it via HTML — pass `value`; HTML paste from other apps is handled by the schema.",
      "Keyboard: `/` menu (↑↓ Enter Esc), Alt+Shift+↑/↓ moves a block, Mod+/ opens the block menu, Markdown shortcuts (`# `, `- `, `> `, ```` ``` ````, `---`)."
    ]
  },
  {
    "docPath": "docs/data-entry/rich-text-editor.tsx",
    "example": "import { RichTextEditor } from \"@godxjp/block-editor\";\nimport { FormField } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\" required error={errors.description}>\n  <RichTextEditor value={description} onValueChange={setDescription} />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/block-editor",
    "name": "RichTextEditor",
    "props": [
      {
        "description": "Controlled HTML body (with `onValueChange`). Read through the schema: script, links, images, attributes, styles, h4–h6, tables are dropped to their text.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial HTML.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "The schema's HTML after each edit — `\"\"` for an empty body, never `<p></p>`. A new `value` from outside (a reset, a locale tab) replaces the document WITHOUT calling it, and never during an IME composition.",
        "name": "onValueChange",
        "type": "(html: string) => void"
      },
      {
        "description": "Shown while the body is empty.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Validation state the frame paints, as on Input; FormField's error arrives as `aria-invalid`.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "BCP-47 tag of the body's language, on the editable.",
        "name": "lang",
        "type": "string"
      },
      {
        "description": "Override any toolbar string (defaults: kit ja / en / vi, `blockEditor.*`).",
        "name": "labels",
        "type": "Partial<RichTextEditorLabels>"
      },
      {
        "description": "Not editable. readOnly drops the toolbar; disabled keeps it, disabled.",
        "name": "disabled / readOnly",
        "type": "boolean"
      },
      {
        "description": "Land on the editable `textbox` — FormField's id, label, helper, required and error wiring reach it directly.",
        "name": "id / aria-*",
        "type": "string"
      }
    ],
    "related": [
      "TranslatableRichText — the same editor holding one HTML value per locale.",
      "BlockEditor — the Markdown block editor in the same package.",
      "MarkdownEditor — Markdown in a Textarea."
    ],
    "rules": [],
    "storyPath": "data-entry/rich-text-editor.tsx",
    "tagline": "A form field that edits HTML (sibling package `@godxjp/block-editor`, gh#1365): a fixed toolbar — bold, italic, strike, inline code · H1–H3, bulleted / numbered list, quote · undo, redo — over a Tiptap surface whose SCHEMA is the sanitiser. `value` / `onValueChange` carry HTML holding only p, br, h1–h3, ul, ol, li, blockquote, strong, em, s, code (`RICH_TEXT_ALLOWED_TAGS`), with one attribute in all (an integer `start` on `<ol>`); anything else in `value` or a paste is kept as its text. For content already stored as HTML; new content belongs in Markdown (BlockEditor / MarkdownEditor).",
    "usage": [
      "DO wrap it in FormField, like any field: the label names the editable, `required` and the error reach it as `aria-required` / `aria-invalid` + `aria-errormessage`.",
      "DO store what onValueChange returns: it is already sanitised by the schema, and render it as HTML. Sanitise again on the server if other clients can write the same column.",
      "DO use TranslatableRichText for a per-locale `{ ja, en, vi }` map instead of wrapping Tabs around it.",
      "DON'T pick it for NEW content: Markdown (BlockEditor, MarkdownEditor) is the kit's storage format; this exists for columns that already hold HTML.",
      "Keyboard: each toolbar group is one tab stop with arrow keys; ⌘/Ctrl+B / I / Z / Shift+Z and the Markdown input rules (`# `, `- `, `1. `, `> `) work in the body."
    ],
    "useCases": [
      "Product, material or recipe description stored as HTML and rendered by a storefront",
      "Notice or email body column that already holds HTML"
    ]
  },
  {
    "docPath": "docs/data-entry/rich-text-editor.tsx",
    "example": "import { TranslatableRichText } from \"@godxjp/block-editor\";\nimport { FormField, type TranslatableValue } from \"@godxjp/ui/data-entry\";\n\nconst [description, setDescription] = React.useState<TranslatableValue>({ ja: \"<p>醤油ラーメン</p>\" });\n\n<FormField label=\"説明\" required>\n  <TranslatableRichText\n    locales={[\"ja\", \"en\", \"vi\"]}\n    fallbackLocale=\"ja\"\n    value={description}\n    onValueChange={setDescription}\n    errors={{ vi: \"ベトナム語の説明を入力してください\" }}\n  />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/block-editor",
    "name": "TranslatableRichText",
    "props": [
      {
        "description": "Controlled per-locale HTML, keyed by BCP-47 tag. A missing key or `\"\"` is an empty locale.",
        "name": "value",
        "type": "TranslatableValue"
      },
      {
        "description": "Uncontrolled initial per-locale HTML.",
        "name": "defaultValue",
        "type": "TranslatableValue"
      },
      {
        "description": "The WHOLE map after an edit; only the active locale's HTML changes.",
        "name": "onValueChange",
        "type": "(value: TranslatableValue) => void"
      },
      {
        "description": "Tab order. Default: AppProvider `contentLocales` when set, else the registered locales.",
        "name": "locales",
        "type": "readonly string[]"
      },
      {
        "description": "First tab. Default: the first of `contentLocales` when in `locales`, else the UI locale.",
        "name": "defaultLocale",
        "type": "string"
      },
      {
        "description": "Whose text an empty locale shows as its placeholder. Default: AppProvider `fallbackLocale`.",
        "name": "fallbackLocale",
        "type": "string"
      },
      {
        "description": "Per-locale messages: tab marks for each (glyph + screen-reader text), `aria-invalid` and FormField's error line for the active one.",
        "name": "errors",
        "type": "Partial<Record<string, ReactNode>>"
      },
      {
        "description": "The tab strip's tier, as on TranslatableTextarea.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "As on RichTextEditor; `ref` is the editor's handle.",
        "name": "placeholder / disabled / readOnly / labels / id",
        "type": "—"
      }
    ],
    "related": [
      "RichTextEditor — the single-locale editor it composes.",
      "TranslatableTextarea — the same contract over plain text.",
      "TranslatableField — the engine, for another control."
    ],
    "rules": [],
    "storyPath": "data-entry/rich-text-editor.tsx",
    "tagline": "ONE RichTextEditor holding an HTML value per locale (`{ ja, en, vi }`), switched by locale tabs (gh#1365). The kit's TranslatableField engine — the same as TranslatableInput / TranslatableTextarea: locales and first tab from AppProvider `contentLocales`, fallback from its `fallbackLocale`, per-locale error marks, the active locale's error in FormField. An empty locale shows the fallback locale's TEXT (not its HTML) as its placeholder; switching tabs swaps the HTML without calling onValueChange.",
    "usage": [
      "DO set `<AppProvider contentLocales={[\"ja\", \"en\", \"vi\"]}>` once; the field then needs no `locales` and opens on the source language.",
      "DO wrap it in FormField and pass your validator's per-locale messages as `errors` (Laravel `description.vi` → `errors.vi`).",
      "DON'T build Tabs + RichTextEditor yourself: the tab↔panel ARIA, lang, error marks and FormField report are the TranslatableField engine's."
    ],
    "useCases": [
      "Product / material / recipe description stored as per-locale HTML (Astrotomic / Spatie translatable)"
    ]
  },
  {
    "example": "import { Prose } from \"@godxjp/ui/data-display\";\nimport Markdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\n\n<Prose size=\"sm\">\n  <Markdown remarkPlugins={[remarkGfm]}>{issue.description}</Markdown>\n</Prose>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Prose",
    "props": [
      {
        "defaultValue": "\"md\"",
        "description": "Body size. `md` is the page body size (a wiki page); `sm` is the compact step (an issue description, a comment).",
        "name": "size",
        "type": "\"sm\" | \"md\""
      },
      {
        "defaultValue": "\"fit\"",
        "description": "`fit` scales images to the column; `original` shows them at their authored size and the container scrolls horizontally.",
        "name": "imageSize",
        "type": "\"fit\" | \"original\""
      },
      {
        "description": "Reading line length (gh#1112): caps the inline size at the `--page-measure-*` token — the same vocabulary and tokens as `Flex measure` / `PageContainer measure` (narrow 42rem, medium 48rem sit in the 45–75 character band). A cap, not a centred column: the body keeps its start edge under its heading. Use it on a document/wiki/decision body instead of narrowing the whole page column; title and metadata stay full width. Unset = full width.",
        "name": "measure",
        "type": "\"narrow\" | \"medium\" | \"wide\""
      },
      {
        "defaultValue": "false",
        "description": "Open the body's images in the kit image preview: click, Enter or Space on an image opens it, paging through the body's other previewable images in document order. Works on any HTML the body renders (@godxjp/markdown output included) with no img override; an image inside a link stays the link's and a kit Image keeps its own preview (gh#1150).",
        "name": "imagePreview",
        "type": "boolean"
      },
      {
        "description": "The rendered content.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Extra classes on the container.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "CodeBlock - a standalone block of preformatted text; Prose delegates its `pre` to the same tokens.",
      "Text / Heading - UI copy, not a document.",
      "LegalDocumentShell - a whole legal document with its own table of contents and section anchors; use Prose for the body of arbitrary rendered content.",
      "ScrollArea - if a very long document needs its own scroll viewport, wrap Prose in ScrollArea."
    ],
    "rules": [],
    "storyPath": "data-display/prose.tsx",
    "tagline": "Typography for rendered content (Markdown, CMS bodies, issue descriptions): styles the semantic HTML inside it from the tokens, with no opinion about where the HTML comes from. To RENDER Markdown, use the sibling package `@godxjp/markdown` inside it — `<Prose><Markdown>{body}</Markdown></Prose>` — the one GFM renderer + sanitiser + Mermaid gate shared by every GoDX app; do not assemble react-markdown + rehype-sanitize per app (gh#1108). Tables: `@godxjp/markdown` wraps each in `.ui-prose-table-scroll`, which Prose scrolls horizontally (focusable only while it overflows), and table cells keep their words whole — a short code like `F02` never breaks (gh#1131).",
    "usage": [
      "DO let the document language do Japanese line breaking: under `:lang(ja)` (AppProvider sets `<html lang>`) every prose paragraph and list item breaks between PHRASES — `word-break: auto-phrase` + `line-break: strict`, the `Text break=\"phrase\"` behaviour (gh#1310) — so 「新しく参加」 is not cut 「新し｜く参加」. Latin and Vietnamese wrap unchanged; a phrase longer than the line still breaks inside itself. A browser without `auto-phrase` keeps its own CJK breaking.",
      "DO import from `@godxjp/ui/data-display`: `import { Prose } from \"@godxjp/ui/data-display\";`",
      "DO wrap the OUTPUT of your Markdown pipeline (react-markdown + remark-gfm + rehype-sanitize), a sanitised CMS string via `dangerouslySetInnerHTML`, or plain JSX. Prose styles descendants: h1..h4, p, ul/ol/li, blockquote, code, pre, table/th/td, a, img, hr.",
      "DO keep soft line breaks the pipeline's job (remark-breaks or the renderer's `breaks` option). Prose does not turn single newlines into `<br>`; it is typography, not parsing.",
      "DON'T restate the heading scale, table cell measures or list rhythm with `[&_h1]:text-lg [&_td]:border …` utilities: they copy token values and never follow a retune. Prose reads --heading-h1..h4, --table-cell-padding-* and --prose-* directly. Its long-form scale keeps every level h1–h6 at or above body size: h1–h3 step down in size, h4–h6 sit at body size in bold (h6 muted), so a host that shifts body headings down one level (page title = the one h1) keeps readable section titles.",
      "DO tune the link through --prose-link-color and --prose-link-decoration-line (gh#717). The ink defaults to hsl(var(--primary)) resolved at the anchor, so a scoped [data-tenant] re-tint reaches it; the resting underline defaults to `underline` and is a SEPARATE knob from --text-link-decoration-line, because Prose is running text and there the underline is a WCAG 1.4.1 requirement, not a taste.",
      "DO style a state your own renderer knows about by marking the anchor and selecting it: `a[data-unresolved]` (a wiki link whose target does not exist yet), then re-declare --prose-link-color inside that selector. Prose writes data-* on its ROOT ONLY — never on a descendant — so every data-* on an `a` inside it is yours and stays selectable. That is a promise held by a test (prose-link-717.test.tsx), not a coincidence; there is no prop for it, the same way `TableRow data-expanded-row` is an attribute rather than an API.",
      "DON'T sanitise inside Prose: it renders whatever HTML it is given. Sanitise before (rehype-sanitize, or the server).",
      "DON'T use Prose for UI text (labels, descriptions, empty states): those are Text / Heading. Prose is for a document."
    ],
    "useCases": [
      "A wiki page rendered from Markdown, at the page body size.",
      "A wiki page whose renderer marks links it knows something extra about: `<a data-unresolved=\"true\">` for a target nobody has written yet, re-declaring --prose-link-color as --text-error inside `a[data-unresolved]` so it reads differently from a link that resolves.",
      "An issue description or a comment in a tracker, `size=\"sm\"`.",
      "A bug-report intake inbox that renders the report body (description, steps, environment table) sent by a browser extension.",
      "A CMS article body delivered as sanitised HTML.",
      "An email preview rendered from stored HTML."
    ]
  },
  {
    "example": "import { Timeline } from \"@godxjp/ui/data-display\";\nimport { ArrowRightLeft, MessageSquare } from \"lucide-react\";\n\n// Default icon variant\n<Timeline items={[\n  { title: \"注文受付\", time: \"2024-06-01 10:00\" },\n  { title: \"発送準備中\", time: \"2024-06-01 14:00\" },\n  { title: \"配送中\", current: true },\n]} />\n\n// Numbered route stepper (Pattern A)\n<Timeline variant=\"ordinal\" items={[\n  { title: \"集荷\", location: \"東京 → 名古屋\", status: \"pending\" },\n  { title: \"幹線輸送\", location: \"名古屋 → 大阪\", status: \"pending\" },\n]} />\n\n// Activity log / issue history: neutral dots, no status prefix (gh#1092)\n<Timeline density=\"compact\" items={[\n  { title: \"Aki がステータスを変更 未対応 → 処理中\", time: \"10/01 08:12\", status: \"log\", icon: ArrowRightLeft },\n  { title: \"Ken がコメント\", time: \"10/01 09:30\", status: \"log\", icon: MessageSquare },\n]} />\n\n// Status tracker (Pattern B): done → check, current → dot, pending → number\n<Timeline variant=\"status\" items={[\n  { title: \"請求書を発行\", status: \"done\" },\n  { title: \"承認待ち\", status: \"current\" },\n  { title: \"消費税を計上\", status: \"pending\" },\n]} />",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Timeline",
    "props": [
      {
        "description": "Array of `{ title, location?, time?, note?, current?, status?, icon? }`. `status` is 'done' | 'current' | 'pending' | 'log'; `current: true` is shorthand for `status: 'current'`. 'log' (gh#1092) is an activity-log / history row: the neutral dot + hairline rail `pending` draws, and NO sr-only status prefix (antd's `color=\"gray\"` item). `icon` (a LucideIcon) overrides the auto-glyph for that item.",
        "name": "items",
        "required": true,
        "type": "TimelineItem[]"
      },
      {
        "description": "Rail glyph strategy (default 'icon'). 'icon' = legacy look (Plane for current, CheckCircle2 otherwise). 'ordinal' = every glyph is its 1-based step number; status sets colour only. 'status' = glyph by status (done → check, current → filled dot, pending → step number).",
        "name": "variant",
        "type": "\"icon\" | \"ordinal\" | \"status\""
      },
      {
        "description": "Item spacing (default 'default'). Sets `--timeline-item-gap` — the space between items and between the rail and the body. 'default' = `--space-stack-md` (16px); 'compact' = `--space-stack-sm` (8px, retune with `--timeline-item-gap-compact`) for a dense activity log / issue history. antd's Timeline has no density prop (one fixed `itemPaddingBottom`, 20px), so this is a godx extension.",
        "name": "density",
        "type": "\"default\" | \"compact\""
      }
    ],
    "related": [
      "Steps — use Steps (navigation group) when the user must actively progress through a wizard (interactive, shows step numbers/status, horizontal layout by default); use Timeline for read-only historical event sequences that have already happened.",
      "Descriptions — use Descriptions to display a flat set of label/value metadata fields (e.g., invoice header); use Timeline when events are ordered chronologically and a connector rail communicates sequence and progress.",
      "DataTable — use DataTable for multi-row, multi-column tabular event logs where sorting, filtering, and pagination are needed; use Timeline when the sequence/rail visual is the primary communication and there are fewer than ~10 events.",
      "Badge — Badge is a single-item inline indicator; Timeline sequences multiple statuses with connectors. Compose Badge inside a Timeline `title` or `note` field for richer per-event context, but do not replace Timeline with a stack of Badges."
    ],
    "rules": [],
    "storyPath": "data-display/timeline.tsx",
    "tagline": "Vertical event list with an icon rail. Current item gets a highlighted glyph. `variant` switches the rail to numbered (ordinal) or status-driven glyphs, and each item carries a `status` (done/current/pending, or `log` for an activity-log entry with no status semantics) plus an optional per-item `icon`. The whole progress column is ONE hue: done dot, current dot and travelled line all paint `--primary`, exactly as `Steps` paints `finish` and `process`.",
    "usage": [
      "DO pass an array of `TimelineItem` objects to `items`. Each item is `{ title, location?, time?, note?, current?, status?, icon? }`. All fields except `title` are optional. The only other props are `variant` and `density`.",
      "DO mark the in-progress event with `current: true` (or `status: 'current'`). In the default `variant='icon'`, the current item renders a `Plane` icon and every other item a `CheckCircle2`. Use `variant='ordinal'` for a numbered route stepper (1,2,3…) or `variant='status'` for a done→check / current→dot / pending→number tracker. Pass `status: 'done' | 'current' | 'pending'` per item to drive colour and (in the status variant) the glyph; pass `icon` (a LucideIcon) to override the glyph for a single item.",
      "DO pass `ReactNode` to `title`, `location`, `time`, and `note` — you can embed formatted text, `<Badge>`, `<Badge>`, or `<span>` inside those fields. Use `formatDate` to pre-format timestamps before passing them as `time`.",
      "DO NOT hand-roll a vertical event list with divs, icons, and connector lines — that is exactly what Timeline ships. Do not apply extra padding or wrapping outside the component; it manages its own rail and spacing internally.",
      "DO pass `density=\"compact\"` when the timeline is a long, dense log (issue history, audit trail) and the default 16px between items reads too airy — it drops the item gap to `--space-stack-sm` (8px). Never tighten it with your own padding/margin on the items.",
      "DO NOT use Timeline for user-facing wizard progress (steps the user must complete in order) — use `Steps` for that. Timeline is read-only historical/status display; it has no interactive state, no `onClick`, and no concept of 'go to step'.",
      "DO wrap Timeline in `<CardContent>` when placing it inside a `Card` — bare `Card` has no inner padding, so the rail will render flush against the card edge without `CardContent`.",
      "THE PROGRESS COLUMN IS ONE HUE (gh#731). The done dot, the current dot and the travelled line all default to `hsl(var(--primary))` — the same contract `Steps` uses for `finish` + `process`, and antd's (only `error` leaves the primary hue). The current item is told apart by a ring (`--timeline-dot-current-ring-width`, `--steps-dot-process-ring-width`'s value), by `aria-current=\"step\"` + a localized sr-only prefix, and by a different glyph — never by a second colour role. Re-theme `--primary` and the whole rail follows. Measured light: done 2.18:1 → 6.31:1 against the card, glyph 6.31:1 on the fill; dark 7.22:1 → 9.84:1, glyph 10.72:1.",
      "DO use `status: 'log'` for every row of an activity log / issue history / audit trail (gh#1092). Those rows already HAPPENED and are not steps: `done` paints them primary and prefixes 「Completed:」, `pending` reads 「Upcoming:」 to a screen reader. `log` draws the quiet neutral dot (a plain pip, or the item's `icon`) and hairline rail, with no prefix — antd's gray item.",
      "DON'T expect `--success` on a `status: 'done'` item — `done | current | pending` is POSITION IN A SEQUENCE, not status, and Timeline has no `tone` axis. Put a real status on the item's `title` or `note` with a `<Badge tone=\"success\">`, which is the surface that owns the status green.",
      "TO RESTORE the pre-27.9 green/violet pairing, set the done dot's fill AND its ink together in your theme (global or `[data-tenant]`) — the two roles have opposite ink polarity, so the fill alone leaves a near-white glyph on 若竹 green at 2.18:1 (gh#643): `--timeline-dot-done-background: hsl(var(--success)); --timeline-dot-done-foreground: hsl(var(--success-foreground));`. `--timeline-dot-current-background` and `--timeline-line-completed-background` retint the other two surfaces the same way."
    ],
    "useCases": [
      "Shipment / delivery tracking — showing a parcel's journey through 'Order placed → Packed → In transit → Delivered' with timestamps and a current-stop indicator.",
      "Accounting document audit trail — rendering the lifecycle of an invoice or payment (Draft → Submitted → Approved → Paid) with the current approval stage highlighted.",
      "Support ticket / task history — displaying a chronological log of status transitions (Open → Assigned → In Review → Closed) with agent names in the `note` field and timestamps in `time`.",
      "MF sync log viewer — listing each sync run event (OAuth refresh, fetch, upsert) with timestamps and record counts so an operator can see what the last sync did.",
      "Approval workflow status panel — showing a multi-stage approval chain where completed stages have CheckCircle2 icons and the pending stage has the Plane (in-flight) icon.",
      "Order / purchase-order lifecycle in an admin detail page — placed alongside a `Descriptions` summary at the top of a `Card` to give a compact at-a-glance history."
    ]
  },
  {
    "example": "import { TimelineGrid } from \"@godxjp/ui/lab\";\n\n<TimelineGrid\n  label=\"週シフト 5月11日〜17日\"\n  start=\"06:00\"\n  end=\"24:00\"\n  interval={2}\n  now=\"14:35\"\n  columns={[\n    { id: \"05-13\", label: \"水 13\" },\n    { id: \"05-14\", label: \"木 14\", current: true },\n  ]}\n  events={[\n    { id: \"a\", columnId: \"05-14\", start: \"09:00\", end: \"17:30\", title: \"早番\", description: \"田中\" },\n    { id: \"b\", columnId: \"05-14\", start: \"13:00\", end: \"22:00\", title: \"遅番\", description: \"高橋\" },\n    { id: \"c\", columnId: \"05-14\", start: \"22:00\", end: \"06:00\", title: \"夜勤\", description: \"伊藤\" },\n  ]}\n  onEventSelect={(event) => openShift(event.id)}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/lab",
    "name": "TimelineGrid",
    "props": [
      {
        "description": "Accessible name of the grid. Required, and a plain string: the grid is a focusable scrolling region, so its name has to survive as an `aria-label`.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "description": "`{ id, label, description?, current? }` in render order. `label` is the column head AND the accessible name of that column's event list, so keep it text. `current: true` marks today's column: it takes the tint and hosts the `now` marker.",
        "name": "columns",
        "required": true,
        "type": "TimelineGridColumnProp[]"
      },
      {
        "description": "`{ id, columnId, start, end, title, description?, color? }` in any order. `start`/`end` are clock times in the column's own day, `\"HH:MM\"` 24-hour (`\"24:00\"` = end of day). An `end` at or before `start` continues into the next day (22:00–06:00 夜勤). `color` is the record's own colour, washed like `Badge color`.",
        "name": "events",
        "required": true,
        "type": "TimelineGridEventProp[]"
      },
      {
        "defaultValue": "the earliest event, on the hour",
        "description": "First clock time on the axis, `\"HH:MM\"`. Left off, the axis is derived from the events, so a block can only fall outside an axis you PINNED.",
        "name": "start",
        "type": "string"
      },
      {
        "defaultValue": "the latest event, on the hour",
        "description": "Last clock time on the axis, `\"HH:MM\"`. A block the pinned axis cuts is drawn to the edge and carries `data-clipped`, and still prints its real range as text.",
        "name": "end",
        "type": "string"
      },
      {
        "defaultValue": "1",
        "description": "Hours between hour rules and axis labels. Use 2 or 3 when a long axis has to fit one screen.",
        "name": "interval",
        "type": "number"
      },
      {
        "description": "Current clock time, `\"HH:MM\"`. Draws the now marker in every column marked `current`. The marker is decorative (`aria-hidden`): the hours it points at are already text on every block.",
        "name": "now",
        "type": "string"
      },
      {
        "description": "Block click handler. Its PRESENCE turns every block into a real `button`, which is what makes the blocks keyboard-reachable; without it the grid is a read-only board.",
        "name": "onEventSelect",
        "type": "(event: TimelineGridEventProp) => void"
      },
      {
        "description": "Extra classes on the scroll region.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "DOM id on the scroll region.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "Timeline — one lane of ordered events with no scale (an audit trail, a shipment history). TimelineGrid is the same family with a measured time axis and one lane per column.",
      "Table — the MONTH calendar is a real table (`bordered`, one row per week, one cell per day), not a TimelineGrid.",
      "Calendar — the single-date PICKER in data-entry. Use it to jump to a month; it does not show events.",
      "Sheet — the usual destination of `onEventSelect`: the block is the affordance, the drawer is the detail.",
      "Badge — the same `color` wash for a record's own colour, on a chip instead of a block."
    ],
    "rules": [
      42,
      45
    ],
    "storyPath": "data-display/timeline-grid.tsx",
    "tagline": "The time-axis half of the Timeline family: a vertical hour axis, one column per day (or room, or machine) and event blocks placed by start time and duration. Overlapping events are laid out side by side automatically. NOT a calendar — no month view, no navigation, no drag-to-create, no recurrence, no timezone conversion.",
    "tier": "lab",
    "usage": [
      "DO import from `@godxjp/ui/lab` (the opt-in lab subpath): `import { TimelineGrid } from \"@godxjp/ui/lab\";`",
      "DO give a shift/booking board one column per day and let the grid place the blocks. DON'T hand-roll a `relative` container with absolutely-positioned divs and percentage offsets — that is the exact shape this primitive replaces (#354 item 7), and it hides one of two overlapping shifts.",
      "DO re-tune the rhythm with the tokens instead of a className: `--timeline-grid-hour-height` is the height of one hour (the knob that fits a 24-hour axis on one screen), `--timeline-grid-column-min-width` is the floor below which the grid scrolls instead of collapsing, `--timeline-grid-axis-width` is the hour rail, and `--timeline-grid-event-color` / `--timeline-grid-now-color` / `--timeline-grid-current-tint` are role-mirror colour knobs.",
      "DO express a shift that crosses midnight as `start: \"22:00\", end: \"06:00\"` — an `end` at or before `start` means the next day. The block is drawn to the end of the window and marked `data-clipped`, and the text still reads 22:00–06:00.",
      "DON'T reach for TimelineGrid for a MONTH calendar: a month grid is a real `Table`, one `TableRow` per week and one `TableCell` per day. TimelineGrid owns the continuous time axis, not the day matrix.",
      "DON'T expect navigation, drag-to-create, recurrence or timezone conversion — none of it ships. `start`/`end` are clock times in the column's own day; which day a column stands for is yours to decide.",
      "DON'T pass an event whose `columnId` matches no column, or one that falls entirely outside a pinned `start`/`end`: it is not drawn. Leave the axis unpinned and it is derived from the events instead."
    ],
    "useCases": [
      "A weekly shift board (シフトカレンダー): seven day columns on a 06:00–24:00 axis, 早番 / 遅番 / 夜勤 blocks in the decorative wa-iro palette, `now` drawing the current-time line on today's column.",
      "A day view of one team: one column per staff member, `interval={1}`, `onEventSelect` opening the shift detail in a Sheet.",
      "Meeting-room booking for a single day: one column per room, the axis derived from the bookings themselves, overlapping holds side by side so a double-booking is visible instead of hidden.",
      "Machine / bay utilisation on a shop floor: one column per machine, blocks coloured by job, the clipped marker showing a run that continues past the shift window."
    ]
  },
  {
    "example": "import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from \"@godxjp/ui/data-display\";\n\n<Table>\n  <TableHeader><TableRow><TableHead>項目</TableHead><TableHead numeric>金額</TableHead></TableRow></TableHeader>\n  <TableBody>\n    <TableRow><TableCell>送料</TableCell><TableCell numeric>¥500</TableCell></TableRow>\n    <TableRow><TableCell indent={1}>うち離島加算</TableCell><TableCell numeric>¥200</TableCell></TableRow>\n    <TableRow><TableCell flush colSpan={2}><ShippingBreakdown /></TableCell></TableRow>\n  </TableBody>\n</Table>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Table",
    "props": [
      {
        "defaultValue": "false",
        "description": "TableRow only. The whole ROW is the target: pointer cursor, a hover that means \"clickable\" rather than merely \"hovered\", and (via focus-ring.css) the design-system focus ring for the real <a>/<button> inside a cell. PRESENTATION ONLY — it binds no handler and deliberately adds no tabindex/role: a <tr> is not a control, and the thing a keyboard user reaches must stay a real control inside the row. Pair it with your own onClick on the row and keep a genuine link/button in the first cell. Reach for it when a design calls for \"a list with COLUMNS whose rows are selectable\" — ListRow gets all of this free by wrapping its own <a>, which a <tr> cannot do, and DataTable solves a different problem (it brings a toolbar and pagination many designs do not have). gh#929.",
        "name": "interactive",
        "type": "boolean"
      },
      {
        "description": "PER-INSTANCE column measures for preset=\"action-collection\", in place of re-pointing its --table-action-collection-* knobs from a consumer stylesheet. Those knobs are global by design, and that is the problem: two collections on one screen do not share a column budget — a console that widened `actions` globally so a Japanese status badge would stop breaking to one character per line (an SC 1.4.10 reflow failure) collapsed a sibling table's name column to ~15px in the same change. Emitted as inline custom properties, the same contract Flex `width` uses for a call-site measurement, leaving data-column-widths on the DOM so each escape stays countable.",
        "name": "columnWidths",
        "type": "{ actions?: string; actionsCompact?: string; metaCompact?: string; minInlineSizeCompact?: string }"
      },
      {
        "description": "On TableHead/TableCell: logical text alignment.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "description": "On TableHead/TableCell: tabular figures and end alignment; explicit align wins.",
        "name": "numeric",
        "type": "boolean"
      },
      {
        "description": "On TableHead/TableCell: allow text to wrap.",
        "name": "wrap",
        "type": "boolean"
      },
      {
        "description": "On TableHead/TableCell: column measure; numbers mean CSS pixels.",
        "name": "width",
        "type": "WidthProp"
      },
      {
        "description": "TableHeader / TableBody composition.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Extra classes on the table element.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Whether Table owns its own horizontal-scroll region (default true). Leave true for a standalone table so a table wider than its container scrolls in a keyboard-reachable wrapper. Set false only when an ancestor already provides the scroll region (DataTable does) to avoid a redundant nested scroller + duplicate keyboard tab stop.",
        "name": "scrollable",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw the full cell GRID: an outer frame plus vertical rules between columns (the horizontal row rules already come from TableRow). Reach for it whenever the table carries rowSpan/colSpan merged cells — without column rules the merge relationships are unreadable. Colour comes from --table-border-color (default --border). Default false emits nothing.",
        "name": "bordered",
        "type": "boolean"
      },
      {
        "description": "Zebra rows: every EVEN LOGICAL body row paints --table-row-striped-background (default --muted at 0.8 alpha — every text role on it stays at AA). Mark a hand-composed detail row `<TableRow data-expanded-row=\"\">` and it is skipped when counting and wears its record's stripe. OMIT to inherit the theme default (`--table-row-striped-alpha`, 0% unless the service set it); `true` emits data-striped=\"\" (100%), `false` emits data-striped=\"false\" (0%) for this table only.",
        "name": "striped",
        "type": "boolean"
      },
      {
        "description": "On TableRow (gh#876): the row's STATE — a leading-edge rail plus a weak wash, the SAME six tones and meanings `DataTable rowTone` already paints. Writes `data-tone`, the attribute the paint is keyed on — `<TableRow data-tone=\"warning\">` still works unchanged, `tone` is just the typed, discoverable route to it. Never the only signal (WCAG 1.4.1): keep the reason in a cell (a Badge, a status column) and let the rail make that cell findable. DO NOT reach for a `bg-<status>/…` utility on a TableRow instead — that bypasses the token-owned wash and the theme can no longer retune it (gh#872).",
        "name": "tone",
        "type": "\"primary\" | \"success\" | \"warning\" | \"info\" | \"attention\" | \"destructive\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Named collection contract. \"default\" emits no attribute and keeps the plain table. \"action-collection\" is the canonical dense approval/action queue: the desktop INTRINSIC column widths (which make a five-column queue wider than its card and force a horizontal scroll at 390) are replaced by table-layout: fixed plus the token-owned column PRIORITY measures (--table-action-collection-*), and cells wrap. Mark each column with `priority` on its TableHead AND its TableCell. Semantics are untouched — no display change, no role rewriting, no card transformation — so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. \"stacked-record-collection\" is the other direction, for a WIDE, HETEROGENEOUS record set with no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Give each TableCell a `label` — its column header — because the <th> association it normally reads from is the thing that just went away; DataTable derives that label from ColumnDef.header for you (gh#864). In card mode the CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The label is aria-hidden and the real <th> stays in the DOM, so the accessible name and table navigation are unchanged.",
        "name": "preset",
        "type": "\"default\" | \"action-collection\" | \"stacked-record-collection\""
      },
      {
        "defaultValue": "\"sm\"",
        "description": "Step at which preset=\"action-collection\" switches to the compact priority measures, or preset=\"stacked-record-collection\" folds its rows into cards. Measured against the TABLE'S OWN container (a container query), not the viewport — a table inside a master rail collapses before the page does. Ignored while preset is \"default\".",
        "name": "collapseBelow",
        "type": "\"sm\" | \"md\" | \"lg\" | \"xl\""
      },
      {
        "description": "Accessible name for the horizontal-scroll REGION — the tabindex=\"0\" wrapper a keyboard user lands on to scroll a table wider than its container — NOT for the <table> element (pass aria-label for that; it still reaches the table). OPTIONAL: left out, the region takes the localized dataTable.scrollRegion default (\"Scrollable table\"), so no consumer has to invent a name for every table. Pass a plain string when the page can say WHICH table; a non-string node cannot be an aria-label and falls back to the default. The wrapper carries role=\"group\" (not \"region\" — a named region is a LANDMARK, and several tables on one page would then collide under axe landmark-unique) and is emitted ONLY while the region actually has overflow to reach, measured at runtime: a table that fits adds no tab stop, no role and no name. (gh#817)",
        "name": "label",
        "type": "LabelProp"
      }
    ],
    "related": [
      "DataTable — choose DataTable for any data array that needs sorting, filtering, pagination, row selection, bulk actions, or density toggle. DataTable internally renders Table primitives, so switching up is non-breaking. Default to DataTable for all admin list pages.",
      "SkeletonTable — use as a loading placeholder before a Table or DataTable mounts. Drop it in the `skeleton` slot of DataState, or render it directly while data is fetching. Do not show a Table with empty rows as a loading state.",
      "Descriptions — choose Descriptions when content is label→value pairs (two columns, no repeated rows of the same type). Table is better when every row shares the same typed columns.",
      "DataState — when your Table's data comes from `useQuery`, wrap it in DataState to handle loading/error/empty states declaratively instead of writing conditional logic around the Table yourself."
    ],
    "rules": [],
    "storyPath": "data-display/table.tsx",
    "subParts": [
      "TableBody",
      "TableCell",
      "TableHead",
      "TableHeader",
      "TableRow"
    ],
    "tagline": "Primitive table shell (Table/TableHeader/TableBody/TableRow/TableHead/TableCell). Prefer DataTable for admin lists; use these for custom one-off tables. Keyboard focus uses the shared inset ring in clipping surfaces; scrollports reveal partially clipped keyboard targets without changing their layout.",
    "usage": [
      "DO compose all six sub-parts in order: wrap with `<Table>`, then `<TableHeader>` containing `<TableRow><TableHead>…</TableRow>`, then `<TableBody>` containing one or more `<TableRow><TableCell>…` rows. Skipping any layer (e.g. bare `<th>` inside `<Table>`) bypasses the design tokens and hover/border styles.",
      "DO use `TableHead` (not `TableCell`) for header cells — it renders `<th>` with `data-slot=\"table-head\"` and the `--table-row-height` CSS variable for consistent header sizing across the design system. `TableCell` renders `<td>` with `data-slot=\"table-cell\"` and is for body rows only.",
      "DO use `numeric` on TableHead/TableCell for tabular end-aligned numbers; `align=\"start|center|end\"` overrides alignment, `wrap` allows multi-line text, and `width` sets a CSS column measure. These props replace alignment and width classes.",
      "DO use `striped` on dense list tables so a row is easy to follow across its columns; to turn it on for EVERY Table and DataTable at once, set `:root { --table-row-striped-alpha: 100%; }` in the theme rather than passing the prop everywhere (`striped={false}` opts one table out). A detail row under a record is `<TableRow data-expanded-row=\"\">` so the stripe counts records, not DOM rows — never stripe with `:nth-child` page CSS or row utilities.",
      "DO NOT hand-roll empty-state handling inside a Table composition. When data can be empty, switch to `DataTable` (which has a built-in empty state) or wrap the `<Table>` with a conditional that renders `<EmptyState>` — never leave a table with only a header and zero rows.",
      "DO NOT use Table for lists that need sorting, filtering, pagination, or row selection — those features are only in `DataTable`. Table is intentionally stateless: it owns no TanStack Table instance, no column definitions, and no toolbar.",
      "DO reach for `preset=\"action-collection\"` for a dense approval / action queue (requester · target · reason · requested date · row actions) that must stay readable at 390px, and mark every column with `priority` on BOTH its `TableHead` and its `TableCell`: `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved first so it can never be pushed off-screen). Leave the free-text column unmarked — it takes the remaining space. The `actions` token measure is a FLOOR sized for an icon trigger: a text action (対応する, Retry) plus a `…` menu grows the column to its content on its own (gh#1067), so it never paints over the previous column and needs no ColumnDef.width; an explicit width still wins. Leave a content column fluid. Do not add a hidden column or page-local breakpoint to make a table fit. The IDENTICAL preset exists on `DataTable` (`preset` + `collapseBelow` on the table, `priority` on the `ColumnDef`) sharing these same tokens — use DataTable when the queue is data-driven and needs sorting/selection/pagination, and reach for the raw `Table` only for a hand-authored queue.",
      "DO reach for `<TableCell flush>` when the cell's CONTENT owns its inset — an expanded detail panel under a row (`<TableCell flush colSpan={n}>`), a nested table, a full-bleed media strip. It drops the cell's own padding so the child spans the whole cell; without it the panel is indented by `--table-cell-space-x` and the only route was a `p-0` utility, which no service theme can reach.",
      "DO express hierarchy with `<TableCell indent={depth}>` — a grouped table's detail rows under their subtotal header, or a tree row under its parent. The measure is `--table-cell-space-x + depth x --table-cell-indent-space-step`, so level 0 sits on the column's own text axis and a service retunes (or flattens) the step in one token. Never hand-roll `style={{ paddingInlineStart }}` at the call site — that is a per-page constant no theme can reach, and it breaks in RTL unless you remember the logical property.",
      "DO place `<Table>` inside a `<CardContent flush>` (or `p-0` card) when embedding in a Card, so the built-in `overflow-auto` wrapper sits flush to the card edges. Wrapping with plain `<CardContent>` adds padding that clips the horizontal scroll shadow."
    ],
    "useCases": [
      "Invoice line-item breakdowns — a fixed, read-only list of product/quantity/unit-price/total rows where columns are predefined and will never need sort or filter controls.",
      "Summary/comparison tables inside a detail panel or Dialog, such as showing two payment plans side-by-side, where the structure is hand-authored and not driven by a data array.",
      "Print or PDF-export views where a minimal, stateless `<table>` element with predictable markup is required and DataTable's JS-driven features would interfere with server-side rendering or CSS print rules.",
      "Embedded sub-tables inside a DataTable expanded row (the inner table uses Table primitives because nesting a full DataTable instance inside another is unsupported).",
      "Static reference tables in documentation, onboarding, or settings pages — e.g. a permission matrix or feature comparison — where every cell is literal JSX content, not from a data array."
    ]
  },
  {
    "example": "import { useQuery } from \"@tanstack/react-query\";\nimport { DataState } from \"@godxjp/ui/query\";\n\ntype MembersPage = { items: { id: string; name: string }[] };\n\n// T flows from the query, so `d` is MembersPage in BOTH callbacks. If you find yourself\n// annotating them, the query is untyped — fix that, never write `(d: any)`.\nconst membersQuery = useQuery<MembersPage>({ queryKey: [\"members\"], queryFn: fetchMembers });\n\n<DataState query={membersQuery} skeleton={<SkeletonTable />} isEmpty={(d) => d.items.length === 0} empty={<EmptyState title=\"会員なし\" />}>\n  {(d) => <MemberTable items={d.items} />}\n</DataState>",
    "group": "data-display",
    "importPath": "@godxjp/ui/query",
    "name": "DataState",
    "props": [
      {
        "description": "The useQuery result.",
        "name": "query",
        "required": true,
        "type": "UseQueryResult<T>"
      },
      {
        "description": "Shown while loading.",
        "name": "skeleton",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Shown when the query is disabled/unstarted (pending + fetchStatus idle).",
        "name": "prerequisite",
        "type": "ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "Force Retry even for non-transient causes. Retry is offered automatically for transient/network/5xx errors regardless of this flag.",
        "name": "showRetry",
        "type": "boolean"
      },
      {
        "description": "Per-instance sign-in button for 401 / expired-token errors when NO `AuthExpiryProvider` is mounted (clicked by the user, never auto-invoked). Prefer `AuthExpiryProvider onAuthExpired` at the app root, which handles every 401 automatically and once (gh#1022).",
        "name": "onAuthError",
        "type": "() => void"
      },
      {
        "description": "Render function with resolved data.",
        "name": "children",
        "required": true,
        "type": "(data) => ReactNode"
      },
      {
        "description": "Shown when isEmpty(data) is true.",
        "name": "empty",
        "type": "ReactNode"
      },
      {
        "description": "Custom empty check.",
        "name": "isEmpty",
        "type": "(data) => boolean"
      }
    ],
    "related": [
      "InfiniteQueryState — use instead of DataState when the query is `useInfiniteQuery`; it accepts a `flatten` function to reduce pages and adds a load-more footer. DataState cannot accept `UseInfiniteQueryResult`.",
      "SkeletonTable / SkeletonStat — pass as the `skeleton` prop of DataState; they are not standalone replacements for DataState, only the loading slot inside it.",
      "EmptyState — pass as the `empty` prop of DataState alongside a matching `isEmpty` predicate; do not hand-roll an empty-check outside DataState by inspecting `query.data` yourself.",
      "AlertMutationFeedback — sibling widget for mutation (not query) lifecycle; use it below a form submit button to surface `useMutation` errors, not DataState which only handles `useQuery`."
    ],
    "rules": [],
    "storyPath": "query/data-state.tsx",
    "tagline": "TanStack Query lifecycle widget — skeleton / error / empty / success for one useQuery block. Import from @godxjp/ui/query.",
    "usage": [
      "DO: pass a `UseQueryResult<T>` directly from `useQuery` — DataState reads `isPending`, `isError`, `isFetching`, `data`, and `error` off it; never destructure those fields manually and branch yourself.",
      "DO: always provide a `skeleton` — it renders during both the initial pending phase and during a re-fetch after an error; pass `<SkeletonTable />` for tabular data or `<SkeletonStat />` for stat card lists — never `null` or a spinner div.",
      "DO: provide `empty` + `isEmpty` together when the data can legitimately return 0 items — e.g. `isEmpty={(d) => d.items.length === 0}` paired with `empty={<EmptyState title=\"…\" />}`. Omitting `empty` means an empty array still falls through to `children`, silently rendering a blank table.",
      "DON'T: wrap DataState in your own conditional — e.g. `{query.isSuccess && <DataState …>}`. DataState IS the conditional; the outer guard is redundant and breaks the retry/refetch skeleton.",
      "DON'T: use DataState for `useInfiniteQuery` results. The `query` prop type is `UseQueryResult<T>`, not `UseInfiniteQueryResult`. Use `InfiniteQueryState` (from `@godxjp/ui/query`) instead, which accepts `flatten` and renders a load-more footer.",
      "DO: mount `AuthExpiryProvider onAuthExpired={…}` once at the app root (SSO apps: redirect to the IdP with the current URL as return target). A 401 then never paints an alert: the handler runs automatically, once for any number of simultaneous 401s, and DataState keeps its `skeleton` with a polite live region announcing the redirect (gh#1022).",
      "DON'T: rely on per-page `onAuthError` for session expiry — it is only a click-to-sign-in button in the fallback alert (no provider). An expired session is an app-wide concern, not a page error.",
      "DO: classify errors by cause. Use session renewal/sign-in for 401, access guidance for 403, contextual correction for domain errors, and opt into showRetry only for transient network/5xx errors.",
      "DO: pass prerequisite for enabled:false queries. Pending + fetchStatus idle is unstarted, not loading, and never renders a skeleton.",
      "DO: rely on the localized, cause-specific error message — the raw backend/token/stack text is never shown. For a domain-specific message (e.g. a 422 field error) pass a custom errorRenderer.",
      "DO: expect a background refetch over existing data to keep the content on screen with a polite sr-only busy status — it does not flash the skeleton. Only the initial fetch (isPending) shows the skeleton."
    ],
    "useCases": [
      "A detail page that loads a single invoice/journal entry via `useQuery` — DataState renders the skeleton row while fetching, an error alert with retry if the API fails, and your invoice card only when data is confirmed non-null.",
      "A list page that shows a `DataTable` of members/partners — wrap the table in DataState so the skeleton matches the column count while loading and `EmptyState` appears when the filtered result set is empty.",
      "A sidebar panel that lazily loads related transactions for the selected entity — DataState keeps the panel in skeleton state during the background fetch without any manual `isPending` branching in the parent.",
      "A dashboard stat card that calls a summary API — DataState handles the loading/error/empty lifecycle so `<StatCard>` is only rendered with fully resolved numbers, preventing NaN or undefined rendering.",
      "Any page using `useQuery` where the empty state and loading state are visually different — DataState enforces the correct visual for each phase without scattered `if` statements across the component tree."
    ]
  },
  {
    "example": "import { AuthExpiryProvider } from \"@godxjp/ui/query\";\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    <AuthExpiryProvider\n      onAuthExpired={() => {\n        const returnTo = encodeURIComponent(window.location.href);\n        window.location.assign(`/auth/login?return_to=${returnTo}`);\n      }}\n    >\n      {children}\n    </AuthExpiryProvider>\n  );\n}",
    "group": "feedback",
    "importPath": "@godxjp/ui/query",
    "name": "AuthExpiryProvider",
    "props": [
      {
        "description": "Called ONCE per expiry — any number of simultaneous 401s share one call; re-armed after every auth-errored view has gone. Typically `window.location.assign(loginUrl(returnTo = location.href))`. Return a promise for a silent refresh (then invalidate queries); a throw/rejection brings back the sign-in alert, whose button calls this again.",
        "name": "onAuthExpired",
        "required": true,
        "type": "(context: { error: unknown }) => void | Promise<void>"
      },
      {
        "description": "The app.",
        "name": "children",
        "type": "ReactNode"
      }
    ],
    "related": [
      "DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError — the surfaces that consult this provider.",
      "classifyQueryError — decides what is auth-class (401, or a status-less 'unauthenticated / access token invalid / token expired' message).",
      "ErrorSurface — the whole-page error surface; a session expiry is neither a page error nor an inline one."
    ],
    "rules": [],
    "tagline": "App-root handler for an expired session (401 / invalid or expired token): every DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError calls it automatically, once, and shows a neutral pending state instead of an error alert. Also exported from @godxjp/ui.",
    "usage": [
      "DO: mount it ONCE near the root, inside the QueryClientProvider/AppProvider, and redirect to the IdP with the current URL as the return target. The IdP returns immediately while its own session is alive — the SSO norm (Google/Microsoft apps, OIDC SPA guidance): no page-level error for an expired session.",
      "DO: expect the surfaces to keep their `skeleton` (DataState/InfiniteQueryState) or a small muted status line (AlertMutationFeedback/AlertQueryError) with `role=\"status\"` announcing 'redirecting to sign-in' — never a destructive alert.",
      "DON'T: wire `onAuthError` on every DataState as the session strategy — that renders an alert and waits for a click. It is the no-provider fallback only.",
      "DON'T: use it for 403 — forbidden is a permission state, not an expired session; it still renders the access-guidance alert.",
      "DO: when there is no provider the 401 fallback is a neutral (tone=default, role=status) sign-in alert capped at `--query-auth-alert-max-inline-size` (36rem, `none` = full width) — backward compatible, but proportionate."
    ],
    "useCases": [
      "An SSO app (GoDX ID / OIDC) whose access token expired while the tab was idle: the first 401 redirects to the IdP and back to the same URL, no error panel flashes.",
      "A dashboard with five DataStates that all 401 in the same tick: exactly one redirect.",
      "A form submit (`AlertMutationFeedback`) that 401s: a muted 'redirecting to sign-in' line instead of a red alert.",
      "A silent token refresh: return a promise that refreshes and then `queryClient.invalidateQueries()`; the views recover from skeleton without ever showing an error."
    ]
  },
  {
    "example": "import { useInfiniteQuery } from \"@tanstack/react-query\";\nimport { InfiniteQueryState, flattenItemPages } from \"@godxjp/ui/query\";\n\ntype Activity = { id: string; label: string };\n\n// `flattenItemPages` constrains the page to `{ items: TItem[] }`, so the query must be typed:\n// an untyped one makes the page `unknown`, which cannot satisfy that constraint.\nconst q = useInfiniteQuery<{ items: Activity[]; cursor?: string }>({\n  queryKey: [\"activity\"],\n  queryFn: fetchActivityPage,\n  initialPageParam: undefined,\n  getNextPageParam: (last) => last.cursor,\n});\n\n<InfiniteQueryState query={q} skeleton={<SkeletonRows />} flatten={flattenItemPages} isEmpty={(it) => it.length === 0}>\n  {(items) => items.map((a) => <ActivityRow key={a.id} activity={a} />)}\n</InfiniteQueryState>",
    "group": "data-display",
    "importPath": "@godxjp/ui/query",
    "name": "InfiniteQueryState",
    "props": [
      {
        "description": "The useInfiniteQuery result.",
        "name": "query",
        "required": true,
        "type": "UseInfiniteQueryResult"
      },
      {
        "description": "Shown while initial load pends.",
        "name": "skeleton",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Reduce pages to a flat list (use flattenItemPages helper).",
        "name": "flatten",
        "required": true,
        "type": "(data) => TFlat"
      },
      {
        "description": "Render with flat data + { fetchNextPage, hasNextPage, isFetchingNextPage }.",
        "name": "children",
        "required": true,
        "type": "(flat, helpers) => ReactNode"
      },
      {
        "description": "Idle text of the built-in load-more button (default: localized `query.loadMore`, 「さらに読み込む」) — e.g. 「さらに表示」「さらに古い版を表示」. The button, its pending label and disabled-while-fetching stay the component's. Ignored when `loadMore` replaces the footer.",
        "name": "loadMoreLabel",
        "type": "ReactNode"
      }
    ],
    "related": [
      "DataState — use instead when the query is a plain `useQuery` (not infinite). Identical lifecycle surface (skeleton/empty/error/children) but expects a single page of data, not accumulated pages. Pick DataState for any paginated table where only one page is visible at a time.",
      "DataTable — use for tabular data with server-side pagination where pages are swapped, not appended. DataTable manages its own pagination UI (cursor buttons); InfiniteQueryState is for append-only / infinite-scroll patterns.",
      "SkeletonTable / SkeletonStat — pass as the `skeleton` prop to InfiniteQueryState; do not render them manually alongside InfiniteQueryState since the component controls when skeleton is visible.",
      "ButtonRefetch — companion component for the page header refresh action wired to `query.refetch()`. Use alongside InfiniteQueryState when you want an explicit refresh control in addition to the built-in load-more footer."
    ],
    "rules": [],
    "storyPath": "query/infinite-query-state.tsx",
    "tagline": "useInfiniteQuery widget — flatten pages, skeleton/empty/error, load-more footer. Import from @godxjp/ui/query.",
    "usage": [
      "DO: Import from `@godxjp/ui/query` (not `@godxjp/ui`). Use the bundled `flattenItemPages` helper for any API that returns `{ items: T[] }` pages — it handles `undefined` data safely. Custom page shapes require a custom `flatten` function.",
      "DO: Always pass `skeleton` (e.g. `<SkeletonTable />` or `<SkeletonStat />`). It shows on initial `isPending`, on refetch-after-error, and whenever `data` is absent. Never show a blank area while loading.",
      "DO: Pass `empty` (an `<EmptyState>` node) to handle the zero-results case — without it the children render-prop is called with an empty array and you get a silent blank screen. Provide a custom `isEmpty` only when `TFlat` is not an array.",
      "DO: Let errors remain cause-aware. Retry is automatic only for classified transient/network/5xx failures. Unknown errors do not get a blind retry unless `showRetry` or `onRetry` is explicitly supplied; a 401 is handled by the app-root `AuthExpiryProvider` (auto, once, skeleton + live region) or, without one, by the `onAuthError` sign-in button; raw backend/token text is never rendered.",
      "DON'T: Hand-roll a load-more button. The component renders a default centered outline Button when `hasNextPage` is true. Rename it with `loadMoreLabel` (keeps the pending state); replace it only via `loadMore` (custom node) or `showLoadMore={false}` (hide entirely). Never call `query.fetchNextPage()` outside the component for pagination.",
      "DON'T: Use `InfiniteQueryState` for a `useQuery` result — it expects `UseInfiniteQueryResult` shape (`pages`, `hasNextPage`, `fetchNextPage`, `isFetchingNextPage`). For regular `useQuery` use `DataState` instead.",
      "DON'T: Confuse the two generics: `TPage` is the raw page shape from the API, `TFlat` is what `flatten` returns (usually `TItem[]`). The `children` render-prop receives `TFlat`, not `TPage`. Pass `isEmpty` if `TFlat` is not a plain array so empty detection works correctly."
    ],
    "useCases": [
      "Activity / audit-log feed that accumulates pages as the user scrolls down or clicks 'Load more' — the default footer button handles `fetchNextPage` automatically.",
      "Invoice or transaction list with cursor-based pagination where total count is unknown and pages are appended rather than replaced (replacing pages is DataTable's job).",
      "Notification inbox, comment thread, or journal entry list where new items are appended at the bottom and the user never pages backwards.",
      "Search results with a 'Show more' button rather than numbered pages — pass `showLoadMore={true}` (default) and hide the button once `hasNextPage` is false without any extra state.",
      "Admin dashboard 'recent events' widget backed by `useInfiniteQuery` — use `SkeletonTable` as `skeleton` and `<EmptyState title='No events yet' />` as `empty` so every state is handled.",
      "Infinite-scroll implementation: receive the `helpers` argument in `children` (`{ fetchNextPage, hasNextPage, isFetchingNextPage }`) to wire a scroll sentinel (Intersection Observer) instead of the built-in button, while still benefiting from error/skeleton/empty lifecycle handling."
    ]
  },
  {
    "example": "import { Form, FormField, Input } from \"@godxjp/ui/data-entry\";\n\n<Form layout=\"horizontal\" labelWidth={120} columns={2} onSubmit={onSubmit}>\n  <FormField id=\"first\" label=\"姓\"><Input id=\"first\" /></FormField>\n  <FormField id=\"last\" label=\"名\"><Input id=\"last\" /></FormField>\n  <FormField id=\"address\" label=\"住所\" colSpan={2}><Input id=\"address\" /></FormField>\n</Form>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Form",
    "props": [
      {
        "defaultValue": "\"vertical\"",
        "description": "Label position relative to control; applied to all FormFields.",
        "name": "layout",
        "type": "\"vertical\" | \"horizontal\" | \"inline\""
      },
      {
        "description": "Label column width in horizontal layout (number→px). e.g. 120 or '8rem'.",
        "name": "labelWidth",
        "type": "number | string"
      },
      {
        "description": "Cap the control width (number→px). Omit to fill the column. Caps the control only — the helper and error still span the field column.",
        "name": "controlWidth",
        "type": "number | string"
      },
      {
        "defaultValue": "\"end\"",
        "description": "Horizontal alignment of the label within its column.",
        "name": "labelAlign",
        "type": "\"start\" | \"end\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Breakpoint below which horizontal collapses to vertical (mobile-first). false = always horizontal.",
        "name": "collapseBelow",
        "type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | false"
      },
      {
        "description": "Lay fields out in a responsive grid (reuses ResponsiveGrid; 1 col on small).",
        "name": "columns",
        "type": "number | { sm?: number; md?: number; lg?: number }"
      },
      {
        "description": "Server validation error bag (e.g. Inertia's form.errors). Each FormField name='…' inside resolves its own message from the bag and CLAIMS its key; <FormErrors /> renders the unclaimed remainder (errors on hidden/derived fields). Works in both <form> and asChild modes.",
        "name": "errors",
        "type": "Partial<Record<string, string | string[]>>"
      },
      {
        "description": "Apply a density to controls inside the form.",
        "name": "density",
        "type": "\"compact\" | \"default\" | \"comfortable\""
      },
      {
        "description": "Disable controls inside FormField, including nested native controls.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Show required marks, hide them, or mark optional fields.",
        "name": "requiredMark",
        "type": "boolean | \"optional\""
      }
    ],
    "related": [
      "FormField — the per-field wrapper (label + control + helper/error) that reads Form's layout context; use one per control inside a Form.",
      "FormErrors — renders the error-bag entries no mounted FormField claims (validation errors on hidden/derived fields); place inside a `Form errors={…}`.",
      "ResponsiveGrid — Form `columns` reuses it; use ResponsiveGrid directly for non-form card grids."
    ],
    "rules": [
      23,
      24
    ],
    "storyPath": "data-entry/form.tsx",
    "tagline": "Ant-style layout container — renders <form> and pushes layout (vertical/horizontal), labelWidth/controlWidth, label alignment, responsive collapse, and multi-column grid down to every FormField (overridable per field).",
    "usage": [
      "DO set `layout`, `labelWidth`, `controlWidth` ONCE on `<Form>` — every `<FormField>` inside inherits them. Override a single field by passing the same prop on that `<FormField>` (Form → FormField priority).",
      "DO rely on mobile-first collapse: `layout='horizontal'` automatically stacks to vertical below `collapseBelow` (default `md`). Pass `collapseBelow={false}` only when a field MUST stay label-beside-control even on phones.",
      "DO use `columns` for multi-field forms (e.g. `columns={2}`) — it reuses ResponsiveGrid (1 column on small screens, more on md/lg). Span a wide field across columns with `<FormField colSpan={2}>`.",
      "ROW RHYTHM IS THE SAME ON EVERY PATH: a grid of FormFields hands its row spacing to the grid's own `row-gap` (--form-grid-row-gap, defaulting to --form-field-row-gap) instead of the per-field margin a stacked Form uses. So `columns={1}` — and any `columns={n}` form once a narrow container collapses it to one column — is pixel-identical to a Form with no `columns`, and a hand-written `<ResponsiveGrid columns={2}>` of FormFields inside a `CardContent` (what a form with several titled Card sections has to write) matches `columns={2}` exactly. Retune --form-field-row-gap to move every path together; --form-grid-row-gap / --form-grid-column-gap to retune the grid rows / gutter alone. Never add margin to a FormField to space a grid row: a per-item margin inside a grid misaligns row 1 and double-counts the track gap.",
      "ACTION ROW in a horizontal form (gh#1271): the submit/cancel buttons go in `<FormField label={false}><Flex gap=\"sm\" wrap>…buttons…</Flex></FormField>` as the LAST field. A label-less field renders no label cell and its control is pinned to the control column (gh#709), so the first button's left edge is the inputs' left edge, it follows `labelWidth`, and it collapses with the fields below `collapseBelow`; `wrap` lets the row break onto a second line on a phone. Requires `labelWidth` on the Form — unset, each label column is as wide as its own text and nothing can align with it. DON'T use `staticText` for buttons (it is the read-only VALUE slot), DON'T push the row with a hand-written padding, and DON'T float it to the far end of the card, away from the column the eye just went down. This is the row for a form INSIDE a card or section; a form that is the whole page puts its primary + cancel in the PageContainer footer instead (docs/general/button/examples/form-actions.tsx).",
      "DON'T hand-roll a `<form>` + Flex stack for spacing — `<Form>` provides the vertical rhythm and the layout context FormField reads. Wire react-hook-form by spreading `onSubmit={handleSubmit(...)}` onto `<Form>`.",
      "SERVER ERROR BAG: pass `errors={form.errors}` (Inertia) ONCE on `<Form>`, give each field a `name`, and put `<FormErrors />` at the top of the form. A named field resolves its message from the bag automatically (no per-field `error={errors.x}`), and FormErrors catches validation errors on hidden/derived keys (`action_mode`, `page`, a source-record id) that no visible field could display — without it those submits fail SILENTLY.",
      "SIBLING FORMS: an edit screen split into several Card+Form sections shares ONE bag by wrapping the region in `<FormErrorsProvider errors={form.errors}>` instead of passing `errors` to each Form — the section Forms (without their own `errors`) join the shared registry, and one `<FormErrors />` anywhere in the region renders the unclaimed remainder."
    ],
    "useCases": [
      "A settings page form where every label sits in a fixed 120px column to the left of its control (horizontal), collapsing to stacked labels on mobile.",
      "A two-column entity-edit form (`columns={2}`) where the address field spans both columns (`colSpan={2}`).",
      "A compact filter form (`layout='horizontal' density='compact'`) above a DataTable.",
      "An Inertia edit screen passing `errors={form.errors}` so every `FormField name='…'` self-binds its server validation message and `<FormErrors />` surfaces the hidden-field remainder."
    ]
  },
  {
    "example": "import { FormField, Input } from \"@godxjp/ui/data-entry\";\n\n<FormField id=\"coupon-name\" label=\"クーポン名\" required error={errors.name} helper=\"最大50文字\">\n  <Input id=\"coupon-name\" placeholder=\"春の花粉症対策15%OFF\" value={name} onValueChange={(v) => setName(v)} />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "FormField",
    "props": [
      {
        "description": "Tên trường của form — dùng khi `id` không đủ để nối control với error/aria.",
        "name": "field",
        "type": "string"
      },
      {
        "description": "Nội dung phụ cạnh nhãn: gợi ý, badge bắt buộc, nút trợ giúp, action chữ ngắn. Nằm TRONG hàng nhãn nên không phá nhịp trường. Ở layout horizontal/inline hàng nhãn xuống dòng: addon không vừa cạnh nhãn thì rơi xuống dòng riêng dưới nhãn, trong cột nhãn, không tràn sang cột control.",
        "name": "labelAddon",
        "type": "ReactNode"
      },
      {
        "description": "Forwarded to Label htmlFor + builds helper/error ids.",
        "name": "id",
        "required": true,
        "type": "string"
      },
      {
        "description": "Error-bag key of this field. When the surrounding Form carries `errors`, the field resolves its message from `errors[name]` automatically (an explicit `error` prop wins) and CLAIMS the key so <FormErrors /> does not repeat it. NOT injected into the child — pass `name` on the control itself for native form submission.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Field label above the control. `false` renders no label row; in a horizontal form the content still sits in the control column — the action row (gh#1271).",
        "name": "label",
        "required": true,
        "type": "ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "Red asterisk + aria-required on the child.",
        "name": "required",
        "type": "boolean"
      },
      {
        "description": "Muted hint shown when there is no error.",
        "name": "helper",
        "type": "string"
      },
      {
        "defaultValue": "\"after\"",
        "description": "Which side of the control the helper sits on. `before` puts it between the label and the input, for a hint the reader needs before answering (a bilingual form's second line, a unit or format note). Paint only — the helper keeps its id and stays on aria-describedby. Prefer it over stuffing a second line into `labelAddon` (a label-row slot for a chip, help button or short action) or into a ReactNode `label` (which loses the string-label aria fallbacks).",
        "name": "helperPlacement",
        "type": "\"before\" | \"after\""
      },
      {
        "description": "Destructive error message (role=alert); overrides helper.",
        "name": "error",
        "type": "string"
      },
      {
        "description": "Override the parent Form's layout for this field only.",
        "name": "layout",
        "type": "\"vertical\" | \"horizontal\" | \"inline\""
      },
      {
        "description": "Override the Form's label width for this field.",
        "name": "labelWidth",
        "type": "number | string"
      },
      {
        "description": "Override the Form's control width for this field. Caps the control only — the helper and error still span the field column.",
        "name": "controlWidth",
        "type": "number | string"
      },
      {
        "description": "Span N columns when inside a `columns` Form grid.",
        "name": "colSpan",
        "type": "number"
      },
      {
        "description": "The single interactive control to render. Mutually exclusive with `staticText` — pass exactly one of the two.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Read-only VALUE instead of an interactive control — renders as plain text styled to match `Descriptions.Item`'s value typography byte-for-byte (`text-sm break-all`), skipping all of FormField's id/aria-* control wiring (there is nothing to label). Mutually exclusive with `children`. Use this to put a read-only field (name, email — anything immutable) on the SAME `<Form>` as editable fields, so it inherits the exact same layout/labelAlign/row-gap automatically instead of reaching for a separate `Descriptions` block that needs its own props reconciled to match.",
        "name": "staticText",
        "type": "ReactNode"
      },
      {
        "description": "Validation state; an actual error takes priority. Warnings do not mark values invalid.",
        "name": "validateStatus",
        "type": "\"success\" | \"warning\" | \"error\" | \"validating\""
      },
      {
        "description": "Show a localized accessible validation status beside its feedback icon.",
        "name": "hasFeedback",
        "type": "boolean"
      },
      {
        "description": "Custom feedback content; the localized status remains available.",
        "name": "feedback",
        "type": "React.ReactNode"
      }
    ],
    "related": [
      "Label — the bare label component. Use directly only when you are building a fully custom layout that cannot accept FormField's stack wrapper, and you will manage aria-describedby/aria-invalid yourself. FormField is always preferred for standard form controls.",
      "Field — the inline row for a boolean control: label and control side by side, with `labelAddon`, `description` and `error` (gh#812 added the last two slots; `error` wires aria-invalid / aria-errormessage / aria-describedby onto the control, so a validated boolean no longer has to be hand-wired). It still renders NO hidden `<input name>` — `Switch name=\"…\"` submits itself — and it has no `required`, `helper`, error-bag `name` binding or layout/width knobs. Still: never wrap a bare `Switch` in FormField.",
      "Field — pairs a single checkbox or radio with a label and optional description in a horizontal layout (control beside text). Use Field instead of FormField when the control and its label sit side-by-side rather than stacked.",
      "CheckboxGroup / RadioGroup — for groups of options where FormField is not needed per-item; the group component handles its own legend/label and option layout."
    ],
    "rules": [
      23
    ],
    "storyPath": "data-entry/form-field/index.tsx",
    "tagline": "Wraps a control with label, helper, and error; injects the accessible name (aria-labelledby), description (aria-describedby) and validation (aria-errormessage/aria-invalid/aria-required) contract onto the child, which forwards it to its real semantic focus target. Reads the parent Form's layout (vertical/horizontal) — overridable per field.",
    "usage": [
      "DO pass the same string to both `id` on `<FormField>` and `id` on the child control — the component wires `<Label htmlFor={id}>`, and builds `{id}-helper` / `{id}-error` ids for `aria-describedby`. If the ids diverge the label click and screen-reader announcements break.",
      "NO LABEL: `label={false}` renders no label row and reserves no label space; in a horizontal form the content still sits in the CONTROL column (gh#709). That is the canonical action row under a horizontal form (gh#1271): `<FormField label={false}><Flex gap=\"sm\" wrap><Button type=\"submit\">Save</Button><Button variant=\"outline\">Cancel</Button></Flex></FormField>`. A label-less field around a CONTROL must name the control itself (a `<Checkbox>label</Checkbox>`, an `aria-label`).",
      "DO pass a SINGLE React element as `children`. FormField calls `React.cloneElement` on it to inject `aria-describedby`, `aria-required`, and `aria-invalid` — if you pass a fragment or multiple nodes, cloneElement silently skips the injection and a11y attributes are lost.",
      "COMPOSITE CHILD: when the single child is a layout wrapper — a `Flex` holding a range from/to pair or a 年/月 input+select combo — the label still reaches every control inside. FormField publishes its label through FieldNameContext and each control's semantic focus target (Input's `<input>`, Select's `role=combobox` trigger, and everything composed on them) adopts it as a LAST-RESORT accessible name; a control's own `aria-label`/`aria-labelledby` always wins, so set a per-control `aria-label` (e.g. 開始日/終了日) when the two halves should announce distinct names. The wrapper itself renders as a named `role='group'` (see Flex).",
      "DO reach for `staticText` (not `children` with a bare string/span) for a read-only field mixed into an otherwise-editable Form — e.g. an immutable name/email row above an editable role Select on the same Members-edit card. It renders with the exact typography `Descriptions.Item`'s value uses, and — because it IS a FormField reading the same Form context — it lines up with every other field's label column, `labelAlign`, and row-to-row gap automatically. A bare string as `children` instead triggers the dev-mode 'expected a single React element child' warning and has no typography contract at all.",
      "WIDTH: a FormField FILLS its container in vertical/horizontal layout — like the conventional Form.Item (vertical → width:100%). It works full-width inside `<Form>`, a `ResponsiveGrid` cell, a bare `<Flex direction='col'>`, or a plain block; you do NOT need to wrap it in a grid to get full width. `layout='inline'` is the only content-width exception (compact, side-by-side). To narrow just the control (keeping the label row full-width), set `controlWidth` — never constrain the FormField itself.",
      "DO use the `error` prop (not a hand-rolled `<p>`) for validation messages — it renders with `role='alert'` and `text-destructive` styling and overrides `helper` automatically. Never render an error paragraph alongside FormField.",
      "DO use `labelAddon` (a ReactNode rendered after the label text, in the label row) for supplementary controls such as a tooltip trigger, a 'copy' icon button or a short text action ('Assign to myself'); in a horizontal/inline layout the label row wraps, so an addon that does not fit beside the label drops under it inside the label column rather than overlapping the control — never insert such controls as siblings outside FormField, which breaks layout.",
      "DON'T wrap `Switch` in FormField — use `Field` instead. Field owns the inline row: `label`, `labelAddon`, `description`, and `error` (which wires aria-invalid / aria-errormessage / aria-describedby onto the control, gh#812). It renders no hidden input of its own — `Switch name=\"…\"` is what submits natively — and it has no `required`, `helper` or layout knobs.",
      "DON'T use FormField for checkbox-beside-label or radio-beside-label patterns — use `Field` (single checkbox/radio with description) or `CheckboxGroup` / `RadioGroup` (multiple options), which have their own integrated labelling.",
      "CONTRACT (which element owns each ARIA relationship): every data-entry control accepts and FORWARDS the injected props to its real semantic focus target, not a wrapper div — Input/Textarea/NumberInput → the `<input>/<textarea>`; Select/Cascader/TreeSelect → the `role=combobox` trigger (with aria-expanded + aria-haspopup + aria-controls per the WAI-ARIA APG combobox pattern); DatePicker/TimePicker → the typeable `role=combobox` input (aria-haspopup=dialog); ColorPicker → the `<input type=color>` swatch; SearchInput → the `role=searchbox` input. GROUP controls own the relationship on their container: RadioGroup → `role=radiogroup` (full validation incl. aria-invalid/-errormessage/-required); CheckboxGroup, `DatePicker range` (two inputs), and Transfer → `role=group` — per ARIA 1.2 a group is not a widget, so the error id is folded into aria-describedby instead of aria-invalid/-errormessage. Upload forwards the label/description onto its native `<input type=file>`; its visible dropzone/button keeps its own action label. This forwarding is implemented once in `src/lib/field-a11y.ts` (`pickFieldA11y` / `pickGroupFieldA11y` / `resolveFieldA11y`) — do not reinvent it per control.",
      "FIELD IDENTITY / AUTOMATION: FormField also injects a `data-field` — the field's stable MACHINE key, resolved as `field` → `name` → `id` — onto the same semantic focus target the ARIA relationships land on, and onto every option of a RadioGroup/CheckboxGroup. Use it (not a generated id, and never the visible Japanese label) as the selector in e2e tests and screen automation. It reaches NESTED controls too: when the direct child is a layout wrapper (a Flex holding a from/to pair, a 年/月 combo, a value beside a 「不明」 checkbox) cloneElement stops on that wrapper, so FormField also publishes the field through context and each control inside resolves its own key from its OWN `id` — which is what keeps `search_billing_date_from` and `..._to` distinct instead of collapsing onto one shared key. A nested control with NO id of its own deliberately gets nothing: a fabricated key is worse than a missing one, because automation binds to it and breaks silently. Two companion pieces: a `Select`'s trigger also carries `data-value` = the selected CODE (the trigger shows the option LABEL, and Radix keeps the value in an aria-hidden 1x1px native `<select>`), and each RadioGroup/CheckboxGroup option gets a deterministic `{groupId}-{optionValue}` id instead of a per-mount `React.useId()` token. Nothing here is opt-in and no DOM structure changed. A `data-field` written on the control itself always wins.",
      "NATIVE `name` IS OPT-IN: FormField emits the same key as a real `name` attribute ONLY when the app set `<AppProvider emitFieldNames>`. It is off by default because `name` decides what a native `<form>` submit sends — turning it on globally in a shared package would make every consumer start posting new keys to its backend on an upgrade. Turn it on in apps that need native form posts or a screen-automation contract; a `name` written on the control itself always wins.",
      "NATIVE FORM PARTICIPATION: pass `name` to a control for HTML form submission — Input/Textarea/NumberInput/Select submit natively; a searchable Select submits via a hidden input; DatePicker/TimePicker emit ISO strings (`yyyy-MM-dd` / 24h `HH:mm`); the range pickers emit `${name}_from` / `${name}_to`. `required`/`readOnly`/`disabled` map to the underlying control. Cascader/TreeSelect/Transfer submit named values via hidden inputs; Upload appends staged local files to FormData when named. Disabled controls are excluded.",
      "ERROR TIMING & RECOVERY: pass `error` only after a field is dirty or the form is submitted (don't show errors on pristine mount). The error node renders with `role='alert'` so it is announced live the moment it appears; clearing `error` (e.g. after the user corrects the value or a server round-trip succeeds) removes aria-invalid and restores the helper. On submit, focus the first invalid control and/or render an error summary that links to each field by `id`."
    ],
    "useCases": [
      "Labelling a text `Input` or `Textarea` in an invoice-entry form, showing a red asterisk for required fields and surfacing server validation errors returned from a Laravel FormRequest.",
      "Wrapping a `Select` or `DatePicker` inside a multi-field filter panel where each control needs a visible label, helper hint (e.g. 'YYYY/MM/DD'), and inline error state.",
      "Adding a `labelAddon` tooltip button next to a 'Tax rate' label in an accounting form to explain when different rates apply, without breaking the label–control association.",
      "Enclosing a `DatePicker range` or `TimePicker` in an admin settings page where the field needs a label, a muted hint ('Inclusive of start and end date'), and conditional error display.",
      "Wrapping a `Select` (with `showSearch`) control for vendor/account lookup in a journal-entry form where the `id` must be kept consistent for programmatic focus management.",
      "Providing structured error feedback for a `Cascader` or `TreeSelect` in a multi-level category assignment screen, replacing ad-hoc error rendering with the standardised `role='alert'` pattern."
    ]
  },
  {
    "example": "import { Form, FormErrors, FormField, Input } from \"@godxjp/ui/data-entry\";\nimport { useForm } from \"@inertiajs/react\";\n\nconst form = useForm({ customer_nm: \"\", action_mode: \"regist\" });\n\n<Form asChild layout=\"horizontal\" labelWidth={140} errors={form.errors}>\n  <form onSubmit={submit}>\n    <FormErrors />\n    <FormField name=\"customer_nm\" label=\"顧客名\" required>\n      <Input value={form.data.customer_nm} onChange={(e) => form.setData(\"customer_nm\", e.target.value)} />\n    </FormField>\n  </form>\n</Form>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "FormErrors",
    "props": [
      {
        "description": "Explicit error bag — overrides the surrounding Form's `errors`. Use when the component sits outside a Form (e.g. inside FormRoot); field claiming still applies when a Form provides the registry.",
        "name": "errors",
        "type": "Partial<Record<string, string | string[]>>"
      },
      {
        "description": "Heading above the messages. Defaults to the localized 'please review your input' title (dataEntry.formErrors.title).",
        "name": "title",
        "type": "ReactNode"
      },
      {
        "description": "Root class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Form — provides the error bag (`errors`) and the claim registry FormErrors reads; FormErrors must sit inside it (or receive `errors` explicitly).",
      "FormErrorsProvider — the shared registry for a REGION of sibling Forms (multi-Card edit screens): wrap the region with it, leave `errors` off the section Forms, and one FormErrors covers the whole screen.",
      "FormField — `name` claims a bag key and self-binds its message; the claimed key never re-appears in FormErrors.",
      "Alert — the underlying destructive banner; use Alert directly for non-validation notices."
    ],
    "rules": [
      23
    ],
    "subParts": [
      "FormErrorsProvider"
    ],
    "tagline": "The 'no field to stand on' error summary — renders the entries of the surrounding Form's server error bag that no mounted FormField name='…' claims: validation errors on hidden/derived fields (action_mode, page, a source-record id) that would otherwise fail silently. Composed on Alert tone='destructive' (role='alert'); renders nothing while every entry is claimed or the bag is empty.",
    "usage": [
      "DO pass the WHOLE bag to `<Form errors={form.errors}>` and place `<FormErrors />` at the top of the form — never hand-filter the bag per page. Fields with `name` claim their keys automatically; FormErrors shows only the remainder, so the consumer never maintains an except-list.",
      "DO give every visible field its `name` when adopting `Form errors` on a screen. A field that keeps a manual `error={errors.x}` WITHOUT `name` does not claim its key, and FormErrors will show that message twice.",
      "DON'T hand-roll a destructive Alert bound to `errors.hidden_key` per page — that is exactly the per-page listing this component exists to remove, and it goes stale the moment the server adds a new derived-field rule.",
      "DON'T use FormErrors as a generic mutation-failure banner — that is `Alert.QueryError` / toast territory. FormErrors is scoped to the VALIDATION bag of the surrounding form.",
      "INSIDE FormRoot: `<FormRoot form={form} onSubmit={(v) => m.mutateAsync(v)} errors={serverErrors(m.error)}><FormErrors /><AlertMutationFeedback mutation={m} />…</FormRoot>` — FormRoot mounts the claim registry, so `<FormErrors />` needs no `errors` of its own and shows only unclaimed keys. A 422 renders exactly once: claimed keys under their fields, unclaimed keys here, no AlertMutationFeedback alert (gh#690) and no FormRoot submitFailed banner (gh#698).",
      "ARRAY ENTRIES: a `string[]` bag value lists every message in the banner; a claimed field shows only the FIRST message of its array (Laravel `$errors->first()` semantics).",
      "SIBLING FORMS: when the screen is split into several Card+Form sections, wrap the REGION in `<FormErrorsProvider errors={form.errors}>` and give NO `errors` to the section Forms — they join the shared registry and one `<FormErrors />` covers the whole screen. A nested Form WITH its own `errors` deliberately starts a separate (shadowed) registry."
    ],
    "useCases": [
      "An Inertia edit screen whose Laravel FormRequest validates hidden/derived inputs (`action_mode`, `page`, `source_slip_cd`) — the user pressed save and previously saw NOTHING because those keys have no visible field.",
      "A ported legacy screen where the server rejects a stale edit-lock or a missing source record under a key that only exists server-side.",
      "A create form where a Laravel `RuleObject` attaches a cross-field error to a synthetic key (e.g. `combination`) rather than to one input."
    ]
  },
  {
    "example": "import { Input } from \"@godxjp/ui/data-entry\";\n\n<Input id=\"qty\" type=\"number\" placeholder=\"例: 500\" value={value} onValueChange={(v) => setValue(v)} />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Input",
    "props": [
      {
        "description": "Validation state the field paints — Ant Design `status`. `error` also reports `aria-invalid`, so the red boundary and what a screen reader hears are one fact; `warning` paints only, because a warning is not a validity failure. antd's `success`/`validating` are not implemented: antd only draws them together with its `hasFeedback` icon slot, which FormField owns here.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "defaultValue": "\"outlined\"",
        "description": "Chrome level — Ant Design `variant`. `outlined` is the historical field; `filled` swaps the boundary for a tinted surface (dense forms); `borderless` drops both, for a field inside a box that already draws one. antd's fourth member `underlined` is deliberately absent — a single bottom rule is a Material convention and SmartHR, the JP authority here, draws every field as a full box.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control height tier — reads the shared `--control-height` ladder.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "antd `prefix` — content pinned INSIDE the start of the field (¥, a unit, a glyph). Unlike `leadingIcon` it is not aria-hidden, because a unit is meaning and not decoration.",
        "name": "prefix",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `suffix` — content pinned INSIDE the end of the field (%, 円). The clear ✕ replaces it while `allowClear` has a value, exactly as it replaces `trailingIcon`.",
        "name": "suffix",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `addonBefore` — a LABEL segment welded OUTSIDE the field's box (`https://`, a currency). Outside is the whole distinction from `prefix`: an addon has its own surface and closes the field's corners on the joined side. For a GLYPH use `prefix` — one box, one border, no seam. antd deprecates both addons in favour of `Space.Compact` (verified in ant-design@master: a `@deprecated` tag and a dev-time warning, still rendering); this library keeps them, because it has no `Space` and `addonBefore` IS its joined control (gh#841).",
        "name": "addonBefore",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `addonAfter` — the same, welded to the end of the box (`.com`, a unit). Deprecated upstream alongside `addonBefore`, and kept here for the same reason.",
        "name": "addonAfter",
        "type": "React.ReactNode"
      },
      {
        "description": "Character counter — Ant Design `count`. Counts CODE POINTS by default, so one emoji and one 全角 kanji are each worth one. It REPORTS an overrun (`data-exceeded`) and never edits the value: antd's `exceedFormatter` truncates while the user types, which in Japanese cuts a live IME conversion in half.",
        "name": "count",
        "type": "{ max?: number; show?: boolean; formatter?: (info: { value: string; count: number; max?: number }) => React.ReactNode; strategy?: (value: string) => number }"
      },
      {
        "description": "Associates with a <label htmlFor>.",
        "name": "id",
        "type": "string"
      },
      {
        "defaultValue": "\"text\"",
        "description": "Native input type.",
        "name": "type",
        "type": "string"
      },
      {
        "description": "Placeholder.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Controlled value.",
        "name": "value",
        "type": "string | number"
      },
      {
        "defaultValue": "false",
        "description": "Opt-in inline ✕ that clears the field while it holds text (works controlled + uncontrolled). Off by default, so existing inputs are unchanged.",
        "name": "allowClear",
        "type": "boolean"
      },
      {
        "description": "Called after the field is cleared via the inline ✕ (requires `allowClear`).",
        "name": "onClear",
        "type": "() => void"
      },
      {
        "description": "A leading affordance pinned inside the start of the field (e.g. a Mail / Lock / Search glyph) — purely decorative, rendered before the text. Sized to the control via tokens and offset with `ps-9` automatically; never hand-roll an absolutely-positioned icon over a plain Input.",
        "name": "leadingIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "A trailing affordance pinned inside the field (e.g. a calendar / clock popover trigger). ONE trailing icon shows at a time: when `allowClear` and the field holds a value the clear ✕ REPLACES this icon; otherwise this icon shows. Never both — this is how DatePicker/TimePicker render their open trigger.",
        "name": "trailingIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "Immediate value callback; native onChange remains supported. Shared form bindings are emitted once.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      }
    ],
    "related": [
      "PasswordInput — the password field. It owns the reveal toggle, the caps-lock hint and the autocomplete contract; a raw Input with type=password re-implements all three.",
      "SearchInput — use instead of Input when the value drives a live filter or search query; SearchInput debounces internally, fires `onSearch` (not `onChange`), and provides a built-in clear button. Never put debounce logic on top of a plain Input.",
      "Textarea — use instead of Input for multi-line text (notes, descriptions, memo fields). Input is strictly single-line.",
      "FormField — always compose Input inside FormField when the field needs a visible label, helper hint, or validation error message; FormField handles all a11y wiring so Input stays a pure unstyled-but-styled primitive.",
      "Select — use instead of Input when the value must come from a fixed or async option list; never render a plain Input and parse free text when the set of valid values is enumerable."
    ],
    "rules": [],
    "storyPath": "data-entry/input.tsx",
    "tagline": "Styled wrapper around native <input>; accepts all HTML input attributes. Pair with FormField for labelled fields.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged. The trailing slot is measured, so adjacent actions and counters reserve their combined width; allowClear still replaces trailingIcon/suffix.",
      "DO always wrap Input in FormField when the field needs a label, helper text, or validation error — FormField injects aria-describedby and aria-invalid onto Input automatically; never wire these attributes by hand.",
      "DO match the `id` prop on Input to the `id` prop on its parent FormField so that `htmlFor` linkage and the generated helper/error ids are consistent.",
      "DO use Input in controlled mode (`value` + `onChange`) for forms driven by Inertia's `useForm` or React state; uncontrolled usage (no `value`) is only acceptable for fire-and-forget inline edits where form state is not needed.",
      "DON'T use a raw `<input>` element — Input adds the full token-based styling (border-input, focus ring, disabled/invalid states, file-slot styling) and the `data-slot='input'` marker that FormField relies on to inject aria attributes.",
      "DON'T hand-roll an error border or red ring with className — Input reads `aria-invalid` (set by FormField) and applies `border-destructive` + `ring-destructive/20` automatically; adding manual destructive classes will conflict.",
      "DON'T use Input for multi-line text — use Textarea; DON'T use it for filtered/debounced search — use SearchInput which fires `onSearch` after a debounce and includes a clear button."
    ],
    "useCases": [
      "Single-line text fields in create/edit forms — invoice reference numbers, company names, contact emails, coupon codes, amounts typed as text (pair with `type='number'` for numeric entry).",
      "Inline editable cells or quick-edit dialogs where a single short value needs to be changed (e.g. editing a journal entry memo or an account code) and full Select/DatePicker overhead is unnecessary.",
      "File upload trigger when wrapped with `type='file'` — the file-slot classes style the native file button consistently without any extra wrapper.",
      "Password entry fields (`type='password'`) in auth or settings screens, where the styled focus ring and disabled-state opacity are needed without building a custom control.",
      "Numeric/currency input in accounting forms (`type='number'`, `inputMode='decimal'`) for quantities, exchange rates, or tax amounts where a free-form numeric entry is required rather than a slider or stepper."
    ]
  },
  {
    "example": "import { NumberInput } from \"@godxjp/ui/data-entry\";\n\n<NumberInput\n  value={qty}\n  onValueChange={setQty}\n  min={1}\n  max={99}\n  step={1}\n  prefix=\"¥\"\n  aria-label=\"数量\"\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "NumberInput",
    "props": [
      {
        "description": "Validation state the field paints — Ant Design `status`. `error` also reports `aria-invalid`, so the red boundary and what a screen reader hears are one fact; `warning` paints only, because a warning is not a validity failure. antd's `success`/`validating` are not implemented: antd only draws them together with its `hasFeedback` icon slot, which FormField owns here.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "defaultValue": "\"outlined\"",
        "description": "Chrome level — Ant Design `variant`. `outlined` is the historical field; `filled` swaps the boundary for a tinted surface (dense forms); `borderless` drops both, for a field inside a box that already draws one. antd's fourth member `underlined` is deliberately absent — a single bottom rule is a Material convention and SmartHR, the JP authority here, draws every field as a full box.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "antd `formatter` — how the committed number is DISPLAYED at rest (a unit, a separator, 円). Replaces the built-in `Intl.NumberFormat`; pair it with `parser` or the text cannot be read back.",
        "name": "formatter",
        "type": "(value: number | null) => string"
      },
      {
        "description": "antd `parser` — the inverse of `formatter`. Without one, the built-in reader NFKC-folds 全角 digits, periods and minus signs to ASCII first, so a value typed on a Japanese keyboard survives blur.",
        "name": "parser",
        "type": "(display: string) => number | null"
      },
      {
        "defaultValue": "true",
        "description": "antd `keyboard` — ArrowUp/ArrowDown step the value (Shift = ×10).",
        "name": "keyboard",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd `changeOnWheel` — a mouse wheel steps the value. Off by default and gated on FOCUS even when on, so scrolling past a long form cannot silently edit it.",
        "name": "changeOnWheel",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "antd `controls` — draw the increment/decrement buttons. `false` keeps the spinbutton role and the arrow keys; it only stops drawing the two buttons.",
        "name": "controls",
        "type": "boolean"
      },
      {
        "description": "Controlled value. `null` = empty field. Pair with `onValueChange`.",
        "name": "value",
        "type": "number | null"
      },
      {
        "defaultValue": "null",
        "description": "Uncontrolled initial value.",
        "name": "defaultValue",
        "type": "number | null"
      },
      {
        "description": "Value change callback (vocabulary triad — NOT onChange). Receives `null` when the field is empty.",
        "name": "onValueChange",
        "type": "(value: number | null) => void"
      },
      {
        "description": "Lower bound — clamps on commit and disables the decrement stepper at the floor.",
        "name": "min",
        "type": "number"
      },
      {
        "description": "Upper bound — clamps on commit and disables the increment stepper at the ceiling.",
        "name": "max",
        "type": "number"
      },
      {
        "defaultValue": "1",
        "description": "Increment for the steppers + ArrowUp/ArrowDown (Shift = ×10).",
        "name": "step",
        "type": "number"
      },
      {
        "description": "Committed decimal places. Inferred from `step` when omitted.",
        "name": "precision",
        "type": "number"
      },
      {
        "description": "Disables typing and stepping.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Value is shown and selectable but not typeable or steppable.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control height tier (--control-height). Aligns with sibling controls on a row.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Placeholder shown when empty.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Leading decorative affix inside the field (e.g. `¥`). aria-hidden.",
        "name": "prefix",
        "type": "React.ReactNode"
      },
      {
        "description": "Trailing decorative affix inside the field (e.g. `%`). aria-hidden.",
        "name": "suffix",
        "type": "React.ReactNode"
      },
      {
        "description": "Form field name — submits its value natively.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Associates with a <label htmlFor> / FormField.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Accessible name for the spinbutton when no visible FormField label is present.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "Input — the plain single-line field NumberInput composes; use Input directly only for free numeric text with no stepper/clamp need.",
      "Slider — use instead when the user picks an approximate value within a range by dragging; NumberInput is for exact keyed entry.",
      "FormField — compose NumberInput inside FormField (matching id) for label/helper/error a11y wiring.",
      "TimePicker — the HH:mm time sibling; NumberInput is for plain numbers, TimePicker for clock times."
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "data-entry/number-input.tsx",
    "tagline": "WAI-ARIA spinbutton for localized numeric entry — composes the real Input (role=spinbutton, inputMode=decimal) with stacked increment/decrement step Buttons. Type freely, Arrow/Shift-Arrow step, value commits clamped to min/max + rounded to precision.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged. The increment/decrement actions sit side by side on touch screens so each owns a non-overlapping target.",
      "DO use NumberInput (not `<Input type='number'>`) whenever numeric entry wants steppers, min/max clamping, precision rounding, or a ¥/% affix — it is the canonical numeric primitive. Plain Input has no stepper and no clamp.",
      "DO drive it controlled with `value` + `onValueChange` carrying `number | null` (the vocabulary triad — NOT `onChange`). `null` means the field is empty; never substitute 0 for empty.",
      "DON'T pass `value` without `onValueChange` — like every controlled @godxjp/ui input it would freeze. Omit both for uncontrolled (use `defaultValue`).",
      "DO set `step` to your increment and let `precision` (or the decimals of `step`) round the committed value: `step={0.25} precision={2}` gives quarter-step entry rounded to 2 places on blur/Enter.",
      "DO set `min`/`max` for bounded quantities — the value clamps on commit and the matching stepper Button auto-disables at the bound. The steppers are tabIndex=-1 so they never pollute the keyboard tab order (Arrow keys cover keyboard stepping).",
      "DON'T wrap it in a hand-rolled label/error markup — compose it inside FormField (matching `id`) for the aria wiring, exactly like Input.",
      "DON'T format the value yourself for display — NumberInput formats at rest via Intl.NumberFormat in the active locale while keeping the raw value typeable on focus."
    ],
    "useCases": [
      "Quantity / line-item steppers in order, invoice, or cart forms (min={1}, step={1}) where ± buttons and a floor are expected.",
      "Price / amount fields with a currency affix (prefix='¥', step={10}) — the affix is decorative and the committed value stays a plain number.",
      "Percentage / rate inputs bounded 0–100 (suffix='%', min={0} max={100}).",
      "Decimal measurements — weight, dimensions, exchange rates (step={0.25}, precision={2}) needing rounded commit.",
      "Any bounded numeric setting (timeouts, retry counts, page sizes) where a slider is too coarse and a free Input lacks clamping."
    ]
  },
  {
    "example": "import { SearchInput } from \"@godxjp/ui/data-entry\";\n\n<SearchInput placeholder=\"クーポン名・IDで検索\" value={search} onSearch={setSearch} />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "SearchInput",
    "props": [
      {
        "description": "Validation state the field paints — Ant Design `status`. `error` also reports `aria-invalid`, so the red boundary and what a screen reader hears are one fact; `warning` paints only, because a warning is not a validity failure. antd's `success`/`validating` are not implemented: antd only draws them together with its `hasFeedback` icon slot, which FormField owns here.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "defaultValue": "\"outlined\"",
        "description": "Chrome level — Ant Design `variant`. `outlined` is the historical field; `filled` swaps the boundary for a tinted surface (dense forms); `borderless` drops both, for a field inside a box that already draws one. antd's fourth member `underlined` is deliberately absent — a single bottom rule is a Material convention and SmartHR, the JP authority here, draws every field as a full box.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "Called after a changed query settles. Never fires for the initial value on mount or callback-only rerenders. Optional when filtering uses onValueChange.",
        "name": "onSearch",
        "type": "(q: string) => void"
      },
      {
        "description": "Controlled value.",
        "name": "value",
        "type": "string"
      },
      {
        "defaultValue": "\"\"",
        "description": "Initial uncontrolled value.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Input placeholder.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "defaultValue": "250",
        "description": "Debounce delay (ms).",
        "name": "debounce",
        "type": "number"
      },
      {
        "description": "Input id; pair with `label` or an external `<label htmlFor>`.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Optional visible label rendered above the search box (falls back to an sr-only label).",
        "name": "label",
        "type": "React.ReactNode"
      },
      {
        "description": "Fires on EVERY keystroke (immediate) — required to keep a controlled `value` responsive.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Accessible search name when there is no visible label.",
        "name": "ariaLabel",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Disable search input and clearing.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "A global shortcut that FOCUSES the field (gh#1250). `false` (default) binds none — two search boxes on one page must not fight over one key; `true` is ⌘K on Apple platforms and Ctrl+K elsewhere (never both, so Ctrl+K keeps its editing meaning on a Mac); a combo string (`\"mod+/\"`, `\"mod+shift+f\"`) is read exactly as CommandPalette `shortcut`. A trailing aria-hidden key cap shows the combo while the field is empty — the clear ✕ takes the slot once there is text. Never fires during IME composition, nor while `disabled`. A BARE key (`\"/\"`) is ignored while focus is in an editable element or an IME is composing, so it stays text there; a modified combo (`mod+k`) still fires from a field (gh#1264).",
        "name": "shortcut",
        "type": "boolean | string"
      },
      {
        "description": "Class on the `<input>` itself. SearchInput renders a WRAPPER (label, icon, clear button, input), so `className` lands on that wrapper and never reaches the field — this is the second handle, for the case where the field and its chrome need different treatment. Layout and colour still belong to tokens; use it for the rare geometry a token cannot reach.",
        "name": "inputClassName",
        "type": "string"
      }
    ],
    "related": [
      "Input — use `Input` (inside `FormField`) when the search field is part of a submitted form and needs a `name` attribute, or when you need full `onChange` control without any debounce or clear button. SearchInput is the right pick when the field only triggers filtering, not form submission.",
      "Toolbar — SearchInput is almost always placed as a direct child of `Toolbar`, which provides the surrounding strip, clear-all button, and active-filter state. Do not use SearchInput as a standalone header widget when a full filter strip (with selects etc.) already exists — compose them together.",
      "Command — use `Command` + `CommandInput` when you need a keyboard-navigable command palette or combobox list with grouped items and keyboard selection. `Command` is only meaningful when paired with `CommandList`; SearchInput is the right pick for a plain filter box with no item-selection behavior.",
      "Select (with showSearch) — when users must pick a value from a list AND search to narrow it, use `<Select options={...} showSearch>` (which has its own built-in search input). SearchInput is for filtering an external data set, not for value selection from an option list."
    ],
    "rules": [
      23
    ],
    "storyPath": "data-entry/search-input.tsx",
    "tagline": "Debounced search box with a clear button. Fires onSearch (NOT onChange) after the debounce. Controlled (value) or uncontrolled (defaultValue).",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "DON'T drop a bare SearchInput into a Topbar slot — use `TopbarSearch` (layout), which passes every SearchInput prop through and folds to a search cell below 40rem instead of shrinking under 8ch (ADR C02).",
      "DO: listen to `onSearch`, not `onChange`. The component debounces internally (default 250 ms) and fires `onSearch(q)` after the delay — never wire your filter logic to `onChange` on SearchInput because it does not expose one.",
      "DO: choose controlled vs uncontrolled deliberately. Pass `value` + `onValueChange` together for controlled mode; optionally add `onSearch` for debounced effects (e.g. when search state lives in a URL param or shared parent). For local-only ephemeral search pass only `defaultValue` + `onSearch` — omitting `value` puts the component in uncontrolled mode.",
      "DO: supply an `ariaLabel` (or visible `label`) when no adjacent label exists. Without either prop, SearchInput falls back to the i18n key `common.search` rendered as a visually-hidden `<Label>` — still accessible, but providing a context-specific string (e.g. `ariaLabel='請求書を検索'`) is more descriptive for screen readers.",
      "DON'T: use SearchInput inside a `<form>` expecting native form submission. The component has no `name` prop and does not emit a form field value — it is a filter-trigger widget. For a form search field, use a plain `Input` inside `FormField`.",
      "DON'T: hand-roll a debounced input when you need a search box. SearchInput ships the debounce, clear button (×), search icon, and accessible label — recreating these with a raw `<Input>` adds code and misses the UX contract.",
      "DON'T: place SearchInput inside a `ToolbarGroup` wrapper — `ToolbarGroup` is for Select/DatePicker controls with a label chip. SearchInput goes directly as a child of `Toolbar` (or standalone above a table), not wrapped in `ToolbarGroup`."
    ],
    "useCases": [
      "List-page filter bar: placed as the first child of `Toolbar` (before any `ToolbarGroup` children) to drive text-based filtering of a `DataTable`. The `onSearch` callback updates a query param or state variable that the table's data fetch reads.",
      "Inline client-side search over a small in-memory list (e.g. a sidebar nav list, a transfer panel, a settings category list) where results narrow immediately as the user types without a server round-trip — use uncontrolled mode (`defaultValue`) so no state is needed in the parent.",
      "URL-synced search: controlled mode where `value` comes from `useSearchParams()` and `onSearch` pushes to the URL, enabling deep-linkable, bookmarkable filtered views on invoice/transaction/customer index pages.",
      "Panel or dialog search: filtering a long dropdown list, a tree, or a multi-item selection panel that does not use the built-in `Command` palette — SearchInput provides the search box while the parent renders the filtered result set.",
      "Toolbar search on a data-heavy accounting page (e.g. journal-entry search, partner lookup in a subledger view) where the 250 ms debounce prevents a flood of API calls on every keystroke without requiring the developer to implement debounce logic."
    ]
  },
  {
    "example": "import { RecordPicker } from \"@godxjp/ui/data-entry\";\n\n<RecordPicker\n  mode=\"multiple\"\n  count={totalApprovers}          // the WHOLE set, from the server\n  loadOptions={async ({ query, filters }) =>\n    api.approvers({ q: query, role: filters.role })\n  }\n  filters={[\n    { name: \"role\", label: \"Role\", options: roleOptions },\n  ]}\n  selectedOptions={form.approvers}  // labels the form already has\n  value={form.approverIds}\n  onValueChange={(ids) => form.setApproverIds(ids as string[])}\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "RecordPicker",
    "props": [
      {
        "defaultValue": "\"single\"",
        "description": "Decides the VALUE SHAPE and the dialog's commit model. `single` fires onValueChange with a single value (or null) and closes on the pick; `multiple` fires an array and commits only on Confirm, so a mis-click in a list of ten thousand is undone by Cancel rather than by re-finding the row.",
        "name": "mode",
        "type": "\"single\" | \"multiple\""
      },
      {
        "defaultValue": "\"auto\"",
        "description": "TWO ENTRY POINTS AT ONCE, instead of \"dropdown OR dialog\" (gh#944). `auto` keeps the original behaviour: `threshold` picks one. `inline` opens both — a typeable field with suggestions REGARDLESS of `count`, and a 「検索」 button beside it that always shows and opens the dialog with filters + pagination; the half-typed text carries into the dialog as its initial query. Reach for it when `count > threshold` is almost always true: a consumer measured customer projects at hundreds-to-thousands of records, so the dialog-only branch was the one users met most, and there they lost the ability to type a key they already knew. The threshold answers \"is the set big\"; the real question is \"does this person already know what they want\" — `inline` stops forcing a choice. NOTE: the forwarded ref lands on the <input> under `inline` and on the trigger <button> otherwise.",
        "name": "shape",
        "type": "\"auto\" | \"inline\""
      },
      {
        "defaultValue": "10",
        "description": "The dropdown/dialog switch. A dropdown is the right control for eight people and the wrong one for eight hundred; this is where that line is drawn, once, by a service rather than per screen.",
        "name": "threshold",
        "type": "number"
      },
      {
        "description": "Size of the WHOLE set, which only a server knows — a page of results does not, so never pass `rows.length` from a fetch. Omitted with `options` it is their length; omitted with `loadOptions` the dialog shape is assumed (you cannot count what you have not fetched).",
        "name": "count",
        "type": "number"
      },
      {
        "description": "Static rows, filtered client-side. Provide this OR `loadOptions`. Same row shape Select takes, `group` included — a mixed 'users and roles' picker is two groups, not two components.",
        "name": "options",
        "type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
      },
      {
        "description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost. `cursor` is EXACTLY the `nextCursor` your previous response returned for the same `query` (undefined on the first page) — the same in all three shapes, dropdown included; an offset or an opaque token both work. Before gh#964 the dropdown sent the page NUMBER instead, so an offset server read \"2\" as offset 2 and returned duplicate rows; if you worked around that, remove the workaround.",
        "name": "loadOptions",
        "type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
      },
      {
        "description": "Filter controls rendered above the dialog list; their values reach `loadOptions({ filters })`. This is the 'search condition' half of the report — picking an issue by key alone fails, picking it by status + type + assignee works.",
        "name": "filters",
        "type": "{ name: string; label: string; options: { value: string; label: string }[] }[]"
      },
      {
        "description": "Labels for values the picker HOLDS but has not loaded. On first render there is no result page at all, so without this a chip renders a raw id; it is merged ahead of anything that loads later, so the chip also survives a query that does not return its row.",
        "name": "selectedOptions",
        "type": "SearchSelectOptionProp[]"
      },
      {
        "description": "A real row meaning \"none\" (an unassigned owner), pinned first — NOT a cleared field. The distinction matters: \"no assignee\" is a value the record holds and the server stores, while an empty field is the absence of an answer.",
        "name": "emptyOption",
        "type": "{ value: string; label: string }"
      },
      {
        "description": "Controlled value. Array for `multiple`, single value (or null) otherwise.",
        "name": "value",
        "type": "string | string[] | null"
      },
      {
        "description": "Receives the SHAPE you passed in — never a one-element array for a single picker, which the consumer would have to unwrap at every call site.",
        "name": "onValueChange",
        "type": "(value: string | string[] | null) => void"
      },
      {
        "description": "Uncontrolled initial value, same shape as `value`. Use `value` + `onValueChange` when a form owns the state.",
        "name": "defaultValue",
        "type": "string | string[] | null"
      },
      {
        "description": "Trigger text while nothing is chosen. Defaults to the localized `dataEntry.recordPicker.placeholder`; once a value exists the trigger shows chips instead.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Field name, normally supplied by FormField. Declared explicitly because BOTH branches must keep it: a server's 422 is pinned to `data-field`, and a consumer measured the dropdown branch dropping it while the dialog branch kept it (gh#942).",
        "name": "data-field",
        "type": "string"
      },
      {
        "description": "Dialog heading. Defaults to the localized `dataEntry.recordPicker.dialogTitle`.",
        "name": "dialogTitle",
        "type": "string"
      },
      {
        "description": "antd `notFoundContent` — shown when the search finds nothing, in the dropdown shape and the dialog alike (default: localized `dataEntry.recordPicker.empty`). Say WHAT has no match, e.g. 「該当するファイルはありません」.",
        "name": "notFoundContent",
        "type": "ReactNode"
      },
      {
        "description": "Text of the dialog's load-more button (default: localized `dataEntry.recordPicker.more`). Only the label — the button still appears only while the server returns a `nextCursor`.",
        "name": "loadMoreLabel",
        "type": "ReactNode"
      },
      {
        "description": "Control height of the trigger, on the shared control ladder.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      }
    ],
    "related": [
      "Select — what this renders at or under the threshold. Use Select directly when the set is small and fixed.",
      "Command — the dialog list is a Command with `shouldFilter={false}`; the query is answered by your server, not by cmdk over a page it cannot see.",
      "DataTable — for displaying and acting on records rather than picking one.",
      "Dialog — the over-threshold branch; `RecordPicker` opens one for you."
    ],
    "rules": [],
    "storyPath": "data-entry/record-picker.tsx",
    "tagline": "ONE picker whose SHAPE follows the size of the set behind it: at or under `threshold` it IS a Select (dropdown with search); over it, a Dialog with search + consumer-declared filters, chips, and a confirm step for `multiple`. Reach for it when the same field may hold eight records on one tenant and eight thousand on another.",
    "usage": [
      "DO pass `count` from the server whenever the list is server-backed. It is the only honest input to the threshold decision — `rows.length` describes the page you fetched, not the set behind it, so a picker over 9000 records would render as a dropdown showing 20.",
      "DO declare `filters` in your own vocabulary (`{ name: 'status', … }`) — the names travel to `loadOptions({ filters })` unchanged, so the server reads the keys it already has.",
      "DO pass `selectedOptions` for any value the form arrives holding. On first render there is no result page, so a chip without it shows a raw id; with it the chip is correct before anything loads and survives a query that excludes its row.",
      "DO use `emptyOption` for \"unassigned\" rather than leaving the field empty — a record that HOLDS \"no owner\" is a different state from a field nobody answered, and only the first round-trips.",
      "DON'T reach for this when the set is small and fixed (a status, a priority, a country): that is `Select`, and RecordPicker just renders Select for you with an extra layer of props in between.",
      "DON'T use it as a table replacement. It picks records; it does not display, sort or edit them — a screen that needs columns and bulk actions wants DataTable.",
      "DON'T nest it inside a Popover. It opens a Dialog, and a dialog inside a popover loses focus containment; it is designed to be opened from a field inside a Dialog or Sheet, which is where the consumer reports put it."
    ],
    "useCases": [
      "Assigning an owner on a busy admin screen where one tenant has 8 people and another has 800.",
      "An approval step that mixes USERS and ROLES in one field — two `group`s in one list, chips distinguishable by the option's `icon`.",
      "Picking an issue/record by key OR title with status/type/assignee filters, replacing a free-text key field that only failed validation after save."
    ]
  },
  {
    "absorbed": [
      "Combobox",
      "Autocomplete",
      "CountrySelect",
      "SearchSelect",
      "Typeahead",
      "AsyncSelect",
      "TagInput"
    ],
    "example": "import {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n} from \"@godxjp/ui/data-entry\";\nimport { Text } from \"@godxjp/ui/general\";\n// as=\"span\" on both: an option row is a phrasing context, so a <div> would be invalid HTML.\nimport { Flex } from \"@godxjp/ui/layout\";\n\n// ── 1. Data-driven (Ant-style) — static list, no search ──────────────────────\nexport function StatusSelect({ value, onChange }) {\n  return (\n    <Select\n      value={value}\n      onValueChange={onChange}\n      options={[\n        { value: \"draft\",    label: \"Draft\" },\n        { value: \"sent\",     label: \"Sent\" },\n        { value: \"paid\",     label: \"Paid\" },\n        { value: \"overdue\",  label: \"Overdue\" },\n      ]}\n      placeholder=\"Select status\"\n      name=\"status\"\n      id=\"status\"\n    />\n  );\n}\n\n// ── 2. Data-driven, searchable static list with groups ────────────────────────\nexport function CurrencySelect({ value, onChange }) {\n  return (\n    <Select\n      value={value}\n      onValueChange={onChange}\n      showSearch\n      options={[\n        { value: \"JPY\", label: \"Japanese Yen\",  group: \"Asia\" },\n        { value: \"VND\", label: \"Vietnamese Dong\", group: \"Asia\" },\n        { value: \"EUR\", label: \"Euro\",           group: \"Europe\" },\n        { value: \"GBP\", label: \"Pound Sterling\", group: \"Europe\" },\n      ]}\n      placeholder=\"Select currency\"\n      searchPlaceholder=\"Search currencies…\"\n      name=\"currency\"\n    />\n  );\n}\n\n// ── 3. Data-driven, async (loadOptions) ──────────────────────────────────────\nexport function AccountSelect({ value, onChange, selectedLabel }) {\n  async function loadOptions({ query, page }) {\n    const res = await fetch(`/api/accounts?q=${query}&page=${page}`);\n    const json = await res.json();\n    return { options: json.data, hasMore: json.hasMore };\n  }\n  return (\n    <Select\n      value={value}\n      onValueChange={onChange}\n      loadOptions={loadOptions}\n      selectedLabel={selectedLabel}\n      placeholder=\"Search accounts…\"\n      renderOption={(opt) => (\n        <Flex as=\"span\" align=\"center\" gap=\"xs\">\n          <Text as=\"span\" tone=\"muted\" mono>\n            {opt.value}\n          </Text>\n          {opt.label}\n        </Flex>\n      )}\n      name=\"account_id\"\n    />\n  );\n}\n\n// ── 4. Compound API — custom trigger content ──────────────────────────────────\nexport function PrioritySelect({ value, onValueChange }) {\n  return (\n    <Select value={value} onValueChange={onValueChange}>\n      <SelectTrigger size=\"sm\" id=\"priority\">\n        <SelectValue placeholder=\"Priority\" />\n      </SelectTrigger>\n      <SelectContent>\n        <SelectGroup>\n          <SelectLabel>Urgency</SelectLabel>\n          <SelectItem value=\"high\">High</SelectItem>\n          <SelectItem value=\"medium\">Medium</SelectItem>\n        </SelectGroup>\n        <SelectSeparator />\n        <SelectItem value=\"low\">Low</SelectItem>\n      </SelectContent>\n    </Select>\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Select",
    "props": [
      {
        "description": "Select several options; value/defaultValue become string arrays. `tags` additionally ACCEPTS what was typed (a value that is not in the list), which is antd's own split between the two. `tags` needs no `options` (`<Select mode=\"tags\" />` works) and only an explicit `disabled` disables it (gh#1063). While text is typed the tags list reads create row, then an exact (case-folded) match, then options, then held values — the first enabled row is the Enter target, never a held tag (gh#1064). The popup is a picker: compact unruled rows (--search-select-option-line-height), a list capped at --search-select-list-max-height (16rem, antd listHeight 256) that scrolls inside, the search as the panel header line with no ring of its own, and the picked rows marked by a check (menuItemSelectedIcon default) + weight while the row background means pointer/keyboard position only. A facet list that wants ruled rows composes `Command split` (gh#699) itself.",
        "name": "mode",
        "type": "\"multiple\" | \"tags\""
      },
      {
        "description": "Maximum selected options; selected items remain removable.",
        "name": "maxCount",
        "type": "number"
      },
      {
        "description": "Collapse extra selected labels into a \"+N\" chip. Without it (mode=\"multiple\"/\"tags\"), the chips WRAP and the trigger grows to hold them, antd's default — a chip is never clipped out of sight. Set it when the trigger must stay one line (a dense rail or table cell).",
        "name": "maxTagCount",
        "type": "number"
      },
      {
        "description": "Static option list. Passing this (or loadOptions) switches Select from the compound API to the data-driven API. An entry may be one of antd's nested GROUPS ({ label, options }) instead of a row; its heading becomes the same `group` a flat row carries. Rows in a foreign shape ({id,name}) go through fieldNames. Each option has { value, label, sublabel?, icon?, group?, disabled? }. `icon` (avatar / flag / lucide node) renders before the label in the rows AND on the trigger once selected. group buckets the option under an optgroup-style heading.",
        "name": "options",
        "type": "(SearchSelectOptionProp | { label: string; options: SearchSelectOptionProp[] })[]"
      },
      {
        "description": "Async remote fetcher. Receives { query, page } (1-based). Must return { options, hasMore? }. Implies showSearch=true automatically.",
        "name": "loadOptions",
        "type": "(params: SearchSelectLoadParamsProp) => Promise<SearchSelectLoadResultProp>"
      },
      {
        "defaultValue": "true when loadOptions is set or mode is multiple/tags, false otherwise",
        "description": "Toggle the searchable combobox mode vs the plain listbox. The OBJECT form is antd's and configures the search in one place (passing it also turns search ON). NOTE the argument order: `showSearch.filterOption(input, option)` is antd's, while the long-standing top-level `filterOption(option, query)` keeps this library's; `filterOption: false` keeps every row, for a list the server already filtered.",
        "name": "showSearch",
        "type": "boolean | { filterOption?: boolean | ((input, option) => boolean); optionFilterProp?: \"label\" | \"value\" | \"sublabel\"; filterSort?; searchValue?: string; onSearch?: (value: string) => void; autoClearSearchValue?: boolean }"
      },
      {
        "defaultValue": "\"\"",
        "description": "Controlled selected value (data-driven API). Pass an empty string to represent no selection.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial value (data-driven API). The trigger shows the matching option's label at rest — including in searchable (showSearch) mode — so an edit form pre-filled from server data renders the label, not the placeholder. Selected option is marked by a background tint (no check icon).",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Change handler for the data-driven API. Receives the new value string and the matching option object. The signature follows `mode` / `labelInValue`, and a bare `(value, option) => …` is inferred for each (no annotation needed under `strict`, gh#679): single `string` / `SelectOption | undefined`; `mode=\"multiple\" | \"tags\"` `string[]` / `SelectOption[] | undefined`; `labelInValue` the `{ value, label }` shapes.",
        "name": "onValueChange",
        "type": "(value: string, option?: SearchSelectOptionProp) => void"
      },
      {
        "description": "Custom per-option renderer for the dropdown ROWS. Defaults to label + optional sublabel. Does not change the trigger — use `labelRender` for that.",
        "name": "renderOption",
        "type": "(option: SearchSelectOptionProp) => React.ReactNode"
      },
      {
        "description": "Custom renderer for the SELECTED value shown on the TRIGGER (`labelRender`) — avatar + name + role badge, etc. `option` is undefined for an async preset whose page hasn't loaded. Only used while a value is selected; the placeholder still shows when empty.",
        "name": "labelRender",
        "type": "(selected: { value: string; label: React.ReactNode; option?: SearchSelectOptionProp }) => React.ReactNode"
      },
      {
        "description": "Display label for the current value when its option is not in the loaded page (async). Prevents a flash of the raw id.",
        "name": "selectedLabel",
        "type": "string"
      },
      {
        "description": "Leading icon shown on the trigger for the current value when its option isn't loaded yet (async preset) — the trigger counterpart of `selectedLabel`, so an edit form pre-filled from the server shows the avatar/flag at rest.",
        "name": "selectedIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "Placeholder shown in the trigger when no value is selected.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Placeholder inside the search input (combobox mode only).",
        "name": "searchPlaceholder",
        "type": "string"
      },
      {
        "description": "Message rendered when the filtered list is empty.",
        "name": "emptyMessage",
        "type": "string"
      },
      {
        "description": "Message rendered while loadOptions is resolving.",
        "name": "loadingMessage",
        "type": "string"
      },
      {
        "description": "Message rendered when an async loadOptions REJECTS — a distinct state from empty/loading. Defaults to a localized 'Couldn’t load options'. The panel shows this instead of a blank surface or a misleading 'no results'.",
        "name": "errorMessage",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Show the clear ✕ when a value is selected (data-driven API). Off by default, as antd `allowClear` — a required select is never one click from empty. Pass it (or `allowClear`) on an optional field whose empty state is a valid answer.",
        "name": "clearable",
        "type": "boolean"
      },
      {
        "description": "Label for the clear row (data-driven combobox mode).",
        "name": "clearLabel",
        "type": "string"
      },
      {
        "description": "Disables the entire select.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Searchable mode only (showSearch/loadOptions). Value is shown (and the clear affordance hidden) but the popover cannot be opened — no new pick, no search. Mirrors the Input/NumberInput readOnly contract: stays focusable and still submits its value, unlike disabled.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Searchable mode only. Height tier forwarded to the SearchSelect trigger Button. For the compound API use SelectTrigger's own size prop instead (below).",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "\"full\"",
        "description": "Trigger width on the data-driven API (options / loadOptions), searchable or not — the same axis SelectTrigger carries on the compound API. `full` is what a form field wants; `auto` is what a filter bar wants, so two Selects share one row instead of stacking (CONSUMER-RULES rule 5); `bounded` holds one width from --control-bounded-width for a trigger whose value varies in length. Never wrap a Select in a fixed-width Flex to get this.",
        "name": "width",
        "type": "\"full\" | \"auto\" | \"bounded\""
      },
      {
        "description": "Searchable mode only. Controlled popover open state (uncontrolled by default). Pair with onOpenChange. With `mode=\"tags\"`, `open={false}` (antd's spelling of \"a tags field whose list never opens\") renders the INLINE chip field instead — no popup, no combobox: type + Enter or a `tokenSeparators` character adds a tag, Backspace on an empty draft drops the last, a pasted \"a, b, c\" becomes three tags, the draft commits on blur, and `name` submits the tags comma-joined in one hidden input. `onValueChange` then receives the tag array only. This is what the retired `TagInput` was (v32 #1223).",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Searchable mode only. Fires on every open/close attempt — including ones ignored because open is externally pinned — so a controlled consumer stays in sync.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Searchable mode only. Controlled search-box query (uncontrolled by default). Pair with onSearchChange.",
        "name": "search",
        "type": "string"
      },
      {
        "description": "Searchable mode only. Fires on every keystroke in the search box.",
        "name": "onSearchChange",
        "type": "(query: string) => void"
      },
      {
        "description": "Searchable mode only, static options (ignored with loadOptions, which owns its own server-side filtering). Overrides the default label/value substring filter. Only consulted while the query is non-empty.",
        "name": "filterOption",
        "type": "(option: SearchSelectOptionProp, query: string) => boolean"
      },
      {
        "description": "Searchable mode only. Custom error slot, overriding the default errorMessage row. retry() reloads from the first page.",
        "name": "renderError",
        "type": "(params: { message: string; retry: () => void }) => React.ReactNode"
      },
      {
        "description": "Searchable mode only. Custom 'load more' affordance appended below the list while another page is available — pairs with (does not replace) the built-in scroll-triggered pagination.",
        "name": "renderLoadMore",
        "type": "(params: { hasMore: boolean; loading: boolean; loadMore: () => void }) => React.ReactNode"
      },
      {
        "description": "Form field name. Submits the selected value via a hidden input (data-driven API). Required for uncontrolled form submission.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "HTML id for the trigger element. Wire to a <label htmlFor> for a11y.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Additional CSS classes applied to the trigger.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Test id on the trigger. Option items get ${data-testid}-option-${value} automatically.",
        "name": "data-testid",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Compound API only. Size variant on the SelectTrigger sub-component.",
        "name": "SelectTrigger size",
        "type": "\"sm\" | \"md\""
      },
      {
        "defaultValue": "\"full\"",
        "description": "Compound API only. `full` fills the field column (inside FormField). `auto` sizes the trigger to its label — use it in a PageContainer `extra` slot, a toolbar or a footer row, where a full-width trigger swallows the row and truncates its siblings.",
        "name": "SelectTrigger width",
        "type": "\"full\" | \"auto\""
      },
      {
        "defaultValue": "true",
        "description": "Compound API only. Set false to omit the built-in chevron disclosure indicator from the DOM entirely (not a CSS hide) — for specialized triggers (icon-only, etc.) that render their own affordance, so no consumer descendant CSS is needed.",
        "name": "SelectTrigger showIndicator",
        "type": "boolean"
      },
      {
        "description": "antd `status`. `error` recolours the control AND sets aria-invalid (a colour-only error fails WCAG 2.2 SC 1.4.1); `warning` recolours only. An aria-invalid injected by FormField always wins.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "antd `variant` — the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "antd `loading` — the FIELD is in flight: the trailing chevron becomes a spinner and the trigger reports aria-busy. Distinct from the list-level spinner an async loadOptions shows inside the popup.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "antd `defaultOpen` — uncontrolled initial popup state. The popup opens once every running ancestor animation has finished (a Popover sliding in), so inside a Popover the listbox is placed below its settled trigger exactly like a click-open (gh#708); with nothing animating it is open on the first render. onOpenChange is not called for this initial open.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd `allowClear` — the clear ✕ on the trigger while a value is selected. Default false, as in antd. The object form replaces the ✕ icon and/or its accessible label. Beats `clearable` when both are given.",
        "name": "allowClear",
        "type": "boolean | { clearIcon?: React.ReactNode; label?: string }"
      },
      {
        "description": "antd `onClear` — fires after the value is cleared through the ✕.",
        "name": "onClear",
        "type": "() => void"
      },
      {
        "description": "antd `notFoundContent` — the node shown when the popup has nothing to list. Outranks the string-only emptyMessage.",
        "name": "notFoundContent",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `autoClearSearchValue` (default true) — clear the search box after a pick / on close. Set false to resume the same filtered list on the next open.",
        "name": "autoClearSearchValue",
        "type": "boolean"
      },
      {
        "description": "antd `filterSort` — orders what filterOption kept. Static options only; with loadOptions the server owns the order. Never mutates the caller's array.",
        "name": "filterSort",
        "type": "(a: SearchSelectOptionProp, b: SearchSelectOptionProp, info: { searchValue: string }) => number"
      },
      {
        "description": "antd `optionRender` — per-option renderer in antd's own (option, { index }) shape. Outranks the older renderOption.",
        "name": "optionRender",
        "type": "(option: SearchSelectOptionProp, info: { index: number }) => React.ReactNode"
      },
      {
        "description": "antd `menuItemSelectedIcon` — a decorative mark on the picked row. In mode multiple / tags it defaults to a check (as in antd), because there the row background means pointer/keyboard position, not selection; pass `null` to drop it. A single-value Select shows none by default.",
        "name": "menuItemSelectedIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `popupMatchSelectWidth`. true (default) pins the popup to the trigger width, false lets it hug its rows, a number pins it to that many pixels.",
        "name": "popupMatchSelectWidth",
        "type": "boolean | number"
      },
      {
        "description": "antd `fieldNames` — read rows in a FOREIGN shape ({id,name,children}) without copying them into a second array first. The same spelling Cascader and TreeSelect take. A foreign row needs a cast at the call site, exactly as theirs does.",
        "name": "fieldNames",
        "type": "{ label?: string; value?: string; options?: string; groupLabel?: string; disabled?: string }"
      },
      {
        "description": "antd `labelInValue` — value and onValueChange speak {value,label} instead of a bare string (an array of them in multiple/tags). It earns its place on the async EDIT form: a record holding {value:'52',label:'東京本社'} renders the pick immediately, with no options page loaded and no flash of the raw id.",
        "name": "labelInValue",
        "type": "boolean"
      },
      {
        "description": "antd `prefix` — a node pinned BEFORE the value on the trigger (a currency mark, an icon). Deliberately not aria-hidden: for role=combobox the trigger's text is the VALUE, and a prefix that is part of the value belongs in it. The NAME still comes from the label.",
        "name": "prefix",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `suffixIcon` — replaces the trailing chevron; `null` removes the indicator entirely (antd's own replacement for the deprecated showArrow) — and, with no clear and no loading, the trigger stops reserving the chevron's end padding, so a `width=\"auto\"` trigger ends at its value. While a value is clearable the ✕ owns that seat, as in antd.",
        "name": "suffixIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `placement`, spelled on the LOGICAL inline axis (antd's bottomLeft/topRight cannot mirror for an RTL layout). Absent = below, start-aligned, with collision flipping — what a picker wants.",
        "name": "placement",
        "type": "\"bottomStart\" | \"bottomEnd\" | \"topStart\" | \"topEnd\""
      },
      {
        "description": "antd `popupRender` — wrap the popup's own node to add a footer, a 'create' action or a hint line. It must still render originNode: dropping it leaves a popup with no options.",
        "name": "popupRender",
        "type": "(originNode: React.ReactNode) => React.ReactNode"
      },
      {
        "description": "antd `listHeight` — the option list's max height in px for THIS instance. It overrides the --select-content-max-height token rather than hard-coding a height, so the token stays the default everywhere else.",
        "name": "listHeight",
        "type": "number"
      },
      {
        "description": "antd `onPopupScroll` — fires on the option list's own scroll. It runs BESIDE the built-in infinite scroll (loadOptions paging), never instead of it.",
        "name": "onPopupScroll",
        "type": "(event: React.UIEvent<HTMLElement>) => void"
      },
      {
        "description": "antd `optionFilterProp` — which field the default filter matches while searching. Unset matches BOTH label and value (this library's long-standing behaviour); antd's own default is value alone.",
        "name": "optionFilterProp",
        "type": "\"label\" | \"value\" | \"sublabel\""
      },
      {
        "description": "antd `tokenSeparators` (multiple/tags) — characters that commit what has been typed. Typing or PASTING 'a,b,c' commits three values in ONE onValueChange. A pasted run is read off the clipboard, so a '\\n' separator works even though a single-line input strips newlines.",
        "name": "tokenSeparators",
        "type": "string[]"
      },
      {
        "defaultValue": "true",
        "description": "mode=\"tags\" — whether typed text may become a NEW value (gh#1052). false, or a predicate returning false for the text, hides the create row and drops unknown tokenSeparators tokens; values already held stay listed so they can be removed. Not an antd prop.",
        "name": "allowCreate",
        "type": "boolean | ((text: string) => boolean)"
      },
      {
        "description": "mode=\"tags\" — the create row's content (gh#1052). Default is the localised Create “text” / 「text」を作成 / Tạo “text”. The committed value is the typed text either way. antd shows the bare text instead — a documented deviation.",
        "name": "createLabel",
        "type": "(text: string) => React.ReactNode"
      },
      {
        "description": "mode=\"tags\" — fires when a value that NO option carries joins the selection (create row, Enter on it, or a tokenSeparators run), beside onSelect/onValueChange. The way to tell invented text from an option id. Not an antd prop.",
        "name": "onCreate",
        "type": "(text: string) => void"
      },
      {
        "defaultValue": "false",
        "description": "Compare typed text case-sensitively (gh#1053). Default false: the built-in filter, the tags create row and a tokenSeparators run all case-fold, so typing \"bug\" when \"Bug\" exists finds Bug instead of offering a duplicate. antd folds its filter but compares the create row exactly; true restores that. A custom filterOption is unaffected.",
        "name": "caseSensitive",
        "type": "boolean"
      },
      {
        "description": "antd `maxTagTextLength` (multiple/tags) — cut each chip's TEXT to this many characters (an ellipsis marks the cut). The value keeps its whole label.",
        "name": "maxTagTextLength",
        "type": "number"
      },
      {
        "defaultValue": "false",
        "description": "multiple/tags — hold the panel's picks as a DRAFT until OK commits them (gh#1248; antd's `needConfirm`, the same word TimePicker/DatePicker use). Row toggles move a pending set and report nothing; the footer's OK (Button size sm) fires ONE onValueChange with the whole set and closes; its Clear empties the pending set; Escape or an outside click discards the draft and the trigger keeps the committed values. onSelect/onDeselect do not fire for draft toggles. A chip's ✕ and Backspace on the closed trigger still commit at once. Default false: every toggle commits.",
        "name": "needConfirm",
        "type": "boolean"
      },
      {
        "description": "antd `tagRender` (multiple/tags) — render one chip yourself. Exactly the shape TagInput's tagRender takes. The onClose handed in is the same remover the built-in ✕ calls, so a custom chip cannot end up unremovable; supplying tagRender withdraws the built-in ✕.",
        "name": "tagRender",
        "type": "(props: { value: string; label: React.ReactNode; onClose: () => void; index: number; disabled: boolean }) => React.ReactNode"
      },
      {
        "description": "antd `onSelect` / `onDeselect` — fires as one option JOINS or LEAVES the selection, beside onValueChange (which reports the whole value).",
        "name": "onSelect / onDeselect",
        "type": "(value: string, option: SearchSelectOptionProp) => void"
      },
      {
        "description": "antd `maxTagPlaceholder` — the node standing in for the values maxTagCount hid. A function receives the omitted values, so '+3 件' or a tooltip listing them is possible.",
        "name": "maxTagPlaceholder",
        "type": "React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)"
      }
    ],
    "related": [
      "Segmented — the same choice when the option set is small and worth showing at once. Select hides its options behind a trigger; Segmented lays them out, which reads better for 2-4 mutually exclusive options.",
      "Select showSearch / loadOptions — the searchable combobox is a MODE of this component (an internal engine, not a public SearchSelect export); never build your own.",
      "TreeSelect — use when options are hierarchical (parent/child tree). Not a drop-in for Select; has expand/collapse and a separate treeData prop.",
      "Select with showSearch — use Select (with the `showSearch` prop) for typeahead/autocomplete lookup patterns instead of the removed Autocomplete component.",
      "RadioGroup — use instead of Select when there are 2-4 mutually exclusive choices that must all be visible at once without opening a popover.",
      "Command — the cmdk-powered list for free-text command search (a palette, a slash menu); a value-picking combobox is Select showSearch — there is no Combobox component."
    ],
    "rules": [
      3,
      6,
      23
    ],
    "storyPath": "data-entry/select.tsx",
    "subParts": [
      "SelectContent",
      "SelectGroup",
      "SelectItem",
      "SelectLabel",
      "SelectScrollDownButton",
      "SelectScrollUpButton",
      "SelectSeparator",
      "SelectTrigger",
      "SelectValue"
    ],
    "tagline": "Polymorphic single-select: pass options/loadOptions for the data-driven (Ant-style) API, or compose sub-parts manually — never use a raw <select>.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "FREE-TEXT TAGS WITH NO LIST (keywords, email recipients, labels typed from scratch): `<Select mode=\"tags\" open={false} value={tags} onValueChange={setTags} />` — the inline chip field. Use plain `mode=\"tags\"` (popup) only when there is a list of existing tags to pick from. There is no `TagInput` component.",
      "DO use the data-driven API (options/loadOptions) for straightforward selects — it handles grouping, search, async, and custom rendering automatically. Only reach for the compound API when you need to inject arbitrary content into the trigger or listbox.",
      "DO pass name= on the data-driven Select so the value is submitted with a native form or Inertia useForm. Without name= the value is React-only and will not appear in form data.",
      "NAMING A SELECT WITH NO VISIBLE LABEL: put `aria-label` on `<Select>`, NOT on `<SelectTrigger>`. Both render the identical button attribute — Select forwards its name down through SelectFieldA11yContext and the trigger writes last — but only the root spelling also names the react-aria field, and a root that cannot see a name warns once per render (gh#869). Inside a FormField or Field, pass nothing: the label id reaches both levels on its own. `aria-labelledby` pointing at your own element works on either level and is the right spelling when a visible heading already names the control.",
      "READING THE SELECTED CODE FROM THE DOM: the trigger publishes `data-value` = the selected VALUE, alongside the `data-field` key it inherits from FormField. Use that in e2e tests and screen automation — the trigger's visible text is the option LABEL (東京本社), and the only other place the code lives is the aria-hidden, 1px-clipped native <select> react-aria renders so a native submit (and browser autofill) carries the value. `data-value` is absent while nothing is selected, and it tracks uncontrolled picks too.",
      "DO use loadOptions + selectedLabel together for async selects: selectedLabel prevents a flash of the raw id string while the first page loads.",
      "A Select is safe inside a draggable element (a Kanban card with draggable=true) and inside a `contain: paint` / `transform` app region: the aria-hidden native <select> fallback is held at its static position beside the trigger (position: absolute, 1px clipped), so the browser's drag image stays the card's own box instead of reaching to the region's corner (gh#708). No wrapper or consumer CSS is needed.",
      "DO name the control with FormField, aria-label, or a <label htmlFor> pointing at the trigger id — all three work. (An earlier version of this entry said htmlFor does NOT name the trigger. That was wrong: the trigger is a <button>, which is a labelable element, so <label for> does name it — src/components/data-entry/__tests__/select-rac.test.tsx pins it on both the old Radix base and the react-aria one, because 17 godx-task files name their Selects exactly that way.) What role=combobox does NOT do is take a name from its own content, so the visible value is the VALUE, never the name — a Select with no label of any kind is anonymous. Wrapping in <FormField label=…> stays the route that also wires helper, error and required. This holds for BOTH APIs; anything set directly on SelectTrigger wins.",
      "DO treat loading / no-options / error / disabled as DISTINCT states. A data-driven Select never opens a blank popover: a static options=[] list auto-disables the trigger (opening it would show nothing), while an async loadOptions shows a loading row, then either the options, a localized empty affordance (override with emptyMessage), or an error affordance if the fetch rejects (override with errorMessage). Disable the Select when there is nothing to pick AND no async loader; keep it enabled (it opens to load/search) whenever loadOptions is set.",
      "DON'T mix the two APIs: once you pass options or loadOptions, Select is data-driven — all compound sub-parts (SelectTrigger, SelectContent, SelectItem) are rendered internally. Do not wrap them manually.",
      "DON'T use a raw <select> element. Select is the one control for all single-select use cases. The only allowed raw <select> is a hidden aria-hidden sr-only element kept as an e2e hook paired with a visible Select.",
      "COMPOUND API sub-parts (when NOT using options/loadOptions): Select → SelectTrigger (contains SelectValue) → SelectContent → SelectItem. Optionally wrap items in SelectGroup + SelectLabel for headings, or add SelectSeparator between sections.",
      "DO reach for open/onOpenChange (searchable mode) to drive the popover from outside — e.g. opening it programmatically after a validation error — and search/onSearchChange to seed or read the query text. Both fall back to internal state when omitted; onOpenChange/onSearchChange still fire either way so a controlled consumer stays in sync.",
      "DO use readOnly (searchable mode) for a value that must stay visible and submittable but not editable in this view — it differs from disabled: the control stays focusable and its value still submits. allowClear / clearable is ignored while readOnly.",
      "DO use filterOption (searchable mode, static options) when the default label/value substring match isn't right — e.g. filtering by a hidden code field. It is NOT consulted when loadOptions is set (that fetcher owns its own filtering).",
      "DO use renderError + renderLoadMore (searchable mode) to replace the default error row with a branded retry affordance, or to pair a manual 'load more' button with (not instead of) the built-in scroll-triggered pagination.",
      "DO set SelectTrigger showIndicator={false} (compound API) on a specialized trigger — icon-only, or one with its own affordance — instead of hiding [data-slot=select-chevron] with consumer CSS."
    ],
    "useCases": [
      "Status filter on an invoice list — pass options=[{value:'draft',label:'Draft'},{value:'paid',label:'Paid'}] with onChange to drive a query param; no search needed so omit showSearch.",
      "Legal-entity switcher — static options list with showSearch=true for client-side filtering when there are many entities; use selectedLabel to show the entity name before the full list loads.",
      "Account category picker backed by an API — pass loadOptions to stream pages of accounts as the user types; use renderOption to show account code + name side by side; pass selectedLabel so the trigger shows the name on first render.",
      "Grouped currency picker — set option.group='Asia' / 'Europe' on each option; the plain (non-search) data-driven mode renders SelectGroup headings automatically.",
      "Form field in an accounting entry — use the compound API when the trigger must show a currency flag icon alongside the SelectValue; wire SelectTrigger size='sm' for dense table rows.",
      "Required department select in a HR form — leave allowClear off (the default) so the user cannot clear the field once set; pair with name='department_id' for Inertia useForm submission. An OPTIONAL filter select passes allowClear so the user can return to 'no filter'.",
      "Async account picker whose API can fail — pass loadOptions plus errorMessage so a rejected fetch shows a clear error affordance in the panel (not a blank surface or a false 'no results'); the loading and empty states are handled automatically."
    ]
  },
  {
    "example": "import { Field, Switch } from \"@godxjp/ui/data-entry\";\n\n// Field, not a hand-rolled row: it owns the label-to-control id wiring, the description slot and\n// the row rhythm. A <div className=\"flex items-center gap-2\"> around a bare <Label> loses all three.\n<Field id=\"stackable\" label=\"他クーポンとの併用を許可\" description=\"会計時に自動で合算されます\">\n  <Switch id=\"stackable\" checked={stackable} onCheckedChange={setStackable} />\n</Field>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Switch",
    "props": [
      {
        "defaultValue": "false",
        "description": "antd `loading` — the toggle is mid-flight: a spinner replaces the thumb glyph and the control refuses the change. It reports `aria-busy`/`aria-disabled` rather than `disabled`, so a keyboard user's focus is not thrown to the next field the instant they flip a switch that saves over the network.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "antd `checkedChildren` — content shown INSIDE the track while on (`有効`, `ON`, a glyph). Rendered aria-hidden: `role=switch` + `aria-checked` already say on/off.",
        "name": "checkedChildren",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `unCheckedChildren` — the same, shown while off.",
        "name": "unCheckedChildren",
        "type": "React.ReactNode"
      },
      {
        "description": "Controlled checked state.",
        "name": "checked",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Uncontrolled initial state. Switch keeps its own state from here, so an uncontrolled toggle works without Field.",
        "name": "defaultChecked",
        "type": "boolean"
      },
      {
        "description": "Fires when toggled.",
        "name": "onCheckedChange",
        "type": "(checked: boolean) => void"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Thumb size — 'sm' for dense rows.",
        "name": "size",
        "type": "\"sm\" | \"md\""
      },
      {
        "description": "Links to a <Label htmlFor>.",
        "name": "id",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "ANNOUNCES the requirement; it does not enforce it. react-aria's Switch omits `isRequired`, and a switch is never the target of native constraint validation in this library, so this writes `aria-required=\"true\"` onto the real input and stops there. The form layer (FormField / your schema) still owns whether an unflipped switch blocks submit — pairing this with nothing that validates is how a screen reader ends up promising a check the form never makes.",
        "name": "required",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Disable the toggle.",
        "name": "disabled",
        "type": "boolean"
      }
    ],
    "related": [
      "Field — use this instead of a bare Switch whenever the toggle needs a visible label, a description, a pressable help affordance (`labelAddon`) or a validation message (`error`, which wires aria-invalid / aria-errormessage / aria-describedby for you, gh#812). Native submission is the Switch's own job: give it `name` and it renders the hidden input itself.",
      "Checkbox — use Checkbox (or CheckboxGroup) when the user is selecting one or more items from a set, or when the binary choice semantically means 'agree/select' rather than 'enable/disable'. Switch implies an immediate, persistent state change; Checkbox implies a form choice.",
      "Field — use for a binary or small-set choice rendered as radio-style cards with rich descriptions, when the visual weight of a toggle is insufficient for the decision importance.",
      "RadioGroup — use when the user must choose exactly one option from 2–4 mutually exclusive values; Switch is only appropriate for a single on/off boolean."
    ],
    "rules": [],
    "storyPath": "data-entry/switch.tsx",
    "tagline": "Toggle switch (bare), on react-aria-components. For a labelled row use Field. `role=\"switch\"` is the real `<input>`; the painted track is the `<label>` around it, and that is where `data-state`/`data-size` live.",
    "usage": [
      "DO use Switch (bare) only when you are building a custom inline toggle without a visible label — e.g., a DataTable row action column. Always pair it with a <Label htmlFor={id}> placed adjacent in the DOM; never leave it label-less for screen readers.",
      "DO pass `name` when the toggle must submit: Switch itself renders `<input type=\"hidden\" name value=\"1|0\">` beside the control, so a native <form> carries the value with no extra wiring. (The old advice here — that a bare Switch drops `name` — was never true of this component.)",
      "DO use the `size` prop ('sm' | 'md') to control thumb size. 'sm' is appropriate in dense DataTable rows or filter bars; omit it (defaults to 'md') everywhere else.",
      "DO wire controlled state: pass both `checked` and `onCheckedChange` together. For uncontrolled use pass `defaultChecked` (or neither) — Switch holds that state itself; Field is not required for it.",
      "DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field — that is exactly what Field provides, including aria-describedby, aria-invalid, error/helper text, and the hidden input. Reach for Field instead.",
      "DO link the switch to its label via matching `id` on Switch and `htmlFor` on Label. Without this pairing, clicking the label text does not toggle the switch and the a11y association is broken."
    ],
    "useCases": [
      "Inline toggle in a DataTable action cell (e.g., 'Active' column) where the label is already provided by the column header and no form submission is involved.",
      "Settings panel where a React state boolean is toggled immediately via an optimistic API call — no <form> submit, so Field's hidden input is unnecessary.",
      "Custom compound component where you compose Switch + Label yourself and need to put your own aria-* or data-* attributes on the control.",
      "Filter toolbar toggle (e.g., 'Show archived') rendered inline next to other filter controls, using size='sm' for density parity with adjacent inputs.",
      "Preview/demo UI where the switch controls a local display state (dark-mode preview, feature flag preview) with no server persistence."
    ]
  },
  {
    "example": "import { Textarea } from \"@godxjp/ui/data-entry\";\n\n<Textarea id=\"notes\" rows={4} placeholder=\"自由記述\" value={notes} onChange={(e) => setNotes(e.target.value)} />\n\n// Chat / comment composer — grows with its content, scrolls past 8 rows, collapses on send.\n<Textarea autoGrow minRows={1} maxRows={8} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder=\"メッセージを入力...\" />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Textarea",
    "props": [
      {
        "description": "Measured padding escape, stamped on the field.",
        "name": "padRaw",
        "type": "PadRawProp"
      },
      {
        "description": "Instance padding with logical asymmetric sides.",
        "name": "pad",
        "type": "PadProp"
      },
      {
        "description": "Validation state the field paints — Ant Design `status`. `error` also reports `aria-invalid`, so the red boundary and what a screen reader hears are one fact; `warning` paints only, because a warning is not a validity failure. antd's `success`/`validating` are not implemented: antd only draws them together with its `hasFeedback` icon slot, which FormField owns here.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "defaultValue": "\"outlined\"",
        "description": "Chrome level — Ant Design `variant`. `outlined` is the historical field; `filled` swaps the boundary for a tinted surface (dense forms); `borderless` drops both, for a field inside a box that already draws one. antd's fourth member `underlined` is deliberately absent — a single bottom rule is a Material convention and SmartHR, the JP authority here, draws every field as a full box. This library's older `default`/`ghost` are still accepted and resolve to `outlined`/`borderless`.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\" | \"default\" | \"ghost\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control height tier — reads the shared `--control-height` ladder.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "antd `autoSize`. `true` is `autoGrow`; the object form carries the row bounds with it, so `autoSize={{ minRows: 2, maxRows: 6 }}` is `autoGrow minRows={2} maxRows={6}`. An explicit `minRows`/`maxRows` still wins.",
        "name": "autoSize",
        "type": "boolean | { minRows?: number; maxRows?: number }"
      },
      {
        "description": "Character counter — Ant Design `count`. Counts CODE POINTS by default, so one emoji and one 全角 kanji are each worth one. It REPORTS an overrun (`data-exceeded`) and never edits the value: antd's `exceedFormatter` truncates while the user types, which in Japanese cuts a live IME conversion in half.",
        "name": "count",
        "type": "{ max?: number; show?: boolean; formatter?: (info: { value: string; count: number; max?: number }) => React.ReactNode; strategy?: (value: string) => number }"
      },
      {
        "description": "Associates with a <Label htmlFor>.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Visible text rows.",
        "name": "rows",
        "type": "number"
      },
      {
        "description": "Controlled value.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Change handler.",
        "name": "onChange",
        "type": "React.ChangeEventHandler<HTMLTextAreaElement>"
      },
      {
        "defaultValue": "false",
        "description": "Opt-in inline ✕ at the top-end that clears the field while it holds text (controlled + uncontrolled). Off by default.",
        "name": "allowClear",
        "type": "boolean"
      },
      {
        "description": "Called after the field is cleared via the inline ✕ (requires `allowClear`).",
        "name": "onClear",
        "type": "() => void"
      },
      {
        "defaultValue": "false",
        "description": "The floor is `minRows` (or `rows`, when that is the only one given) and never undercuts the `--control-height` tier, so a resting one-row composer still lines up with the Button beside it; past `maxRows` the control stops growing and scrolls internally. Sizing happens in CSS from a hidden replica of the text, so it follows a paste, an IME composition, a programmatic value change, a font swap and a container resize — not just typing — and the component never writes `style.height`, never reads `scrollHeight` and never moves `scrollTop`. Works controlled and uncontrolled. Off by default: an existing Textarea keeps the exact geometry it has today.",
        "name": "autoGrow",
        "type": "boolean"
      },
      {
        "defaultValue": "1 (--textarea-autogrow-min-height-rows)",
        "description": "Floor in text rows while `autoGrow`. Theme-global default is the `--textarea-autogrow-min-height-rows` token; this prop overrides it per instance. Ignored when `autoGrow` is false.",
        "name": "minRows",
        "type": "number"
      },
      {
        "defaultValue": "8 (--textarea-autogrow-max-height-rows)",
        "description": "Ceiling in text rows while `autoGrow`; beyond it the box stops growing and scrolls internally rather than pushing the page. Pass `0` for no ceiling — only correct inside an owning scroll container. Theme-global default is the `--textarea-autogrow-max-height-rows` token. Ignored when `autoGrow` is false.",
        "name": "maxRows",
        "type": "number"
      },
      {
        "description": "Immediate value callback; native onChange remains supported. Shared form bindings are emitted once.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      }
    ],
    "related": [
      "Input — use Input for single-line values (names, amounts, codes). Use Textarea only when the expected value spans multiple lines or could be longer than ~80 characters.",
      "FormField — always the parent wrapper for Textarea in forms; provides label, helper text, error message, and injects all required aria attributes onto the Textarea child automatically.",
      "Select — when the user must pick from a finite set of multi-line-looking options (e.g. template choices) use Select, not a Textarea presenting options as free text.",
      "Select with showSearch or SearchSelect — if the multi-line field is actually a tag/token input or a constrained lookup, prefer Select (showSearch) or SearchSelect over a Textarea that the user types into freely."
    ],
    "rules": [],
    "storyPath": "data-entry/textarea.tsx",
    "tagline": "Styled wrapper around native <textarea>. Pair with FormField for labelled fields. For a Markdown body (wiki page, issue description, mail) use the sibling package `@godxjp/editor` `MarkdownEditor` — this Textarea plus a formatting toolbar, preview through `@godxjp/markdown`, and paste/drop upload through a host `upload` function (gh#1109).",
    "usage": [
      "DO reach for `variant=\"ghost\"` ONLY when a parent surface already draws the box and owns focus — the composer Card, an inline edit cell. A standalone field keeps the default: without its own border it reads as plain text, not something you can type into.",
      "DO always wrap Textarea in FormField when it appears in a form — FormField clones aria-describedby, aria-required, and aria-invalid onto the child, giving error/helper announcements and screen-reader labelling for free. Pass matching id props to both.",
      "DO use the godx-ui Textarea (`import { Textarea } from '@godxjp/ui/data-entry'`) — never a raw `<textarea>`. The component applies the `ui-control-multiline` token class that picks up density, focus-ring, and border tokens from the design system.",
      "DO control the value with `value` + `onChange` in React-managed forms (e.g. Inertia `useForm`). Textarea is a plain `forwardRef` over the native element so it accepts all standard `HTMLTextAreaElement` attributes — `rows`, `maxLength`, `disabled`, `name`, `placeholder`, `readOnly` all pass through directly.",
      "DO pass `name` when the textarea sits inside an HTML `<form>` for native form submission or when Inertia's `useForm` destructures field values by key — the `name` attribute maps the value into the form data bag.",
      "DON'T apply manual height or padding classes directly on Textarea to simulate a taller field — use `rows` for a fixed height, or `autoGrow` for a box that grows with its content.",
      "DON'T hand-roll auto-grow with an `onInput` handler that sets `style.height` from `scrollHeight` — that is a forced synchronous reflow on every keystroke, it re-derives the library's own box model (`--control-height`, `--control-padding-x`, `--control-border-width`), it freezes the height against the density axis and a tenant retheme, and it misses the paths that matter: a paste, an IME composition in ja/vi, a programmatic reset after submit, and a webfont swap. Pass `autoGrow` instead — the library owns the measurement in CSS.",
      "DO reach for `autoGrow` + `minRows`/`maxRows` for a chat or comment composer: `<Textarea autoGrow minRows={1} maxRows={8} />` starts one row tall, grows line by line as the author types or pastes, scrolls internally past the ceiling, and collapses back to `minRows` the moment a controlled value is reset to `\"\"` after send. Bound it in ROWS, never in px — a row count survives a density change and a `--font-size-base` retheme.",
      "DON'T hand-roll label + error markup next to a bare Textarea. Always use FormField: it injects aria-invalid (red ring on the control), renders a `role='alert'` error paragraph, and links them via aria-describedby automatically."
    ],
    "useCases": [
      "Free-text memo or note fields on an invoice or transaction detail form — e.g. '備考 / Notes' that can hold multi-line internal comments alongside structured Invoice fields.",
      "Rejection reason or approval comment in an admin workflow dialog — a short-to-medium text block a reviewer types before confirming an action in a Dialog or Sheet.",
      "Address or multi-line description input on a vendor / partner entity form where a single-line Input would be too restrictive.",
      "Email body composer or message template editor in a lightweight CRM or notification settings screen where rich text is not required.",
      "Audit log annotation — allowing an accountant to attach a plain-text explanation to a manual journal entry or adjustment record."
    ]
  },
  {
    "example": "import { FormField, TranslatableInput, TranslatableTextarea, type TranslatableValue } from \"@godxjp/ui/data-entry\";\n\nconst [productName, setProductName] = React.useState<TranslatableValue>({ ja: \"醤油ラーメン\" });\nconst [description, setDescription] = React.useState<TranslatableValue>({});\n\n<FormField label=\"商品名\" required>\n  <TranslatableInput\n    locales={[\"ja\", \"en\", \"vi\"]}\n    fallbackLocale=\"ja\"\n    value={productName}\n    onValueChange={setProductName}\n    errors={{ en: \"英語名を入力してください\" }}\n  />\n</FormField>\n\n<FormField label=\"説明\">\n  <TranslatableTextarea locales={[\"ja\", \"en\", \"vi\"]} value={description} onValueChange={setDescription} rows={3} />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "TranslatableInput",
    "props": [
      {
        "description": "Controlled per-locale values — `Partial<Record<string, string>>` keyed by BCP-47 tag. A missing key or `\"\"` is an empty locale.",
        "name": "value",
        "type": "TranslatableValue"
      },
      {
        "description": "Uncontrolled initial per-locale values.",
        "name": "defaultValue",
        "type": "TranslatableValue"
      },
      {
        "description": "The WHOLE map after an edit: only the active locale's key changes, every other locale is carried over untouched. The ✕ of `allowClear` empties the active locale only.",
        "name": "onValueChange",
        "type": "(value: TranslatableValue) => void"
      },
      {
        "description": "The BCP-47 tags the field edits, in tab order. Default: AppProvider `contentLocales` when set (gh#1347 — state the content languages once at the root instead of on every field), else every locale registered with the app (`getRegisteredLocales()` — built-in `vi`/`en`/`ja` plus `registerLocale`). Tabs are named by `Intl.DisplayNames` in the reader's UI language; the tags stay the keys.",
        "name": "locales",
        "type": "readonly string[]"
      },
      {
        "description": "The locale tab selected first. Default: the first of AppProvider `contentLocales` (the source language) when set and in `locales`, else the AppProvider's active locale when it is in `locales`, else the fallback locale.",
        "name": "defaultLocale",
        "type": "string"
      },
      {
        "description": "The locale whose text an EMPTY locale shows as its placeholder. Default: the AppProvider's `fallbackLocale`, or the first of `locales` when that is not one of them.",
        "name": "fallbackLocale",
        "type": "string"
      },
      {
        "description": "Per-locale validation messages. Each locale that has one gets an error mark on its tab — a glyph plus screen-reader text («Vietnamese, has an error»), never colour alone. The ACTIVE locale's message sets `aria-invalid` and is shown in the enclosing FormField's own error line (wired by `aria-errormessage`); without a FormField it is shown under the field.",
        "name": "errors",
        "type": "Partial<Record<string, ReactNode>>"
      },
      {
        "defaultValue": "\"md\"",
        "description": "The field's control tier, as on Input. The tab strip uses the Tabs `sm` tier (`md` for an `lg` field).",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Shown while the active locale is empty and there is no fallback text to show (the fallback locale itself, or an empty fallback).",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Disables the control; the locale tabs stay operable so every locale can still be read.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Lands on the control, so a FormField label click focuses it.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root (strip + field) class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "TranslatableTextarea — the same contract over a Textarea.",
      "Input — the control it composes; every Input prop applies.",
      "FormField — the label / helper / error wiring it reports into.",
      "Tabs — the switcher it composes (strip-only, `controls` → the field's panel).",
      "AppSettingPicker kind=\"locale\" — chooses the UI language, not per-locale content."
    ],
    "rules": [],
    "storyPath": "data-entry/translatable-input.tsx",
    "tagline": "ONE text field holding a value per locale (`{ ja, en, vi }`), switched by locale tabs above it (gh#1336). For user content stored per locale — a product name, a category label, a menu description (Astrotomic / Spatie `*_translations`). Locales, first tab and fallback come from AppProvider; an empty locale shows the fallback locale's text as its placeholder; a locale with an error is marked on its tab. Every other `Input` prop applies to the one control.",
    "usage": [
      "DO set the app's content languages ONCE on the root provider — `<AppProvider contentLocales={[\"ja\", \"en\", \"vi\"]}>` — when every translatable field edits the same list; each field then needs no `locales` and opens on the source language (the first). Pass `locales` on a field only where it differs.",
      "DO wrap it in FormField: the label names the control together with the active locale («商品名 English») and names the tab strip («商品名 Language»), `required`/`helper` reach the control, and the active locale's error renders in FormField's error line.",
      "DO pass the per-locale error map from your validator as `errors` (e.g. Laravel `name.ja` → `errors.ja`). Don't pass the same message to FormField `error` as well: FormField's own `error` wins over the reported one, and it would mark the field invalid on every locale.",
      "DO set `fallbackLocale` to the content's SOURCE language (often `ja`) when it differs from the AppProvider's UI fallback — the placeholder of an untranslated locale then shows the source text to translate from.",
      "DON'T build a local wrapper of Tabs + Input for this: the tab↔panel ARIA, the lang attribute, the error marks and the FormField wiring are what this component exists for.",
      "DON'T use it for a choice of language that IS the form's value (a user's preferred language): that is `AppSettingPicker kind=\"locale\"` or a Select.",
      "The locale switcher is the WAI-ARIA APG tabs pattern (tablist → one tabpanel holding the field), not a Segmented radio group: the locale is not form state — every locale is submitted together — the tab only chooses which locale the one field shows."
    ],
    "useCases": [
      "Product / menu item name in ja, en and vi on an admin edit form",
      "Category or section label stored per locale (Astrotomic `*_translations`)",
      "Translated description or recipe text (TranslatableTextarea)"
    ]
  },
  {
    "example": "import { FormField, TranslatableTextarea } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\">\n  <TranslatableTextarea locales={[\"ja\", \"en\", \"vi\"]} value={description} onValueChange={setDescription} autoGrow minRows={3} />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "TranslatableTextarea",
    "props": [
      {
        "description": "Controlled per-locale values, keyed by BCP-47 tag.",
        "name": "value",
        "type": "TranslatableValue"
      },
      {
        "description": "Uncontrolled initial per-locale values.",
        "name": "defaultValue",
        "type": "TranslatableValue"
      },
      {
        "description": "The whole map after an edit; only the active locale's key changes.",
        "name": "onValueChange",
        "type": "(value: TranslatableValue) => void"
      },
      {
        "description": "The BCP-47 tags the field edits, in tab order. Default: AppProvider `contentLocales` when set, else the app's registered locales.",
        "name": "locales",
        "type": "readonly string[]"
      },
      {
        "description": "The locale tab selected first. Default: the first of AppProvider `contentLocales` when set and in `locales`, else the AppProvider's active locale.",
        "name": "defaultLocale",
        "type": "string"
      },
      {
        "description": "The locale whose text an empty locale shows as its placeholder. Default: the AppProvider's `fallbackLocale`.",
        "name": "fallbackLocale",
        "type": "string"
      },
      {
        "description": "Per-locale messages: tab marks for each, `aria-invalid` + FormField's error line for the active one.",
        "name": "errors",
        "type": "Partial<Record<string, ReactNode>>"
      },
      {
        "defaultValue": "\"md\"",
        "description": "The field's control tier, as on Textarea.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Shown while the active locale is empty and there is no fallback text.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Disables the control; the locale tabs stay operable.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Lands on the control.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "TranslatableInput — the single-line form and the full contract.",
      "Textarea — the control it composes."
    ],
    "rules": [],
    "storyPath": "data-entry/translatable-input.tsx",
    "tagline": "TranslatableInput over a Textarea (gh#1336): one multi-line field holding a value per locale, switched by locale tabs. Same locale, fallback-placeholder, error-mark and FormField contract; every Textarea prop (rows, autoGrow, count…) applies to the one control.",
    "usage": [
      "DO use it for per-locale multi-line content (a description, a recipe); for a single line use TranslatableInput.",
      "DO wrap it in FormField, exactly as TranslatableInput."
    ],
    "useCases": [
      "Menu item description in ja, en and vi",
      "Recipe or notice body stored per locale"
    ]
  },
  {
    "example": "import { FormField, TranslatableField, Textarea } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\">\n  <TranslatableField\n    locales={[\"ja\", \"en\", \"vi\"]}\n    value={description}\n    onValueChange={setDescription}\n    renderControl={(control) => <Textarea {...control} rows={4} />}\n  />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "TranslatableField",
    "props": [
      {
        "description": "Renders the one control in the panel. Spread `control` onto it: `lang`, `value` / `onValueChange` (the active locale's text), the resolved `placeholder`, `status`, `size`, and the field-a11y props (`aria-labelledby` with the locale's name, `aria-invalid`, `aria-errormessage`) plus every prop the field does not own (`id`, `disabled`, …).",
        "name": "renderControl",
        "type": "(control: TranslatableFieldControlProp) => ReactNode"
      },
      {
        "description": "Turns the fallback locale's stored text into the plain placeholder an empty locale shows (HTML → text for a rich-text control). Default: the text as stored.",
        "name": "toPlaceholder",
        "type": "(fallbackText: string) => string"
      },
      {
        "description": "The per-locale map; onValueChange gets the whole map, only the active key changed.",
        "name": "value / defaultValue / onValueChange",
        "type": "TranslatableValue"
      },
      {
        "description": "Exactly as on TranslatableInput.",
        "name": "locales / defaultLocale / fallbackLocale / errors",
        "type": "—"
      },
      {
        "description": "Passed to the control; the tab strip uses Tabs `sm` (`md` for `lg`).",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Shown when there is no fallback text.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Passed to the control; the active locale's error turns it into `error`.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "Passed to the control, so a FormField label click focuses it.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root (strip + panel) class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "TranslatableInput — the engine around an Input.",
      "TranslatableTextarea — the engine around a Textarea.",
      "TranslatableRichText (@godxjp/block-editor) — the engine around a RichTextEditor."
    ],
    "rules": [],
    "storyPath": "data-entry/translatable-input.tsx",
    "tagline": "The locale-tab engine behind TranslatableInput, TranslatableTextarea and `@godxjp/block-editor`'s TranslatableRichText (gh#1365): the locale list and first tab (AppProvider `contentLocales`), the per-locale map, the fallback placeholder, the error marks and the FormField error report — around ONE control you render with `renderControl`. Use it only to make another control translatable; for text use TranslatableInput / TranslatableTextarea.",
    "usage": [
      "DO render ONE control and spread `control` onto it (`<X {...control} ref={ref} />`): the engine swaps its value per tab, so the ref, FormField id and IME composition stay on one node.",
      "DO make the control accept `lang`, `status`, `placeholder` and the field-a11y props on its focus target — that is how the locale, the error and the label reach it.",
      "DON'T use it for a plain text field: TranslatableInput / TranslatableTextarea are this engine with an Input / Textarea already."
    ],
    "useCases": [
      "Making a rich-text, date or custom control hold one value per locale with the kit's tab / error / fallback contract"
    ]
  },
  {
    "example": "import { Label } from \"@godxjp/ui/data-entry\";\n\n<Label htmlFor=\"stackable\">併用を許可</Label>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Label",
    "props": [
      {
        "description": "Id of the associated control.",
        "name": "htmlFor",
        "type": "string"
      },
      {
        "description": "Label content.",
        "name": "children",
        "type": "ReactNode"
      }
    ],
    "related": [
      "FormField — prefer this over a bare Label whenever the field needs helper text, an error message, or a required marker; FormField renders Label internally and wires aria-describedby/aria-invalid automatically.",
      "Field — use for a Checkbox or Radio.Item that needs a visible label and optional description line; it renders Label internally — do NOT add a second Label around it.",
      "Field — use instead of a bare Switch + Label pair when the control must submit a value via an HTML form name; Field owns the Label + hidden input composition.",
      "Checkbox — the most common bare-Label partner; pair with Label via shared useId() id/htmlFor when Field's layout is too heavy."
    ],
    "rules": [],
    "storyPath": "data-entry/label.tsx",
    "tagline": "Styled React Aria Label; use htmlFor to associate with a control.",
    "usage": [
      "DO: always pass `htmlFor` matching the `id` of the associated control — this is the entire purpose of the component. Without it, clicking the label text does NOT focus or toggle the control, breaking a11y and UX.",
      "DO: import from `@godxjp/ui/data-entry` (not shadcn or a primitive library directly). The godx-ui Label extends React Aria's Label with `data-slot=\"label\"`, `select-none`, and `group-data-[disabled]` opacity-50 — hand-rolling a `<label>` loses all of these.",
      "DON'T: use Label as a standalone visible heading or section title. It is a form-control association primitive. For page/section headings use semantic HTML (`<h2>`, etc.) or a typography class instead.",
      "DON'T: wrap Label around a control that is already labelled internally. FormField, Field, and CheckboxGroup all render Label internally — adding a second Label creates a duplicate association and redundant screen-reader announcement.",
      "DO: pair Label with Checkbox or Switch when NOT using the compound wrapper (Field). In that case generate the shared id with `React.useId()` and pass it to both `id` on the control and `htmlFor` on Label.",
      "PREFER FormField over a bare Label + control pair whenever you also need helper text, error messages, or `required` asterisk. FormField injects `aria-describedby` and `aria-invalid` automatically; a bare Label does not."
    ],
    "useCases": [
      "Pairing with a standalone Checkbox when Field's two-line layout is unnecessary — e.g. a single 'Remember me' option in a login form.",
      "Labelling a bare Switch (not Field) in a settings row where the switch is controlled by parent state and no HTML form name attribute is needed.",
      "Adding a visible label to a custom or third-party control that accepts an `id` prop but isn't wrapped by FormField or Field.",
      "Labelling a Textarea in a free-text form field when FormField's helper/error slots aren't needed, keeping the markup minimal.",
      "Rendering an accessible label inside a table row where a FormField's block layout would break the inline/grid structure.",
      "Adding a label to a DatePicker, TimePicker, or ColorPicker inside a simple layout that doesn't need the full FormField wrapper."
    ]
  },
  {
    "example": "import { Checkbox } from \"@godxjp/ui/data-entry\";\n\n// children is the inline label (antd): box first, label on the same line, the text toggles the box.\n// Never a hand-rolled <div className=\"flex items-center gap-2\"> around a bare <Label>.\n<Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)}>\n  利用規約に同意する\n</Checkbox>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Checkbox",
    "props": [
      {
        "description": "antd `indeterminate` — paint the PARTIAL mark (a dash) and announce `mixed`, without changing `checked`. This component also accepts the same state as `checked=\"indeterminate\"`; the flag is antd's spelling of it, and the box falls back to the underlying `checked` the moment the flag goes false.",
        "name": "indeterminate",
        "type": "boolean"
      },
      {
        "description": "Controlled checked state.",
        "name": "checked",
        "type": "boolean | 'indeterminate'"
      },
      {
        "description": "Uncontrolled initial state. It takes the tri-state too, and the box keeps the dash until the first click rather than falling back to unchecked.",
        "name": "defaultChecked",
        "type": "boolean | 'indeterminate'"
      },
      {
        "description": "Fires when checked state changes.",
        "name": "onCheckedChange",
        "type": "(checked) => void"
      },
      {
        "description": "Links to a <Label htmlFor>.",
        "name": "id",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Keeps its HTML spelling here and becomes react-aria's `isRequired`, so unlike Switch — where the same prop only ANNOUNCES the requirement — this is real constraint validation on the underlying input: an unchecked box blocks native form submission. The consent checkbox is the case it exists for. It does not render an asterisk; the required MARK belongs to FormField's label.",
        "name": "required",
        "type": "boolean"
      },
      {
        "description": "antd `<Checkbox>label</Checkbox>` — the INLINE label: box first, label at inline-end on the same line, the same markup as a `Checkbox.Group` option. A real `<label for>`: clicking the text toggles the box and the text is its accessible name. `className` then styles the labelled row.",
        "name": "children",
        "type": "React.ReactNode"
      }
    ],
    "related": [
      "CheckboxGroup — use instead of bare Checkbox when you have a list of 2+ options from an array; it handles id generation, Field wrapping, value array management, and the `name` prop for form submission. Checkbox is for a single boolean; CheckboxGroup is for multi-select.",
      "Switch / Field — use Switch when the action takes immediate effect (enable/disable a feature in settings) rather than selecting an option to be submitted later. Checkbox implies 'will be submitted as part of a form'; Switch implies 'applies now'. Field adds a hidden input for HTML form compatibility.",
      "RadioGroup — use when only one option in a group may be selected at a time (mutually exclusive). CheckboxGroup = multiple selections allowed; RadioGroup = single selection only.",
      "Field — the internal layout primitive (control slot + Label + description) that Checkbox.Group renders per item. Use it directly only when you need a one-off labelled checkbox or radio item outside of a group, and you want the consistent indent/description layout without the group's value-management overhead."
    ],
    "rules": [],
    "storyPath": "data-entry/checkbox.tsx",
    "subParts": [
      "CheckboxVisual"
    ],
    "tagline": "Checkbox on react-aria-components; standalone or via CheckboxGroup with an options array. `role=\"checkbox\"` is the real `<input>`; the painted box is the `<label>` around it and carries `data-state`.",
    "usage": [
      "DO give a single boolean its label as children (antd `<Checkbox>label</Checkbox>`, gh#709): `<Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)}>利用規約に同意する</Checkbox>` — box first, label on the same line, the text toggles the box and is its accessible name. In a form: `<FormFieldControl name=\"agree\" valuePropName=\"checked\">{(field) => <Checkbox {...field}>利用規約に同意する</Checkbox>}</FormFieldControl>`.",
      "DON'T render a boolean field's label ABOVE its checkbox — `<FormFieldControl label=\"…\">{(field) => <Checkbox checked={field.value} … />}</FormFieldControl>` puts the label on one row and the box far below it. Put the label in the Checkbox's children and use `valuePropName=\"checked\"` on the field.",
      "DO pair every standalone Checkbox WITHOUT children with a `<Label htmlFor={id}>` (or an `aria-label`) — the id prop on Checkbox must match the htmlFor on Label so screen readers announce the label on focus. Without this pairing the control is inaccessible.",
      "DO use the controlled pattern (`checked` + `onCheckedChange`) for any form-bound checkbox. `onCheckedChange` receives `boolean | 'indeterminate'` — always coerce with `!!v` or an explicit guard before storing in state.",
      "DO use `Checkbox.Group` (alias for CheckboxGroup) with the `options` prop when you have ≥2 choices from an array — it renders each item inside a `Field` (label + optional description), generates stable ids automatically, and manages the `string[]` value array. NEVER hand-roll a loop of bare `<Checkbox>` elements for a multi-select list.",
      "DO pass `name` on `Checkbox.Group` (not on individual checkboxes) when the group must submit as form fields — the group propagates the name to each internal checkbox so the browser serialises all checked values under that key.",
      "DON'T use `checked='indeterminate'` on `Checkbox.Group` children, and DON'T hand-wire a select-all box above a group — pass `selectAll` (or `selectAll={{ label: '…' }}`) on `Checkbox.Group` with `options`. It renders a leading box derived from the value (checked / `mixed` / clear), a muted `{n} of {m} selected` count (`common.selectedOfTotal`, numbers via Intl.NumberFormat) and a Separator; one click is ONE `onValueChange` with every enabled value; disabled options are never counted, added or removed; the box has no `name`, so a native submit never carries it.",
      "DON'T wrap a standalone Checkbox in `Field` manually — `Field` is the internal composition primitive that `Checkbox.Group` uses. For a single boolean with a label, pass the label as children (`<Checkbox …>label</Checkbox>`, the catalog example) — it renders the same row. Use `<Field id='x' label='…' description='…'><Checkbox id='x' … /></Field>` only when the row needs a description line — Field owns the label-to-control id wiring, the description slot and the row rhythm, and a hand-rolled flex row owns none of them; for a full labelled-checkbox with description, use `Field` directly only if you need a one-off item outside a group."
    ],
    "useCases": [
      "A 'Select all' / bulk-action row above a DataTable — standalone Checkbox with `checked='indeterminate'` when some (not all) rows are selected, toggling between all-selected and none-selected.",
      "A multi-step filter panel (e.g. filter invoices by payment status: Paid, Unpaid, Overdue) — `Checkbox.Group` with `options` prop and `orientation='vertical'`, controlled value wired to Toolbar state.",
      "Confirmation or consent acknowledgement before a destructive action in a Dialog — standalone Checkbox with controlled state used to enable/disable the confirm Button.",
      "Settings panel where each feature flag is a boolean toggle with a description line — `Checkbox.Group` with options carrying a `description` field so each row renders label + subtext via Field.",
      "Bulk-edit form row in an accounting ledger (e.g. 'Apply to all selected entries') — standalone Checkbox with name + value inside a `<form>` for native HTML form submission.",
      "Onboarding checklist (e.g. 'I have read the terms', 'I consent to data processing') with multiple distinct items whose values are independent — two separate standalone Checkboxes, each with their own id/state, not a Checkbox.Group (since each item maps to a different boolean field)."
    ]
  },
  {
    "example": "import { RadioGroup } from \"@godxjp/ui/data-entry\";\n\n<RadioGroup value={trigger} onValueChange={setTrigger} orientation=\"horizontal\" options={[\n  { label: \"初回購入\", value: \"first_purchase\" },\n  { label: \"誕生日\", value: \"birthday\" },\n]} />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "RadioGroup",
    "props": [
      {
        "defaultValue": "\"default\"",
        "description": "antd `optionType` — how each choice is DRAWN. `default` is a radio dot beside its label; `button` welds the choices into one segmented bar. The role stays `radiogroup`/`radio` either way: this is paint, never semantics, so a screen reader hears the same group in both.",
        "name": "optionType",
        "type": "\"default\" | \"button\""
      },
      {
        "defaultValue": "\"outline\"",
        "description": "antd `buttonStyle` — fill of the SELECTED choice while `optionType=\"button\"`. Inert on the default option type.",
        "name": "buttonStyle",
        "type": "\"outline\" | \"solid\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Button-bar size while `optionType=\"button\"`. `touch` (gh#1304) is the glove step: every button at least 56×56 CSS px (`--control-height-touch`) and the bar un-welded into separate buttons `--control-touch-gap` (8px) apart, for wet or gloved hands. Inert on radio dots.",
        "name": "size",
        "type": "\"md\" | \"touch\""
      },
      {
        "description": "Id của nhóm; `FormField` tự truyền xuống để nối nhãn ↔ control.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Controlled selected value.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Fires on selection change.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Declarative list: { label, value, disabled?, description? }.",
        "name": "options",
        "type": "ChoiceOptionProp[]"
      },
      {
        "defaultValue": "\"vertical\"",
        "description": "Layout direction.",
        "name": "orientation",
        "type": "\"horizontal\" | \"vertical\""
      },
      {
        "description": "Uncontrolled initial selected value.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Disable the whole group.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Form field name for native submission.",
        "name": "name",
        "type": "string"
      }
    ],
    "related": [
      "CheckboxGroup — use when the user may select zero or more values simultaneously (multi-select); RadioGroup enforces exactly one selection. Both share the same options array shape and orientation prop.",
      "Select — use when there are 5 or more options or the option list is dynamic/searchable; RadioGroup is preferred for 2-4 fixed visible choices where scanning all options at once matters.",
      "Field — use when there are exactly two states that map to on/off (boolean); RadioGroup is the right pick when the two-or-more options are semantically distinct named values, not a toggle.",
      "Field — the low-level label+description wrapper that RadioGroup uses internally for each item. Use it directly only when manually composing Radio.Item children inside Radio.Group; never hand-roll a label alongside a bare Radio.Item without it."
    ],
    "rules": [
      23
    ],
    "storyPath": "data-entry/radio-group.tsx",
    "subParts": [
      "RadioGroupRoot"
    ],
    "tagline": "Radio group accepting an options array or RadioItem children.",
    "usage": [
      "DO use the `options` prop for the data-driven path: pass `{ label, value, disabled?, description? }[]` and RadioGroup renders every option as a correctly labelled Field automatically — never hand-roll Radio.Item + Label pairs in a loop yourself.",
      "DO provide `name` whenever the group lives inside an HTML form: the group renders a hidden `<input name={name}>` carrying the selected string value, making the field natively form-submittable without a separate hidden input.",
      "DO use controlled mode (`value` + `onValueChange`) for any form managed by useForm or a state manager. Use `defaultValue` only for truly uncontrolled UI where you never need to read the value in code.",
      "DO NOT reach for children / manual composition unless the options list is dynamic-JSX (e.g. each item needs a custom rendered label with an icon). When you do compose children manually, wrap each Radio.Item in a Field — rendering a bare Radio.Item without Field skips the label and breaks a11y.",
      "DO NOT use RadioGroup when the user may select zero or multiple items — that is CheckboxGroup. RadioGroup enforces exactly one selection at all times (or none before first interaction when uncontrolled).",
      "A11y: the root emits `role=radiogroup`; each item is a real `<input type=\"radio\">` (implicit `role=radio`) and is keyboard-navigable with arrow keys. Never suppress `name` on the Root when inside a form — without it the input is unnamed and won't submit. Name an item with `<Label htmlFor>` (or `Field`) BESIDE it, never by wrapping it: the control already sits inside its own `<label>`, and a second one nested outside leaves it nameless."
    ],
    "useCases": [
      "Selecting a single billing cycle (monthly / quarterly / annual) in an invoice or subscription settings form where all 2-4 options must be visible at once.",
      "Choosing a report output format (PDF / CSV / Excel) before triggering an async export job — keeps all options scannable without opening a dropdown.",
      "Picking a transaction type (income / expense / transfer) on an accounting entry form where the choice changes which subsequent fields are shown.",
      "Selecting a sync trigger mode (first purchase / birthday / manual) in a campaign or automation settings panel — matches the catalog example exactly.",
      "Filtering a compact inline control (horizontal orientation) such as date granularity (day / week / month) inside a dashboard filter bar where a full Select dropdown would be over-engineered.",
      "Choosing an approval status (pending / approved / rejected) on an admin detail sheet where all states must be visible so reviewers can compare them without interaction."
    ]
  },
  {
    "absorbed": [
      "DateRangePicker"
    ],
    "example": "import { useState } from \"react\";\nimport { DatePicker, FormField } from \"@godxjp/ui/data-entry\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport type { DateRange } from \"react-day-picker\";\n\n// One component, three shapes: a day, a month, and a range.\nexport function BillingFields() {\n  const [dueDate, setDueDate] = useState<Date | undefined>(undefined);\n  const [month, setMonth] = useState<Date | undefined>(undefined);\n  const [period, setPeriod] = useState<DateRange | undefined>(undefined);\n\n  return (\n    <Flex direction=\"col\" gap=\"md\">\n      <FormField id=\"due-date\" label=\"支払期日\" required>\n        <DatePicker\n          id=\"due-date\"\n          name=\"due_date\"\n          value={dueDate}\n          onValueChange={setDueDate}\n          minDate={new Date()}\n        />\n      </FormField>\n\n      <FormField id=\"billing-month\" label=\"請求年月\">\n        <DatePicker\n          id=\"billing-month\"\n          name=\"billing_month\"\n          picker=\"month\"\n          value={month}\n          onValueChange={setMonth}\n        />\n      </FormField>\n\n      <FormField id=\"period\" label=\"会計期間\">\n        <DatePicker id=\"period\" name=\"period\" range value={period} onValueChange={setPeriod} />\n      </FormField>\n    </Flex>\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "DatePicker",
    "props": [
      {
        "description": "Controlled panel visibility.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Initial panel visibility.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Panel visibility changes.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Validation appearance; error announces invalid state.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "Shared control surface.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\" | \"underlined\""
      },
      {
        "description": "Shared control sizing.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "antd `inputReadOnly` — sets the readonly attribute on the input so the mobile virtual keyboard stays down. The PANEL STILL OPENS: this is 'pick from the panel, do not type', not a second `disabled`. Use `disabled` to make the control inert.",
        "name": "inputReadOnly",
        "type": "boolean"
      },
      {
        "description": "Keep invalid draft text on blur; never submit it as a committed value.",
        "name": "preserveInvalidOnBlur",
        "type": "boolean"
      },
      {
        "description": "Logical popup placement.",
        "name": "placement",
        "type": "\"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\""
      },
      {
        "description": "Additional panel footer content.",
        "name": "renderExtraFooter",
        "type": "() => ReactNode"
      },
      {
        "description": "Ref to the editable input (with `range`, the start edge).",
        "name": "ref",
        "type": "Ref<HTMLInputElement>"
      },
      {
        "description": "Display using date-fns patterns, Intl options (Japanese era supported), or a callback. Submission stays ISO.",
        "name": "format",
        "type": "string | Intl.DateTimeFormatOptions | ((date: Date) => string)"
      },
      {
        "description": "Parser for custom or era display. Complete ISO input always works.",
        "name": "parseFormat",
        "type": "(text: string) => Date | undefined"
      },
      {
        "description": "Earliest selectable date, inclusive, enforced on BOTH routes into the value — the panel greys the cell out and a typed date is rejected. The ONLY lower bound: the removed MonthPicker's `fromYear` is `minDate={new Date(year, 0, 1)}`. Alias of fromDate.",
        "name": "minDate",
        "type": "Date"
      },
      {
        "description": "Latest selectable date, inclusive, enforced on both routes. `toYear={y}` becomes `maxDate={new Date(y, 11, 31)}`. Alias of toDate.",
        "name": "maxDate",
        "type": "Date"
      },
      {
        "description": "Show week numbers.",
        "name": "showWeek",
        "type": "boolean"
      },
      {
        "description": "How the day panel's month caption is drawn (react-day-picker `captionLayout`, forwarded to Calendar). Default `label`. `dropdown` turns month and year into selects; without minDate/maxDate the year list spans 100 years back to the current year.",
        "name": "captionLayout",
        "type": "\"label\" | \"dropdown\" | \"dropdown-months\" | \"dropdown-years\""
      },
      {
        "description": "Which period the PANEL opens on, independently of the value (antd `defaultPickerValue`), and — as in antd — RE-APPLIED every time the panel opens, not only at mount. Use it for 'open on the fiscal year's start month' or 'open on the month of the row being edited'.",
        "name": "defaultPickerValue",
        "type": "Date"
      },
      {
        "description": "Controlled panel period (antd `pickerValue`). Wins over `defaultPickerValue` and over the value, and freezes the panel's own navigation — the parent owns which period is shown.",
        "name": "pickerValue",
        "type": "Date"
      },
      {
        "description": "Stage choices until confirmed; defaults on with showTime.",
        "name": "needConfirm",
        "type": "boolean"
      },
      {
        "description": "Include time editing; object takes TimePicker steps/hour-cycle/disabledTime. Not available with `range` or `multiple`.",
        "name": "showTime",
        "type": "boolean | object"
      },
      {
        "description": "Quick choices resolved on click and checked against the constraints. `T` follows the cardinality: `Date` normally, `DateRange` with `range`.",
        "name": "presets",
        "type": "{ label: ReactNode; value: T | (() => T) }[]"
      },
      {
        "description": "GRANULARITY of one selection (antd `picker`). `date`/`week` show the day grid; `month`/`quarter`/`year` show a period grid. The value is always the START of the chosen period, and `name` submits the matching reduced ISO form.",
        "name": "picker",
        "type": "\"date\" | \"week\" | \"month\" | \"quarter\" | \"year\""
      },
      {
        "description": "CARDINALITY: two endpoints in one control (antd `DatePicker.RangePicker`). value/defaultValue and the callback become `DateRange`; the pair submits as `${name}_from` / `${name}_to`. Composes with `picker`, so a month range is `<DatePicker range picker=\"month\" />`. Never place two DatePickers side by side to fake this.",
        "name": "range",
        "type": "boolean"
      },
      {
        "description": "Select several dates; value/defaultValue and the callback use Date[]. Incompatible with showTime and with range.",
        "name": "multiple",
        "type": "boolean"
      },
      {
        "description": "With `range`: which of the two endpoints may stay empty (antd `allowEmpty`). Default `[true, true]`.",
        "name": "allowEmpty",
        "type": "[boolean, boolean]"
      },
      {
        "defaultValue": "true",
        "description": "With `range`: a polite live summary (`aria-live=\"polite\"`, always mounted) under the DAY grid — the start date alone while only the start is held, then `Intl.DateTimeFormat#formatRange` of both edges plus the INCLUSIVE day count through `Intl.PluralRules` (1 → 7 reads \"Mar 1 – 7, 2026 · 7 days\"; ja 「7日間」). Muted `Text size=\"xs\" tabular`, no new token. The month / quarter / year period grids ignore it. Independently of this flag, while only the start is chosen the `to` edge's placeholder becomes \"Select end date\" (`dataEntry.dateRangePicker.selectEnd`). `false` removes the summary.",
        "name": "showSummary",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Normalise the selection into ascending order (antd `order`). ONE rule seen through two value shapes: a `range` picked backwards is SWAPPED, a `multiple` selection is SORTED. Pass false to keep pick order.",
        "name": "order",
        "type": "boolean"
      },
      {
        "description": "Decorate a day cell — 祝日, a booked day, a deadline. It WRAPS the library's own day button rather than replacing it: `originNode` already carries the selection state, `aria-selected`, the disabled handling and its place in the grid's roving tabindex. Write `<>{originNode}<Badge …/></>` — decorate, never rebuild, or every marker re-derives all of that and most get it wrong.",
        "name": "cellRender",
        "type": "(date: Date, info: { originNode: ReactNode }) => ReactNode"
      },
      {
        "defaultValue": "true",
        "description": "Rule the popup's DAY grid — forwarded to `Calendar bordered`, on by default like it. Applies to `picker=\"date\"` / `\"week\"` in single, `multiple` and `range`; the month / quarter / year period grid has no day cells and ignores it. `bordered={false}` opts out. Line colour: `--calendar-grid-border-color` (default `hsl(var(--border))`, the same tier as the popover edge and a table row rule — lightened from `hsl(var(--input) / 0.5)` in gh#730). The caption→weekday-row step is ONE token now, `--calendar-grid-space-block-start` (space-3, measured 32px → 12px): it used to stack with the month column gap.",
        "name": "bordered",
        "type": "boolean"
      },
      {
        "description": "Forbid individual dates by predicate — a business rule the `minDate`/`maxDate` window cannot express (holidays, blackout days, a 開始 date already chosen). A forbidden day is refused on BOTH routes into the value: it cannot be clicked and it is rejected when typed.",
        "name": "disabledDate",
        "type": "(date: Date) => boolean"
      },
      {
        "defaultValue": "false",
        "description": "Footer action that jumps to the current month and selects today (single), fills the open end of the range (range) or adds today (multiple). Disabled when today is outside minDate / maxDate or matches `disabledDate`.",
        "name": "showToday",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Footer action that calls `onClose`.",
        "name": "showClose",
        "type": "boolean"
      },
      {
        "description": "Invoked by the showClose footer action.",
        "name": "onClose",
        "type": "() => void"
      },
      {
        "description": "Controlled selection. `Date` by default, `Date[]` with `multiple`, `DateRange` with `range`. The input text and the panel stay in sync with it.",
        "name": "value",
        "type": "Date | Date[] | DateRange | undefined"
      },
      {
        "description": "Uncontrolled initial selection, same shape as `value`.",
        "name": "defaultValue",
        "type": "Date | Date[] | DateRange | undefined"
      },
      {
        "description": "Fires when the user commits — a panel pick, a complete typed entry, Enter, or a clear (undefined). The argument type follows the cardinality, so no narrowing is needed at the call site.",
        "name": "onValueChange",
        "type": "(value: Date | Date[] | DateRange | undefined) => void"
      },
      {
        "description": "Form field name. Submits ISO-8601 at the precision `picker` selects: `2026-03-01` for date/week, `2026-03` for month and quarter, `2026` for year. With `range` the pair submits as `${name}_from` / `${name}_to`.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "HTML `id` for the input (the group, with `range`). Auto-generated when omitted, so the field always has one.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Placeholder shown while empty. Defaults to the i18n string for the cardinality and granularity in play.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Makes the control inert: the input, the clear button and the panel trigger all go dead, and a controlled `open` cannot force the panel up. With `range`, the TUPLE form `[from, to]` locks ONE endpoint and leaves the other editable (antd's RangePicker `disabled`) — the locked edge keeps its committed value whatever is typed or picked, and the ✕ is withdrawn because clearing would wipe it.",
        "name": "disabled",
        "type": "boolean | [boolean, boolean]"
      },
      {
        "description": "Extra CSS classes on the outermost wrapper. Use for width/margin overrides.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Locale object (from `date-fns/locale`) forwarded to the panel. Controls month/day names and the week start. The input always accepts the ISO form regardless of locale.",
        "name": "locale",
        "type": "DayPickerProps[\"locale\"]"
      },
      {
        "description": "Alias of minDate, kept for the antd v4 spelling.",
        "name": "fromDate",
        "type": "Date"
      },
      {
        "description": "Alias of maxDate, kept for the antd v4 spelling.",
        "name": "toDate",
        "type": "Date"
      },
      {
        "defaultValue": "true",
        "description": "Inline ✕ that resets the value when one is set (antd `allowClear`). The object form replaces the icon and/or the accessible label. Pass `false` on a required field.",
        "name": "allowClear",
        "type": "boolean | { clearIcon?: ReactNode; label?: string }"
      },
      {
        "description": "Accessible NAME of the calendar button. The default (「カレンダーを開く」 / 'Open calendar') is right for one picker on a page and useless for three: a screen-reader user hears the same sentence three times with nothing to say which field each button belongs to (WCAG 2.2 SC 2.4.6). Name it after the FIELD — `開始日のカレンダーを開く`. Same axis and same reason as Select's `clearLabel`.",
        "name": "triggerLabel",
        "type": "string"
      }
    ],
    "related": [
      "TimePicker — companion for HH:mm time selection; same form-submittable-input pattern with a `name` prop.",
      "Calendar — the bare calendar grid used inside DatePicker; reach for it only when you need an always-visible month grid with no input.",
      "MonthPicker / MonthRangePicker / DateRangePicker were separate components until 22.0.0. They are now `picker=\"month\"`, `range picker=\"month\"` and `range` on this component."
    ],
    "rules": [
      3,
      6,
      13,
      31
    ],
    "storyPath": "data-entry/date-picker.tsx",
    "tagline": "ONE date control: `picker` sets the granularity (day · week · month · quarter · year), `range` makes it a two-endpoint field, `multiple` a set. A real typeable input holds the value and submits ISO-8601 at the picker's own precision; the panel is the visual-only affordance.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "ONE component covers every date-shaped field. `picker` is the granularity axis, `range` and `multiple` are the cardinality axis, and they compose: `<DatePicker range picker=\"month\" />` is a month range.",
      "DO use `name` to make the field form-submittable — it emits ISO-8601 at the picker's own precision (`2026-03` for a month, `2026` for a year, `2026-03-01` for a day). No hidden input of your own is needed.",
      "DO wrap it in FormField like every other labelled control; FormField injects the id/aria wiring onto the input.",
      "DO pass `triggerLabel` whenever a screen has MORE THAN ONE date field. Without it every calendar button on the page carries the identical name and a screen-reader user cannot tell which field each one opens.",
      "DO test by filling the input directly: `await user.type(screen.getByRole('combobox'), '2026-04-15')`. With `range`, the two edges are named textboxes (From / To). The panel is secondary and not required for testing.",
      "DO use `minDate` / `maxDate` to restrict what is selectable — they are enforced on the panel AND on typed entry, so the keyboard is not a way around the rule the mouse obeys.",
      "DON'T place two DatePickers side by side to fake a from~to pair — that is `range`, which is one control, one shell and one value.",
      "DO use captionLayout=\"dropdown\" for a birth date or any far-away date — the year select reaches 1999 in one pick instead of ~320 arrow clicks; also set `maxDate` when the date can lie in the future (the year list stops at the current year).",
      "DO use `defaultPickerValue` when the panel should open somewhere other than the value — it is the only way to say it, and it re-applies on every open.",
      "DON'T reach for `inputReadOnly` to switch the control off; it only sets the input's readonly attribute (the panel still opens). `disabled` is the inert one.",
      "DON'T hand-roll a date text input + calendar popover — this component IS that pattern at WAI-ARIA combobox spec level."
    ],
    "useCases": [
      "Invoice due-date field in an accounting form — attach `name='due_date'` and submit natively.",
      "A 対象月 / 締め年月 field where the day is meaningless — `picker=\"month\"`, which submits `2026-03`.",
      "A 会計期間 from~to pair — `range`, submitting `period_from` / `period_to`.",
      "A quarterly or fiscal-year reporting filter — `picker=\"quarter\"` or `picker=\"year\"`.",
      "Restricting a closing date to future days with `minDate={new Date()}`.",
      "Locale-aware picker in a multi-language admin panel — pass a `date-fns` locale for the panel while the input keeps its ISO form."
    ]
  },
  {
    "example": "import { useState } from \"react\";\nimport { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\n\nfunction CreateDialog() {\n  const [open, setOpen] = useState(false);\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild><Button size=\"sm\">新規作成</Button></DialogTrigger>\n      <DialogContent className=\"max-w-lg\">\n        <DialogHeader>\n          <DialogTitle>新規クーポン作成</DialogTitle>\n          <DialogDescription>クーポン情報を入力してください。</DialogDescription>\n        </DialogHeader>\n        {/* The middle MUST be a DialogBody — that is the only scrolling box. Without it, content\n            taller than the viewport is clipped at both ends and the footer goes with it. */}\n        <DialogBody>{/* fields */}</DialogBody>\n        <DialogFooter>\n          <Button variant=\"outline\" onClick={() => setOpen(false)}>キャンセル</Button>\n          <Button onClick={() => setOpen(false)}>保存</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "Dialog",
    "props": [
      {
        "description": "Controlled open state.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Initial open state when uncontrolled.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Open-state change handler.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "defaultValue": "\"default\"",
        "description": "How dangerous this dialog is. ONE prop, THREE results, because they always travel together: `destructive` renders `role=\"alertdialog\"` instead of `role=\"dialog\"`, stops an outside click from dismissing, and gives `DialogAction` the destructive emphasis (antd `okType=\"danger\"`). It also defaults the corner ✕ off, because a ✕ is an accidental-dismiss affordance too. Escape still closes either way. Settable on the root (covers the tree) or on `DialogContent` (the nearer one wins). This is what replaces reaching for the separate `AlertDialog*` family — gh#567.",
        "name": "variant",
        "type": "\"default\" | \"destructive\""
      },
      {
        "defaultValue": "true",
        "description": "`false` renders a NON-MODAL dialog (WAI-ARIA APG allows non-modal dialogs): the page behind stays interactive and in the accessibility tree (no inert/aria-hidden), no scroll lock, no scrim, an outside press does NOT close it, and there is no `aria-modal`. The dialog keeps role=dialog + its title as name, centred fixed placement, sizes and tokens; focus moves into it on open, Tab can leave it, Escape closes it while focus is inside, and focus returns to the trigger on close (only if focus was still inside). Ignored, with a dev warning, under `variant=\"destructive\"` — an alertdialog is always modal. gh#696.",
        "name": "modal",
        "type": "boolean"
      }
    ],
    "related": [
      "AlertDialog — the flat confirm PRESET (title/description/challenge/step-up/pending, no markup of your own). Reach for the preset when it covers the case. When it does not, do NOT reach for the `AlertDialog*` compound parts: use `Dialog` with `variant=\"destructive\"`, which is the same role and the same scrim with a freeform body.",
      "Sheet — use Sheet instead of Dialog when the content is a slide-in panel (filters, detail sidebar, settings drawer). Sheet uses `side` prop and is better suited for wide filter forms or contextual detail panels that don't demand full focus interruption.",
      "Alert — use Alert for inline, non-modal status messages (validation errors, success banners on the page). Dialog is modal and focus-trapping; Alert is inline and never blocks interaction.",
      "Popover — use Popover for lightweight non-modal overlays anchored to a trigger (quick-edit a single field, tooltip-style confirmation for low-stakes actions). Dialog is full-modal; Popover stays near its trigger and doesn't dim the page.",
      "AlertMutationFeedback — use AlertMutationFeedback for toast/inline feedback after the Dialog closes, not inside it. Putting a success toast inside a Dialog that is about to unmount causes it to disappear immediately; emit the feedback after `onOpenChange(false)` resolves."
    ],
    "rules": [
      23,
      3
    ],
    "storyPath": "feedback/dialog.tsx",
    "subParts": [
      "DialogAction",
      "DialogBody",
      "DialogCancel",
      "DialogClose",
      "DialogContent",
      "DialogDescription",
      "DialogFooter",
      "DialogHeader",
      "DialogOverlay",
      "DialogPortal",
      "DialogRoot",
      "DialogTitle",
      "DialogTrigger"
    ],
    "tagline": "Compound modal. Controlled via open + onOpenChange. Parts available flat (DialogTrigger/DialogContent/…) or as Dialog.Trigger/Dialog.Content. Rendered with role=dialog.",
    "usage": [
      "Use `Dialog` for form-style or wizard-style modal flows that need freeform content and a close action.",
      "DO reach for `variant=\"destructive\"` for a dangerous confirmation, INCLUDING one that needs a form inside it (a required reason, a typed challenge). It gives the alertdialog role and the non-dismissable scrim without giving up the freeform body. The separate `AlertDialog*` parts remain for existing code and still work, but they are the older way in.",
      "DO leave `variant` alone for everything that is not destructive. The prop is a danger level, not a colour knob: to tint only the header band use `DialogHeader tone`, which is a separate axis with seven values.",
      "DO always control open state via `open` + `onOpenChange`. Dialog has no uncontrolled shortcut — omitting `open` means the trigger alone drives state, which is fine for simple trigger-only cases, but any async submission flow must use controlled state so you can hold the dialog open while `pending=true` and close it only on success.",
      "DO include `DialogHeader` with `DialogTitle` (and optionally `DialogDescription`) inside every `DialogContent`. a dialog needs an accessible title for screen readers; omitting it triggers a console warning and breaks a11y.",
      "DO wrap tall/scrolling content in `DialogBody` (the ring-safe scroll slot, max-height ~60vh). It insets the content to match the dialog padding so a full-width control's focus ring never clips against the scroll container — mirror of SheetBody.",
      "DO set `modal={false}` when the user must keep working on the page behind an open dialog (edit a list while a payment or detail dialog stays open). Control `open` yourself: an outside press no longer closes it, so give it a visible close action. Escape closes it only while focus is inside the dialog.",
      "DO pass `width` on `DialogContent` for a wide dialog (an overview grid, a comparison): `<DialogContent width=\"60rem\">` (number = px, string = any CSS length; antd Modal `width`, same vocabulary as SheetContent `width`). It is capped at the viewport minus --dialog-viewport-inset, so it never leaves a phone screen. Default 32rem (--dialog-width-default). Do NOT switch to a full-width Sheet to get room (gh#1174).",
      "Enter/exit timing is a token: --dialog-motion-duration (and --sheet-enter-duration / --sheet-exit-duration on Sheet), default var(--duration-fast) = 150ms. Under prefers-reduced-motion both surfaces do not animate at all (gh#1178)."
    ],
    "useCases": [
      "Inline form dialog — create or edit a record (invoice line, supplier, coupon) without navigating away. Place `FormField`/`Input`/`Select` inside `DialogContent`, wire the submit button to your mutation, and hold `open` while `pending` to prevent double-submit.",
      "Read-only detail popup — show a full transaction audit trail, attachment preview, or approval history in a modal without leaving the list page. Use `Dialog` with no `DialogFooter` action buttons, just a close trigger.",
      "Non-modal side task — `modal={false}` keeps a list or cart behind the dialog editable while the dialog stays open (take payment while the order lines can still change).",
      "Wizard / multi-step flow — step through entity setup (legal entity → fiscal year → opening balances) using a single Dialog whose `DialogContent` conditionally renders different step panels. Control which step is shown in local state."
    ]
  },
  {
    "example": "import { AlertDialog } from \"@godxjp/ui/feedback\";\n\n<AlertDialog\n  open={open}\n  onOpenChange={setOpen}\n  title=\"Delete project\"\n  description=\"This action cannot be undone.\"\n  confirmLabel=\"Delete\"\n  cancelLabel=\"Cancel\"\n  onConfirm={async () => {\n    await deleteProject();\n    setOpen(false);\n  }}\n  variant=\"destructive\"\n/>",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "AlertDialog",
    "props": [
      {
        "description": "Controlled open state.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Open-state change handler.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Accessible title/announcement for the alertdialog.",
        "name": "title",
        "required": true,
        "type": "string"
      },
      {
        "description": "Optional supporting explanatory text.",
        "name": "description",
        "type": "string"
      },
      {
        "description": "Primary action label (defaults to translated continue).",
        "name": "confirmLabel",
        "type": "string"
      },
      {
        "description": "Dismiss action label. Defaults to `feedback.alert.cancel` — en 'Cancel', ja 'キャンセル', vi 'Quay lại' (not 'Hủy': in Vietnamese that is also the verb of the destructive actions a confirm guards, gh#1284). A development warning fires when `confirmLabel` and the cancel label are the same word once tone marks are ignored.",
        "name": "cancelLabel",
        "type": "string"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Variant passed through to the confirm button.",
        "name": "variant",
        "type": "\"default\" | \"destructive\""
      },
      {
        "description": "Optional type-to-confirm phrase to prevent accidental confirm.",
        "name": "confirmPhrase",
        "type": "string"
      },
      {
        "description": "Semantic alias of `confirmPhrase` — the exact token to type (e.g. an org slug) before confirm arms.",
        "name": "challenge",
        "type": "string"
      },
      {
        "description": "Primary action handler.",
        "name": "onConfirm",
        "type": "() => Promise<void> | void"
      },
      {
        "description": "Optional step-up re-auth (passkey/2FA) gate; must resolve truthy before `onConfirm` fires. Returning false keeps the dialog open and announces failure.",
        "name": "stepUp",
        "type": "() => Promise<boolean> | boolean"
      },
      {
        "description": "Keep modal open after confirm when true.",
        "name": "keepOpenOnConfirm",
        "type": "boolean"
      },
      {
        "description": "Disable actions while async work is running.",
        "name": "pending",
        "type": "boolean"
      }
    ],
    "related": [
      "Dialog — use for form-style and non-destructive modal flows, no confirm preset behavior.",
      "AlertDialogRoot — the compound counterpart, and the OLDER way in. It still works and is not going away in this major, but a confirmation body the preset does not cover (a summary table, a diff, a required reason field) is now better written as `Dialog` with `variant=\"destructive\"`: same `role=\"alertdialog\"`, same non-dismissable scrim, freeform body, and `DialogBody` / `DialogClose` available — the two parts the `AlertDialog*` family never had (gh#567)."
    ],
    "rules": [
      23,
      3
    ],
    "storyPath": "feedback/alert-dialog.tsx",
    "subParts": [
      "AlertDialogRoot",
      "AlertDialogAction",
      "AlertDialogCancel",
      "AlertDialogContent",
      "AlertDialogDescription",
      "AlertDialogFooter",
      "AlertDialogHeader",
      "AlertDialogOverlay",
      "AlertDialogPortal",
      "AlertDialogTitle",
      "AlertDialogTrigger"
    ],
    "tagline": "Canonical modal confirmation flow (destructive / high-stakes decisions). Preserves confirm semantics with `role=\"alertdialog\"` and built-in cancel/confirm handling.",
    "usage": [
      "COMPOUND (AlertDialogRoot): Reach for the flat `AlertDialog` preset FIRST — it already covers title/description/confirm/cancel, the typed `challenge`, `stepUp` re-auth and `pending`. Use `AlertDialogRoot` only when the confirm body needs content the preset does not model (an impact summary, a diff, a nested list).",
      "COMPOUND (AlertDialogRoot): DO name it `AlertDialogRoot`, not `AlertDialog` — the `AlertDialog` export is the flat preset and takes a completely different (non-compound) prop API.",
      "COMPOUND (AlertDialogRoot): DO include `AlertDialogHeader` with `AlertDialogTitle` inside every `AlertDialogContent` — `role=alertdialog` needs an accessible title; omitting it warns in the console and breaks screen-reader announcement. `AlertDialogHeader` also takes the prop-driven `title`/`subtitle`/`extra`/`tone` form, where `subtitle` renders the `AlertDialogDescription`.",
      "COMPOUND (AlertDialogRoot): DO portal explicitly: `AlertDialogContent` does NOT self-portal (unlike `DialogContent`). Wrap it in `AlertDialogPortal` with a sibling `AlertDialogOverlay`, or the scrim and stacking context are wrong.",
      "COMPOUND (AlertDialogRoot): DO use `AlertDialogAction` / `AlertDialogCancel` for the footer buttons — they carry the button styling AND the close semantics. Do not wrap them in `asChild` `<Button variant=…>`: the Root's button classes and the child's would both land on the element and the variant would not win.",
      "COMPOUND (AlertDialogRoot): DO NOT import `@radix-ui/react-alert-dialog` directly in a consumer app. Everything the compound needs is exported from `@godxjp/ui/feedback`.",
      "Use `AlertDialog` for destructive/irreversible actions (delete, void, unpublish, archive, etc.).",
      "Use `confirmPhrase`/`challenge` for high-friction operations (e.g. typing an org slug) to reduce accidental confirmation — both force the destructive shape: the destructive confirm button plus a leading status glyph beside the title, which is Ant Design `Modal.confirm` parity. The header surface stays UNTINTED — antd signals danger with the glyph and never tints a modal header (its soft `colorErrorBg` belongs to Alert/Tag/message). To tint the band anyway, set `DialogHeader tone` yourself; it is a separate seven-value axis the preset no longer imposes.",
      "Pass `stepUp` for a passkey/2FA re-auth gate that must resolve truthy before `onConfirm` runs (refunds, org deletion).",
      "Pass `keepOpenOnConfirm` when the confirm handler advances a multi-step flow and should not close immediately."
    ],
    "useCases": [
      "Dangerous delete or irreversible workflow confirmation that should block the background UI.",
      "Organization/resource deletion gated behind typing the exact slug (`challenge`).",
      "Refunds or privileged actions requiring step-up re-authentication before they run.",
      "Destructive batch operations that should remain modal and explicit until action is intentionally confirmed."
    ]
  },
  {
    "example": "import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\n\n<Sheet open={open} onOpenChange={setOpen}>\n  <SheetTrigger asChild><Button variant=\"outline\" size=\"sm\">絞り込み</Button></SheetTrigger>\n  <SheetContent side=\"right\">\n    <SheetHeader><SheetTitle>フィルター設定</SheetTitle></SheetHeader>\n    {/* filter fields */}\n  </SheetContent>\n</Sheet>",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "Sheet",
    "props": [
      {
        "description": "Controlled open state.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Open-state change handler.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "defaultValue": "true",
        "description": "On Sheet (root). `false` renders a NON-MODAL sheet (WAI-ARIA APG allows non-modal dialogs): the page behind stays interactive and in the accessibility tree (no inert/aria-hidden), no scroll lock, no scrim, an outside press does NOT close it, and there is no `aria-modal`. The panel keeps role=dialog + its title as name, and the same side placement, width, responsive presentation and tokens. Focus moves into it on open, Tab can leave it, Escape closes it while focus is inside, and focus returns to the trigger on close (only if focus was still inside). Same contract as Dialog `modal={false}`. gh#701.",
        "name": "modal",
        "type": "boolean"
      },
      {
        "description": "On SheetContent (side left/right): desired panel width (number→px). Default w-3/4 sm:max-w-md.",
        "name": "width",
        "type": "number | string"
      },
      {
        "description": "On SheetContent (same name as on DialogContent): fires on every close before focus returns to the trigger. Call event.preventDefault() and focus your own target (e.g. the new page's heading after a drawer navigation) to send focus there instead (gh#1134).",
        "name": "onCloseAutoFocus",
        "type": "(event: Event) => void"
      },
      {
        "defaultValue": "\"side\"",
        "description": "On SheetContent: the responsive drawer / detail-panel contract. \"side\" (default) always renders the physical `side` you named. \"auto\" renders the desktop side panel above --sheet-responsive-breakpoint-width (48rem/768px) and the mobile BOTTOM sheet at/below it, capped by --sheet-bottom-max-height (85dvh). \"bottom\" pins the bottom-sheet presentation.",
        "name": "responsive",
        "type": "\"auto\" | \"side\" | \"bottom\""
      },
      {
        "description": "On SheetHeader (Ant-style): title (→ SheetTitle, accessible name), subtitle (→ SheetDescription), right-aligned extra actions, and a soft semantic `tone` background band. Children still supported.",
        "name": "title / subtitle / extra / tone",
        "type": "ReactNode / ReactNode / ReactNode / ToneProp"
      }
    ],
    "related": [
      "Dialog — use Dialog (centered modal) when the action is destructive, requires full user focus, or needs a confirm/alertdialog (mode='confirm'). Use Sheet when the user benefits from seeing the page content behind the slide-over (filters, detail peek, quick-edit).",
      "Toolbar/ToolbarGroup — use Toolbar for inline persistent filter controls above a DataTable (no overlay). Use Sheet when the filter set is large (>4 controls) or on mobile where inline controls collapse poorly.",
      "Popover — use Popover for lightweight, anchor-positioned context menus or single-control overlays (date picker, color picker). Use Sheet when the panel has a header, multiple fields, or footer actions that need a dedicated panel.",
      "SplitPane — use SplitPane for a persistent side-by-side layout where both panes are always visible. Use Sheet when the secondary panel is transient and should overlay the primary content."
    ],
    "rules": [
      3
    ],
    "storyPath": "feedback/sheet.tsx",
    "subParts": [
      "SheetBody",
      "SheetClose",
      "SheetContent",
      "SheetDescription",
      "SheetFooter",
      "SheetHeader",
      "SheetOverlay",
      "SheetPortal",
      "SheetTitle",
      "SheetTrigger"
    ],
    "tagline": "Side-panel drawer / responsive detail panel (React Aria modal dialog). Parts: Sheet/SheetTrigger/SheetContent(side=right|left|top|bottom, responsive=auto|side|bottom)/SheetHeader/SheetBody/SheetTitle/SheetFooter.",
    "usage": [
      "DO build the panel with SheetHeader (pass `title`/`subtitle`/`extra`/`tone` OR children) > SheetBody (scrollable, ring-safe) > SheetFooter (pinned). SheetTitle is required for a11y — the `title` prop renders it for you. Never skip the title.",
      "DO set `width` on SheetContent for a wider/narrower panel (e.g. width={480}); it caps at the viewport so small screens still get a full-width panel.",
      "DO use responsive=\"auto\" for a record detail panel / drawer that must be a desktop side panel and a mobile bottom sheet — ONE <Sheet>, no page-local media query. The breakpoint is the --sheet-responsive-breakpoint-width token, so a service moves the line once for every overlay. `width` is ignored while the bottom presentation is active (a bottom sheet is full-bleed).",
      "DON'T hardcode an overlay breakpoint in app code (useMediaQuery(\"(max-width: 390px)\")). If a composite must swap a desktop surface for a mobile sheet (a Popover→Sheet switcher, for example), call the exported useSheetResponsiveMode(\"auto\") hook so it reads the same themeable token.",
      "DO use all named sub-parts in order: Sheet (root) > SheetTrigger (opener) > SheetContent (panel) > SheetHeader > SheetTitle (required for a11y — it is the dialog's heading, announced as the accessible name) > optional SheetDescription > body content > SheetFooter. Never skip SheetTitle inside an open SheetContent.",
      "DO control state explicitly with open + onOpenChange on Sheet root when you need to close programmatically (e.g. after form submit). Uncontrolled (no props) works for simple trigger-only cases but gives you no hook to reset form state on close.",
      "DO use SheetTrigger asChild to wrap a Button or other interactive element — this avoids a nested <button> in the DOM. Never render a raw <button> as a direct child of SheetTrigger.",
      "DO wrap a long/scrolling body in SheetBody (between SheetHeader and a pinned SheetFooter). It is the ring-safe scroll slot: a hand-rolled <div className='overflow-y-auto'> clips the 3px focus ring of a full-width Input/Select at the scroll edges — SheetBody insets the content so the ring never clips.",
      "DO use SheetFooter (renders at the bottom via mt-auto, symmetric 16/24 padding, full-bleed top border) for primary/cancel action Buttons. Never float action Buttons inside the body — they will not stick to the panel bottom.",
      "DO put one <form> around SheetBody + SheetFooter when the footer holds the submit button (`<Form asChild errors={…}><form onSubmit={…}><SheetBody>…</SheetBody><SheetFooter><Button type=\"submit\">…</Button></SheetFooter></form></Form>`). A <form> that wraps SheetBody is transparent to the panel layout (same in Dialog), so the body still scrolls and the footer stays pinned (gh#1239); no className on the form is needed.",
      "DON'T set showCloseButton={false} on SheetContent unless you provide your own SheetClose element; omitting both leaves users with no keyboard-accessible close path and breaks a11y.",
      "DO set `modal={false}` on Sheet when the user must keep working on the page behind the open panel (edit a list while a detail panel stays open). Control `open` yourself: an outside press no longer closes it, so keep the ✕ or a footer close action. Escape closes it only while focus is inside the panel.",
      "DON'T put a Sheet inside a Dialog (nested modal overlays conflict). If you need a slide-over triggered from within a modal, close the Dialog first, then open the Sheet."
    ],
    "useCases": [
      "Filter/search panel: slide in from the right with filter FormFields (Select, `DatePicker range`, CheckboxGroup) that affect a DataTable — preferred over a Dialog because filters do not require confirmation and benefit from seeing the table behind the overlay.",
      "Quick-edit drawer: open an entity's editable fields (e.g. invoice line items, account settings) without navigating away, with Save/Cancel in SheetFooter — use side='right' and keep the main page visible as context.",
      "Detail peek panel: show read-only Descriptions / Timeline of a selected record (e.g. a journal entry or invoice) from a DataTable row click, using side='right' with showCloseButton={true}. Add responsive='auto' so the same panel becomes a bottom sheet on a phone instead of a 100%-wide slab.",
      "Non-modal side panel: `modal={false}` keeps a list behind the sheet editable while the panel stays open (change order lines while their running total stays visible in the panel).",
      "Mobile-first navigation drawer: side='left' sheet acting as a slide-in nav menu on small viewports when the AppShell Sidebar is hidden — triggered by a hamburger Button.",
      "Step-by-step wizard side panel: multi-step form (Steps component inside SheetContent) for onboarding or import flows where full-page navigation would lose list context."
    ]
  },
  {
    "absorbed": [
      "Banner",
      "Callout"
    ],
    "example": "import { Alert, AlertTitle, AlertDescription } from \"@godxjp/ui/feedback\";\n\n<Alert tone=\"warning\">\n  <AlertTitle>3 件の打刻漏れがあります</AlertTitle>\n  <AlertDescription>本日中に確認してください。</AlertDescription>\n</Alert>",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "Alert",
    "props": [
      {
        "defaultValue": "\"default\"",
        "description": "STRUCTURAL axis, orthogonal to `tone` (which owns colour + icon): \"default\" is the inline rounded card; \"banner\" is the full-bleed page/shell attention strip (square corners, page-gutter inset, actions wrap below 640px); \"callout\" is the aside INSIDE a document body — `role=\"note\"`, never a live region, `kind` picks the admonition. v32 folded the former `Banner` and `Callout` exports into these values.",
        "name": "variant",
        "type": "\"default\" | \"banner\" | \"callout\""
      },
      {
        "defaultValue": "\"note\"",
        "description": "`variant=\"callout\"` only. GitHub/Obsidian admonition preset; resolves the tone AND the leading glyph. A preset, not a second colour axis — `tone` and `icon` still override per instance. Ignored by the other variants.",
        "name": "kind",
        "type": "\"note\" | \"tip\" | \"important\" | \"warning\" | \"caution\""
      },
      {
        "description": "Renders an × dismiss button when provided.",
        "name": "onDismiss",
        "type": "() => void"
      },
      {
        "description": "Override or hide (false) the icon.",
        "name": "icon",
        "type": "LucideIcon | false"
      },
      {
        "description": "Semantic tone driving the colour + leading icon AND live-region politeness (\"destructive\"/\"warning\" announce assertively via role=\"alert\"; every other tone politely via role=\"status\"). Under `variant=\"callout\"` tone drives colour only — a callout is never a live region.",
        "name": "tone",
        "type": "\"default\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"muted\" | \"neutral\""
      }
    ],
    "related": [
      "Toaster — use for transient, auto-dismissing feedback ('Record saved', 'Deleted'). Alert is for persistent page-scoped banners; Toaster is for fire-and-forget notifications triggered by toast() from sonner.",
      "AlertMutationFeedback — use when you want inline success/error feedback tightly coupled to a form mutation's state (renders inline below the submit button). Alert requires you to manage show/hide state yourself.",
      "DataState — use for full query lifecycle (loading skeleton + empty state + error) inside a data-fetching section. Alert.QueryError is the error sub-component DataState uses internally; prefer DataState when you also need the loading/empty states.",
      "ErrorSurface — a whole-page semantic exception state (403/404/5xx) that REPLACES the page. An `Alert` (any variant) annotates a page that still works.",
      "Prose — the typographic column a `variant=\"callout\"` normally sits inside; the callout's block margin is tuned to that rhythm.",
      "EmptyState — use for the zero-data case inside a list or table section, not for errors or warnings. Alert is for status messages; EmptyState is for the absence of data."
    ],
    "rules": [],
    "storyPath": "feedback/alert.tsx",
    "subParts": [
      "AlertActions",
      "AlertBase",
      "AlertContent",
      "AlertDescription",
      "AlertMutationFeedback",
      "AlertQueryError",
      "AlertTitle"
    ],
    "tagline": "Status message with a tone icon and optional dismiss, in three presentations: inline card, full-bleed page/shell strip (`variant=\"banner\"`), or a non-live aside inside a document (`variant=\"callout\"`, GitHub `> [!NOTE]`). Parts: Alert.Title/Description/Content/Actions/QueryError.",
    "usage": [
      "ANATOMY (positions are fixed — never re-lay-them-out): Alert is ONE horizontal row — a SINGLE leading tone icon at the inline-start (top-aligned to the first text line, auto-selected by `tone`, never two icons), then the text body (Title/Description), then `<Alert.Actions>` in a trailing-RIGHT column (≥sm), and the dismiss × pinned to the TOP-RIGHT corner (rendered by `onDismiss`). DON'T stack these vertically, DON'T make the action a full-width bar under the text, DON'T center the × at the bottom, DON'T add a second icon — that hand-rolled vertical banner is the #1 Alert mistake.",
      "DO compose text as `<Alert.Title>` + `<Alert.Description>` — they stack vertically inside the body (the Example below is canonical). When you add `<Alert.Actions>`, the body becomes a two-column grid (text | actions) at ≥sm; group multi-part text in `<Alert.Content>` so it occupies the text column as one block: `<Alert tone=\"destructive\"><Alert.Content><Alert.Title>Error</Alert.Title><Alert.Description>{msg}</Alert.Description></Alert.Content><Alert.Actions><Button …/></Alert.Actions></Alert>`.",
      "DO use `Alert.QueryError` (alias `AlertQueryError`) for TanStack Query / API failure surfaces — it already renders humanError(error), an i18n title, and an optional Retry button. Never hand-roll that pattern.",
      "`AlertMutationFeedback` props: `mutation`, `onRetry?`, `showRetry?` (default `true`), `pending?`, `ignoreValidationErrors?: boolean`, `className?`. `ignoreValidationErrors={true}` skips the alert for every error where `classifyQueryError(mutation.error).category === \"validation\"` (400/422). DEFAULT (omitted): skip a validation error only when rendered inside a `FormRoot`/`Form` whose `errors` bag holds at least one message (the fields show it, `<FormErrors />` shows unclaimed keys); outside such a form, or with an empty/absent bag, the alert renders. So DON'T hand-guard `{!isValidation && <AlertMutationFeedback …/>}` inside `FormRoot errors={…}` — a 422 is not drawn twice, and a 5xx still renders. Pass `ignoreValidationErrors={false}` to force the alert in such a form.",
      "DON'T pass raw action elements directly as top-level children of `<Alert>` without wrapping them in `<Alert.Actions>` — the layout slot only activates correctly via the `data-slot=\"alert-actions\"` wrapper.",
      "DON'T hand-roll a dismiss ✕ button — pass `onDismiss` to `<Alert>` and the component renders its own accessible dismiss button with `aria-label=\"Dismiss\"`. The `onDismiss` handler may return a Promise.",
      "DON'T suppress the icon with `icon={false}` unless there is a deliberate design reason; the icon is the primary a11y cue for sighted users since the root already carries `role=\"alert\"` for screen readers.",
      "BANNER (`variant=\"banner\"`): place it FULL-BLEED at the top of the surface it warns about — directly under the Topbar inside AppShell's children for shell-wide attention (past-due subscription, active support session), or as the first child of a page. Its inline inset defaults to the page gutter (--space-page-active-x) and its geometry is owned by the `--banner-*` tokens; never fake a strip with `className` overrides. At most ONE per surface.",
      "CALLOUT (`variant=\"callout\"`): for content that is PART of the document being read — a docs admonition, a CMS note, a policy caveat. It renders `role=\"note\"` and never announces. Map Markdown `[!NOTE]`/`[!TIP]`/`[!IMPORTANT]`/`[!WARNING]`/`[!CAUTION]` straight onto `kind`. DON'T pass `onDismiss` (prose is not dismissed) and DON'T use a callout for something that just HAPPENED — that is the default or banner variant, which announce.",
      "LIVE OR NOT (gh#1271): static, unchanging guidance → `variant=\"callout\"` (not a live region, `role=\"note\"`); `role=\"alert\"`/`role=\"status\"` (the default and banner variants) are for messages that appear in response to something — a failed save, a sync that finished. A hint that is on the page when it loads is not a status message (WCAG 4.1.3), so it must not announce on every render.",
      "DO NOT use `Alert` for transient ephemeral feedback (e.g. 'saved successfully'). Use `toast()` from sonner + `<Toaster>` for that. `Alert` is for persistent, page-scoped banners that stay visible until the user acts or dismisses."
    ],
    "useCases": [
      "Page-level error banner after a form submission fails server-side validation — `tone=\"destructive\"` with `Alert.Title` summarising the error and `Alert.Description` listing field issues, paired with `onDismiss` so the user can clear it.",
      "Inline warning at the top of an accounting invoice list when the OAuth token for the MF sync is about to expire — `tone=\"warning\"` with an `Alert.Actions` containing a 'Reconnect' Button.",
      "Success confirmation banner rendered after a bulk-import job completes and the user returns to the list page — `tone=\"success\"` with `Alert.Description` showing the record count imported.",
      "TanStack Query data-fetch failure inside a Card body — use `<Alert.QueryError error={error} onRetry={refetch} />` instead of writing a custom error state.",
      "Informational notice at the top of a settings page when a feature is in beta or requires a plan upgrade — `tone=\"info\"` with a short description and an `Alert.Actions` 'Learn more' link.",
      "Past-due subscription strip across the console shell — `variant=\"banner\" tone=\"warning\"` with an `Alert.Actions` \"お支払い方法を更新\" Button, rendered above the page slot until billing recovers.",
      "Docs/handbook admonition inside a prose column — `variant=\"callout\" kind=\"tip\"`; a destructive-consequence note beside a runbook step is `kind=\"caution\"`, still silent on load.",
      "Dismissible billing-overdue notice at the top of the dashboard — `tone=\"destructive\"` with `onDismiss` that sets a session flag so it does not reappear until the next login."
    ]
  },
  {
    "example": "// app root — mount once\nimport { Toaster } from \"@godxjp/ui/feedback\";\n<>{children}<Toaster richColors /></>\n\n// anywhere\nimport { toast } from \"@godxjp/ui/feedback\";\ntoast.success(\"クーポンを公開しました\"); // auto-dismiss after --toast-duration\ntoast.error(\"保存に失敗しました\"); // also carries a close button",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "Toaster",
    "props": [
      {
        "defaultValue": "\"bottom-right\"",
        "description": "Toast stack anchor.",
        "name": "position",
        "type": "\"top-right\" | \"top-center\" | \"bottom-right\" | \"…\""
      },
      {
        "description": "Enable Sonner rich variant colours.",
        "name": "richColors",
        "type": "boolean"
      },
      {
        "description": "Auto-dismiss time in ms for every toast. Omit it to let the `--toast-duration` token (default 4000ms) decide — read once on mount from the ThemeScope host or `<html>` (gh#1253). A per-call `toast(…, { duration })` still wins.",
        "name": "duration",
        "type": "number"
      },
      {
        "description": "Sonner's switch for a close button on EVERY toast. Leave it unset for the default: `toast.error` / `toast.warning` from `@godxjp/ui/feedback` carry a close button, `toast.success` / `toast.info` stay auto-dismiss only (gh#1253). The button is named via `t(\"feedback.toast.close\")`.",
        "name": "closeButton",
        "type": "boolean"
      }
    ],
    "related": [
      "Alert — use for persistent, inline feedback that must stay visible (validation summaries, page-level warnings, destructive notices). Unlike Toaster, Alert does not auto-dismiss and lives in the document flow.",
      "AlertMutationFeedback — use when you have a TanStack `useMutation` result and want an inline error + retry UI inside a form or card. Renders nothing on success/idle; pairs naturally with a `toast.success` in `onSuccess`.",
      "Dialog — use when the user must make a conscious decision (confirm delete, resolve conflict) before proceeding. Toaster toasts are fire-and-forget; Dialog blocks until the user responds."
    ],
    "rules": [],
    "tagline": "Mount once at app root to enable toasts. Trigger them with `import { toast } from \"@godxjp/ui/feedback\"` — Sonner's API, with a close button on `toast.error` / `toast.warning` by default.",
    "usage": [
      "DO: Mount exactly ONE `<Toaster richColors />` at the app root (e.g. inside your layout or AppShell children). Multiple mounts create duplicate toast stacks — there is no provider context, only DOM portals.",
      "DO: Import `toast` from `\"@godxjp/ui/feedback\"` to fire toasts anywhere: `toast.success(…)`, `toast.error(…)`, `toast.warning(…)`, `toast.info(…)`, `toast.loading(…)`, `toast.promise(…)`. It is Sonner's own API with one house default: `toast.error` and `toast.warning` carry a close button (opt out per call with `{ closeButton: false }`), success/info stay auto-dismiss only (gh#1253).",
      "DON'T: Import `toast` from `\"sonner\"` directly — it works, but it skips the close-button default on error/warning. Also note `toast.promise` settling into an error does not gain the button; pass `closeButton` there yourself if the failure must stay dismissible.",
      "DO: Tune how long toasts stay with the `--toast-duration` token in the theme (default 4000ms) rather than a `duration` prop on every mount. Never truncate a toast's body — it wraps.",
      "DO: Let the wrapper handle theming — it uses `useDocumentTheme()` to sync with the document `dark` class and `prefers-color-scheme` automatically. Never pass a hardcoded `theme` prop unless you are deliberately overriding.",
      "DON'T: Use `Toaster` for persistent errors or blocking confirmations. Toasts auto-dismiss; they are not a substitute for `Alert` (inline persistent warnings) or `Dialog` (decisions requiring user input).",
      "DO: Pass `position` to relocate the stack if a persistent sidebar would obscure the default `bottom-right`. The wrapper already sets a safe `mobileOffset`; don't add redundant mobile offsets unless your layout differs.",
      "DON'T: Hand-tune `offset` to dodge a sticky action bar. The bottom offset (desktop 24px, mobile `--toast-mobile-offset`) already adds `--toast-sticky-bar-height`, which a visible `PageContainer stickyFooter` or `MobileShell` action/tab bar publishes at runtime, so the stack clears it on its own (gh#1271). For bottom chrome the library does not render (an app's own fixed bar), set `--toast-offset-bottom` once in theme.css — it is added on desktop and mobile alike."
    ],
    "useCases": [
      "After a successful form save (invoice, journal entry, vendor record) — show `toast.success(\"保存しました\")` to confirm without blocking navigation.",
      "After a background job is enqueued (e.g. bulk sync or export) — show `toast.info(\"エクスポートを開始しました\")` then later update with `toast.promise()` to track completion.",
      "Mutation error fallback when the error is transient and retrying is the right UX — show `toast.error(message)` instead of replacing page content; reserve `AlertMutationFeedback` for inline, persistent error display inside a form.",
      "Soft destructive action confirmation outcome — e.g. \"削除しました\" after an item is removed, paired with an undo action via `toast(\"…\", { action: { label: '元に戻す', onClick: undo } })`.",
      "OAuth / session expiry warnings — surface a brief `toast.warning(\"セッションの有効期限が近づいています\")` without interrupting the user's current form state."
    ]
  },
  {
    "example": "import { Tabs } from \"@godxjp/ui/navigation\";\n\n<Tabs\n  defaultValue=\"overview\"\n  items={[\n    { value: \"overview\", label: \"概要\", content: \"概要コンテンツ\" },\n    { value: \"history\", label: \"履歴\", content: \"履歴コンテンツ\" },\n  ]}\n/>",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "Tabs",
    "props": [
      {
        "description": "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` — never a disabled one — and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant=\"editable-card\"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` — antd's per-item `destroyOnHidden` is deliberately not offered, because on this component \"kept mounted\" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label — 「未対応 12」 — in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces \"未対応, 12 件の課題\" and never the concatenated \"未対応12\". Prefer this over antd's answer, which is to put a Badge inside `label` — with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently. `href` makes the strip a ROUTE strip (gh#1364) — see `linkComponent`.",
        "name": "items",
        "type": "{ value: string; label: React.ReactNode; content?: React.ReactNode; href?: string; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]"
      },
      {
        "description": "Controlled active tab key.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial tab key. Ignored (falls back to the first enabled item) when it names a disabled item or an unknown key.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Active-tab change handler.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Strip-only mode (gh#1021): the tabs drive content OUTSIDE the strip. No tabpanel is rendered, and the selected tab's `aria-controls` points at this id, which is the consumer's own region (give it `role=\"region\"` and a name). Content there is never remounted on a tab switch, so a typed draft survives. With the compound form it applies to triggers without a declared `TabsContent` (a declared panel wins). A pure filter with NO region to point at is not a tab strip; use `Segmented`. The root's `aria-label`/`aria-labelledby` name the `role=\"tablist\"` (gh#1020), and a compound `TabsTrigger` takes `count`/`overflowCount`/`showZero`/`countLabel`, drawn exactly like `items[].count`.",
        "name": "controls",
        "type": "IdProp"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Trigger-strip appearance — this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all — only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) — so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`.",
        "name": "variant",
        "type": "\"default\" | \"line\" | \"card\" | \"editable-card\""
      },
      {
        "defaultValue": "\"top\"",
        "description": "Which edge the trigger strip parks on — Ant Design 6.6.2's `tabPlacement` (its `tabPosition` is deprecated there), so the inline values are already RTL-logical. `start`/`end` also flip the tablist to vertical roving focus, which is what `orientation=\"vertical\"` did on its own before; either prop still works and the other is derived from it. The strip stays FIRST in the DOM at every placement — `bottom`/`end` are a flex reversal, not a re-ordered tree. NARROW FOLD: `start`/`end` become `top`/`bottom` (arrow keys included) at or below `--tabs-placement-responsive-breakpoint-width` (48rem) — a vertical strip and its panel share one inline axis and a phone has room for one of them; Ant Design folds the same pair the same way. Set that token to `0px` to keep the strip vertical at every width.",
        "name": "tabPlacement",
        "type": "\"top\" | \"bottom\" | \"start\" | \"end\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control tier of the triggers (Ant Design `size`). Expressed as the library's own control bands (--tabs-trigger-height-*/--tabs-trigger-font-size-*), so a tab strip and the Buttons beside it stay on one rhythm; `md` reproduces the previous trigger exactly.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Ant Design `centered` — centre the strip on its own inline axis. Keeps the `safe` centring rule the strip depends on, so a strip that overflows still falls back to start alignment instead of stranding the leading tab outside the scrollport.",
        "name": "centered",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw the PANEL BODY the card strip opens into, so the strip and the panel are ONE object: a surface, a border continuous with the rail, and the radius only on the two corners AWAY from the strip. `variant=\"card\"` already repaints the active tab's joined edge in the surface colour (antd `genCardStyle`), but the package shipped no surface for it to merge into — measured at 1440px, `<Card>` under a card strip left an 8px gap AND a second 1px border at a 9.708px radius (two stacked boxes), `<Card variant=\"borderless\">` and no box at all left the same 8px gap. With `bodied`: gap 0 (the body is pulled back exactly one border width so its edge and the tabs' sit on one device row), one continuous line around the whole object, and no line at all across the active tab. HONOURED ONLY BY `card` / `editable-card` — the pill strip floats by design and the `line` strip's body is the `Card` it lives in (`<Card tabList>`), which is already joined; on any other variant it emits no attribute and changes no pixel. Retune it from `--tabs-panel-{background,border-width,radius,space-inset}`; `--tabs-panel-background` is read by the body AND by the merged tab edge, so the two can never disagree.",
        "name": "bodied",
        "type": "boolean"
      },
      {
        "description": "Ant Design `tabBarExtraContent`, renamed to the library's `extra` slot and made logical: antd's `left`/`right` keys are `start`/`end` here. A bare node goes to `end` (antd's own default). Renders a bar row beside the strip; without it — and without an add button — no extra wrapper is emitted at all. Centred on the tab faces (antd `.ant-tabs-nav { align-items: center }`). Under `bodied` the tabs stretch to the bar's height (antd's stretched nav list), so they stay joined to the body while the extra stays centred and off the body's border; only the add button sits on the rail (gh#1073).",
        "name": "extra",
        "type": "React.ReactNode | { start?: React.ReactNode; end?: React.ReactNode }"
      },
      {
        "defaultValue": "true",
        "description": "Ant Design `destroyOnHidden`. `true` (the default here, and Radix's own behaviour) unmounts a panel the moment it stops being selected. `false` keeps EVERY panel mounted and only hides the inactive ones, so a live chart, a scroll position or an unsent form draft survives a tab switch. The default is deliberately the opposite of antd's, which keeps panels mounted.",
        "name": "destroyOnHidden",
        "type": "boolean"
      },
      {
        "description": "Ant Design `onEdit`. Required for `variant=\"editable-card\"` to grow its controls. `remove` passes the item's own `value`; `add` passes the click event. Tabs never mutates `items` itself — the consumer owns the list.",
        "name": "onEdit",
        "type": "(target: string | React.MouseEvent<HTMLButtonElement>, action: \"add\" | \"remove\") => void"
      },
      {
        "description": "Ant Design `addIcon` — replaces the default + on the editable-card add button.",
        "name": "addIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "Ant Design `hideAdd` — keep editable-card's remove shortcuts but drop the add button.",
        "name": "hideAdd",
        "type": "boolean"
      },
      {
        "description": "Ant Design `removeIcon` — the strip-wide default glyph on `editable-card`'s remove shortcut. An item's own `closeIcon` still wins over it, which is antd's precedence. It replaces the glyph only: the × stays an `aria-hidden` pointer shortcut inside the tab and the announced route stays Delete/Backspace, so a custom icon never becomes a second focusable control inside a `role=\"tab\"`.",
        "name": "closeIcon",
        "type": "React.ReactNode"
      },
      {
        "defaultValue": "\"scroll\"",
        "description": "What the trigger strip does when there are more tabs than fit — Ant Design's `more`, mapped onto the `overflow` vocabulary Topbar already uses for the same question rather than re-spelled. `scroll` (default, and what every strip does today) keeps one bounded row that scrolls its own inline overflow, with the active (or, under manual activation, focused) trigger re-pinned into view. `menu` keeps all of that AND puts a real named button beside the strip listing the tabs currently outside the scrollport; choosing one selects it. DIVERGES FROM ANTD DELIBERATELY: antd REMOVES the overflowing tabs from the bar, but the WAI-ARIA APG tab pattern requires the tablist to own every tab and a `display: none` tab cannot take roving focus — so here every tab stays in the strip and the menu is an ADDITIONAL pointer route, not a relocation. The default is not `menu` because switching it would change the rendered bar for every existing consumer at once.",
        "name": "overflow",
        "type": "\"scroll\" | \"menu\""
      },
      {
        "description": "Ant Design `onTabClick`. POINTER activation of a trigger, carrying the DOM event — that is what makes it a different prop from `onValueChange` rather than a second spelling of it. Keyboard activation is NOT routed here: under `activationMode=\"manual\"` the arrow keys move focus without activating, so a key-driven \"click\" would be a fiction. Use `onValueChange` for the selection, whatever moved it. Note that `onValueChange` also fires when the ALREADY SELECTED tab is clicked, so `onTabClick` is not the way to detect a re-click.",
        "name": "onTabClick",
        "type": "(value: string, event: React.MouseEvent<HTMLButtonElement>) => void"
      },
      {
        "defaultValue": "{ inkBar: true, tabPane: false }",
        "description": "Ant Design `animated`, ported from antd's own `useAnimateConfig`: `false` turns both switches off, `true` turns both ON, an object merges over `{ inkBar: true }`. `inkBar` is the `line` variant's active bar cross-fading between triggers (on by default, and the default is byte for byte what the strip already painted). `tabPane` fades the panel in when the selection moves — antd's motion really is an opacity fade and nothing else, so this is a port, not an invention; it reads `--tabs-pane-motion-duration`. Both switches are additionally off under `prefers-reduced-motion`, which antd's are not. What is NOT ported is antd's fade-OUT of the leaving pane (it parks the old node `position: absolute; inset: 0`): this component destroys a hidden panel by default, so there is usually no leaving node.",
        "name": "animated",
        "type": "boolean | { inkBar?: boolean; tabPane?: boolean }"
      },
      {
        "defaultValue": "{ size: \"full\", align: \"center\" }",
        "description": "Ant Design `indicator`, governing the `line` variant's active bar only (no other variant has one). `align` keeps antd's name AND its values, which are already logical, so it reads the shared TextAlignProp vocabulary. `size` keeps antd's name with this library's values: antd takes `number | (origin) => number` — a px length or a function of the measured tab width — and neither can enter this API (a literal is what `no-arbitrary-spacing` stops; an origin function is the free-form escape hatch docs/DESIGN-AUTHORITY.md refuses). `full` is the whole trigger (antd's default, today's bar) and `label` is the trigger's content box, i.e. minus its own inline padding — which is what `size: (origin) => origin - 2 * padding` is written to produce. `align` only has anything to place once `size` is shorter than the trigger, exactly as upstream.",
        "name": "indicator",
        "type": "{ size?: \"full\" | \"label\"; align?: \"start\" | \"center\" | \"end\" }"
      },
      {
        "description": "Ant Design `moreIcon` (its `more.icon` in 6.x; the flat prop is still published there) — the glyph on the `overflow=\"menu\"` button. Flat here for the same reason `addIcon` and `closeIcon` are: `overflow` names the BEHAVIOUR, the icon is a slot. The button keeps its own `aria-label`, so a custom glyph never costs the control its accessible name.",
        "name": "moreIcon",
        "type": "React.ReactNode"
      },
      {
        "description": "Ant Design `onTabScroll`, fired whenever the trigger strip's own scrollport moves — a swipe, a wheel, or the component re-pinning the active trigger (antd reports its own re-pins too). LOGICAL VALUES instead of antd's `left | right | top | bottom`: two of those four are just the other axis of the same event, and upstream's pair is read off the sign of an inner transform, so in an RTL strip its `left` means the opposite of what it means in LTR. `start`/`end` say the same thing on whichever axis and in whichever direction the strip is written. Only fires for the `items` API, which is the path that owns the strip element.",
        "name": "onTabScroll",
        "type": "(info: { direction: \"start\" | \"end\" }) => void"
      },
      {
        "description": "ROUTE STRIP (gh#1364). When ANY item carries `href`, Tabs renders a `<nav>` landmark of real links instead of a tablist: each tab is an `<a href>` (middle-click, Ctrl/⌘-click, \"open in new tab\" and the context menu all work — no click is ever `preventDefault`ed), the item whose `value` equals `value` gets `aria-current=\"page\"`, and no tabpanel is rendered (`content` is ignored; the route renders its own page). Pass the CURRENT ROUTE as `value` (derive it from the pathname). `linkComponent` is the SAME contract and the same value as `Sidebar.linkComponent` / `NavList.linkComponent` — Inertia `<Link>`, or `createSidebarLink(Link, \"to\")` for React Router / TanStack — and must forward every prop to its `<a>`. Without it each tab is a plain `<a href>`. Name the nav with `aria-label`. Same variants, sizes and `--tabs-*` tokens as the tablist, and the strip scrolls its overflow exactly like one (`overflow=\"menu\"` additionally exposes hidden routes as native links). Keyboard is the browser's: Tab walks the links, Enter follows one. WHY NOT React Aria link tabs: `<Tab href>` renders `<a role=\"tab\">` (the link role is hidden) and React Aria preventDefaults every real click on it to replay a synthetic one.",
        "name": "linkComponent",
        "type": "SidebarLinkComponentProp"
      },
      {
        "description": "Class on the TRIGGER STRIP (`TabsList`) under the `items` API — the handle that composing the tree manually gives you as `<TabsList className>`. `className` reaches only the root, which holds the strip AND the panels, so anything meant for the bar alone belongs here. Almost always unnecessary: placement, size, centring and the card rail are props and `--tabs-*` tokens.",
        "name": "listClassName",
        "type": "string"
      },
      {
        "description": "Class on EVERY panel (`TabsContent`) under the `items` API. It is written so it can WIN: the joined card body travels to CSS as `data-bodied` on the root rather than as a class, precisely so a consumer class on the panel is not fighting a utility the component already claimed (gh#762). Reach for the `bodied` prop and the `--tabs-panel-*` tokens first — this is for the geometry no token exposes.",
        "name": "contentClassName",
        "type": "string"
      }
    ],
    "related": [
      "Steps (@godxjp/ui/navigation) — sequential wizard/progress indicator. Use Steps when order and completion state matter (multi-step forms, onboarding flows); use Tabs when panels are non-sequential and any tab can be visited freely.",
      "Toolbar / ToolbarGroup (@godxjp/ui/navigation) — the list-page filter strip; it WRAPS onto extra rows and never scrolls sideways the way a tab strip may (rule #51). Visually resembles `line`-variant tabs but is semantically different: Toolbar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.",
      "DropdownMenu (@godxjp/ui/navigation) — use for space-constrained contexts where showing all tab triggers at once is impractical (e.g. mobile overflow menu). If only 2-3 options exist and screen space is tight, a Segmented control is a lighter alternative to a full tab strip."
    ],
    "rules": [],
    "storyPath": "navigation/tabs.tsx",
    "subParts": [
      "TabsContent",
      "TabsList",
      "TabsTrigger"
    ],
    "tagline": "React Aria tab container with optional Ant-style `items` API. Pass items for the common full TabsList/TabsContent set, or compose TabsList/TabsTrigger/TabsContent manually when you need per-panel control.",
    "usage": [
      "DO pass `items` when all tab content is known up front — each item needs a unique `value`, trigger `label`, and panel `content`.",
      "When not using `items`, compose the full four-part tree — `<Tabs>` root, `<TabsList>` trigger bar, one `<TabsTrigger value=\"…\">` per tab, one `<TabsContent value=\"…\">` per matching trigger.",
      "DO: use `defaultValue` (uncontrolled) for simple local state; use `value` + `onValueChange` together (controlled) when the active tab is driven by URL query params, router state, or parent state. NEVER set both simultaneously.",
      "DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",
      "DO: pass `orientation=\"vertical\"` to `<Tabs>` (not to `TabsList`) for a side-rail layout — the CSS group classes on root and triggers respond automatically, so no extra className gymnastics are needed.",
      "DON'T: hand-roll the active-indicator underline or selected-state ring — `TabsTrigger` already applies `data-[state=active]` styles, including the token-owned indicator bar for the `line` variant. Adding your own `border-b-2 border-primary` (or a page-local `ring-0` override to remove one) breaks the design; retune --tabs-indicator-{background,size,offset} in the service theme instead.",
      "DO trust the horizontal `TabsList` to scroll its own overflow (hidden scrollbar, swipeable) instead of clipping when tab labels — especially long localized ones (Japanese, German) — don't fit a narrow container. Don't wrap it in your own `overflow-x-auto` div or truncate labels to work around clipping; that is now the framework's job.",
      "DON'T assume the first item is ever auto-selected when it is `disabled` — Tabs always resolves the fallback to the first ENABLED item (or none, if all are disabled). A `disabled: true` first item is safe to author without also setting `defaultValue`.",
      "DON'T write your own resize/scroll-into-view effect to keep the selected tab on screen — `TabsList` observes its own size and its triggers' `data-state` and re-pins the active (or focused, under `activationMode=\"manual\"`) trigger with `scrollIntoView({ block: \"nearest\", inline: \"nearest\" })`, honoring `prefers-reduced-motion` and leaving a deliberate manual scroll alone. Before that, a 1440 → 1024 → 390 resize could strand the ACTIVE FIRST tab entirely outside the strip while it still reported `aria-selected=\"true\"`.",
      "DO reach for `variant=\"editable-card\"` + `onEdit` instead of hand-rolling a closable tab bar. The × inside a tab is an `aria-hidden` pointer shortcut and the announced keyboard route is Delete/Backspace on the focused tab (`aria-keyshortcuts`) — a real <button> there is an axe failure twice over (`aria-required-children`, because a tablist may own nothing but tabs, and `nested-interactive`). The ADD button is a real button because it sits outside the tablist.",
      "DO use `destroyOnHidden={false}` when a hidden panel must keep state — a mounted chart, a scroll position, an unsent draft. Note it is the opposite default from Ant Design: here panels are destroyed unless you say otherwise.",
      "DO add `bodied` whenever a `card`/`editable-card` strip stands on its own (a saved-views ribbon over a list). DON'T wrap the panel in your own `<Card>` to give it a surface — that is the measured defect `bodied` exists for: it adds a SECOND border at the Card radius over the strip↔panel gap and the pair reads as two stacked boxes. The one place you do not need it is inside `<Card tabList>`, where the card's own border already wraps strip and body.",
      "DO put a tab's count in `count` (+ `countLabel`), not inside `label`. A number concatenated into the label loses the pill's size, its tone as the tab is selected/deselected, and — measured on Button in gh#734 — the accessible name: the digits run straight onto the label («未対応12»). With the slot the tab announces \"未対応, 12 件の課題\". DON'T drop a `<Badge>` into `label` to get the same look; there is one counting API and Button, Toggle and Tabs all draw it.",
      "DON'T go looking for antd's `tabBarGutter`, `tabBarStyle`, `renderTabBar`, `classNames`/`styles` or `more.popupRender` — each is declined on the record, not missing. The gutter between triggers is a theme knob (`--tabs-list-line-space-gap`, `--tabs-card-list-space-gap`; the pill strip has no gutter by design) because a px number is a constant, not a semantic axis; the other four exist upstream to let a consumer replace the rendered markup, and this library answers that layer with tokens (docs/DESIGN-AUTHORITY.md refuses them by name).",
      "DON'T re-centre the strip with a `justify-center` utility. `TabsList` aligns with `safe center` on purpose: plain centring splits the overflow across BOTH edges while `scrollLeft` only ever covers the trailing one, so the leading tab ends up permanently outside the scrollport and no gesture reaches it. `safe` keeps the centred look while the tabs fit and falls back to start alignment the moment they don't.",
      "DO give a strip of sibling ROUTES (a settings area: /settings/cart-timeout, /settings/customer, …) `items[].href` + `linkComponent` and pass the current route as `value`. DON'T hand-roll it from `Link` pills in an `inline-flex rounded-lg bg-muted p-1` div (no `aria-current`, flagged by ui-audit), and DON'T drive a button tablist with `onValueChange` + `router.push` — that loses open-in-new-tab and middle-click (gh#1364).",
      "DON'T draw a list status view as Tabs. A list status view (All / Active / Closed, each with its count) that shows the SAME table — same columns, same row actions, a different query — is a filter, so it is `Segmented` with `count`, placed in the list's `Toolbar`; past 4 statuses it is a `Select`. `Tabs` only when each status is a genuinely different panel (its own columns, actions or empty state), declared as `TabsContent` or pointed at with `controls` (WAI-ARIA APG: a tab selects a panel, a radiogroup selects a value) (gh#1324)."
    ],
    "useCases": [
      "Detail drawers or pages that need full per-panel control — e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `Tabs` cannot pass.",
      "Controlled tabs driven by URL search params (e.g. `?tab=history`) where the parent reads/writes the active key and passes it to `value` / `onValueChange`.",
      "Vertical side-rail navigation inside a `SplitPane` or settings layout where `orientation=\"vertical\"` on the root and `variant=\"line\"` on `TabsList` combine to produce a sidebar-style tab strip.",
      "Lightweight widget tabs on a dashboard card — e.g. a 'Pending' panel (due date, remind action) and a 'Paid' panel (paid date, receipt link), each its own `DataTable` with its own columns — where an uncontrolled `defaultValue` is sufficient and no URL state is needed. Two views of the SAME table are a filter: `Segmented`.",
      "A settings area with ~14 sibling routes shown as a horizontal strip above the page body — `items[].href` + `linkComponent`, `value` = the current route; it scrolls at phone width (gh#1364).",
      "Admin entity profile pages (company, partner, employee) where each `TabsContent` wraps an Inertia deferred prop panel, lazy-loading expensive data only when the tab is first activated."
    ]
  },
  {
    "example": "import { Pagination } from \"@godxjp/ui/navigation\";\n\n<Pagination value={page} total={filtered.length} pageSize={10} showTotal onValueChange={(p) => setPage(p)} />",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "Pagination",
    "props": [
      {
        "description": "Tên khả truy cập của landmark phân trang. Bắt buộc khi một trang có nhiều bộ phân trang.",
        "name": "ariaLabel",
        "type": "string"
      },
      {
        "defaultValue": "1",
        "description": "Current page (1-indexed).",
        "name": "value",
        "type": "number"
      },
      {
        "description": "Total number of items.",
        "name": "total",
        "type": "number"
      },
      {
        "defaultValue": "10",
        "description": "Items per page.",
        "name": "pageSize",
        "type": "number"
      },
      {
        "description": "Show total count, or a custom label fn.",
        "name": "showTotal",
        "type": "boolean | (total, range) => ReactNode"
      },
      {
        "description": "Page / page-size change handler.",
        "name": "onValueChange",
        "type": "(page: number, pageSize: number) => void"
      },
      {
        "description": "Selectable page sizes shown in the size changer.",
        "name": "pageSizeOptions",
        "type": "number[]"
      },
      {
        "description": "Show the page-size selector beside the pager.",
        "name": "showSizeChanger",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Hide the control when there is nothing to page through — zero items OR exactly one page. Set false to opt in to the bar on a single page (e.g. to keep showTotal visible); total=0 is always hidden.",
        "name": "hideOnSinglePage",
        "type": "boolean"
      },
      {
        "description": "Compact form for narrow contexts — Prev / n·N / Next, no page-number buttons. The intentional mobile transformation (desktop never wraps).",
        "name": "simple",
        "type": "boolean"
      },
      {
        "description": "Ant Design `showQuickJumper` — a 'go to page' number field at the inline end of the bar. Enter (or the optional `goButton`) commits, clamped into [1, pageCount]; a non-numeric commit is ignored rather than jumping to NaN. The field is named by a real `<label htmlFor>`, not an aria-label.",
        "name": "showQuickJumper",
        "type": "boolean | { goButton?: React.ReactNode }"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Ant Design `size` (`small` → `sm`, `middle` → `md`). Implemented as ONE local `--control-height` on the bar, so the page buttons, the size-changer trigger and the quick-jumper field shrink together and cannot drift apart. `sm` reads `--control-height-sm` of the surrounding density scope (knob `--pagination-control-height-sm`, default `initial`), so a small pager in a compact DataTable matches its size=\"sm\" toolbar controls.",
        "name": "size",
        "type": "\"sm\" | \"md\""
      },
      {
        "defaultValue": "\"end\"",
        "description": "Ant Design `align`, on the logical inline axis. `end` keeps the long-standing table-footer position. With `align=\"end\"` the `showTotal` label sits BESIDE the page buttons (antd); `start` / `center` keep the total pushed to the inline start.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "defaultValue": "true",
        "description": "Ant Design `responsive`. Collapses the bar to its `simple` form below the library's single mobile breakpoint (`useIsMobile`, max-width 767px) instead of leaving a number strip wider than the phone to scroll. `simple` always wins; pass `responsive={false}` to pin the full pager at every width.",
        "name": "responsive",
        "type": "boolean"
      },
      {
        "description": "Disable all navigation controls.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Override the nav landmark's accessible name (defaults to a localized \"Pagination\"). Required when more than one Pagination renders on the same page/view — two nav landmarks sharing one name/role fail landmark-unique.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "DataTable.Pagination — use instead of standalone Pagination when the list is rendered inside a DataTable compound and uses cursor-based navigation (cursor + hasMore + onChange). DataTable.Pagination handles First/Next without page arithmetic; standalone Pagination requires a known total.",
      "InfiniteQueryState — use for infinite-scroll / load-more lists driven by useInfiniteQuery. It auto-manages skeleton, empty, and error states; Pagination is inappropriate here because there is no discrete page number.",
      "DataTable — when offset pagination is needed inside DataTable, prefer composing DataTable with a standalone Pagination below the card rather than DataTable.Pagination if the API is offset-based and returns a total count. DataTable itself does not paginate; you supply `data` for the current page.",
      "SearchInput — often placed in the same toolbar as Pagination. Resetting `value` to page 1 inside the search `onSearchChange` handler is mandatory; forgetting this is the most common bug when combining search and Pagination."
    ],
    "rules": [
      40
    ],
    "storyPath": "navigation/pagination.tsx",
    "subParts": [
      "PaginationContent",
      "PaginationEllipsis",
      "PaginationItem",
      "PaginationLink",
      "PaginationNext",
      "PaginationPrevious"
    ],
    "tagline": "Offset/page-based pagination bar. Sits below a table card. Keyboard focus uses the shared inset ring in clipping surfaces; scrollports reveal partially clipped keyboard targets without changing their layout.",
    "usage": [
      "DO always control Pagination externally: store `value` (page) and `pageSize` in React state (or URL params), and update both in the `onValueChange(page, pageSize)` callback. Pagination is fully controlled — it has no internal state and will not move unless `value` changes.",
      "DO let Pagination hide itself for zero items and single pages (`hideOnSinglePage`, default true) — it is navigation between multiple result pages. Render it inside a table footer only in the DATA state: never during loading, empty, error, or an unmet prerequisite. Pass `hideOnSinglePage={false}` only when you still want the bar on one page to keep `showTotal` visible.",
      "DO trust Pagination to stay ONE horizontal row on desktop (it never wraps). For genuinely narrow viewports use `simple` for the intentional compact transformation rather than letting controls wrap.",
      "DO pass `total` as the raw item count (not page count). The component computes `Math.ceil(total / pageSize)` internally; passing a pre-computed page count as `total` will over-paginate.",
      "DO use `showSizeChanger` together with `pageSizeOptions` when the user needs density control (default options are [10, 20, 50, 100]). When `showSizeChanger` is omitted the page-size Select is not rendered at all — do NOT hand-roll your own Select beside Pagination.",
      "DO use `simple` mode for compact contexts (mobile, sidebars, sheet footers) — it renders Prev / `n / total` / Next with no page-number buttons. Use the full form for primary admin list pages.",
      "DO use `showTotal` to surface item counts: pass `true` for the built-in i18n label, or a function `(total, [from, to]) => ReactNode` for a custom range label like '1–10 of 342 invoices'. Never hard-code a total string beside the component.",
      "DON'T use Pagination for cursor- or infinite-scroll-based lists. Pagination is strictly offset/page-based (`value` is a page number). For cursor pagination inside a DataTable use `DataTable.Pagination`; for infinite scroll use `InfiniteQueryState`.",
      "NOTE the page strip scrolls horizontally rather than wrapping, and its page buttons are normally the keyboard route to that overflow. Disable the whole bar (`disabled`) and there is no such route, so the strip takes `tabindex=0` itself to stay keyboard-scrollable (WCAG 2.1.1). Nothing to configure — just don't strip the attribute in consumer CSS/JS."
    ],
    "useCases": [
      "Server-paged DataTable: do NOT place a Pagination below the card — pass `pagination={{ total, current, pageSize, onChange }}` to DataTable and it renders this component in its own footer (total + page numbers, density-sized).",
      "Standalone offset-paginated admin list pages (e.g. invoice list, customer list, transaction history) rendered outside DataTable — place Pagination below the table card, outside the card border, with `showTotal` and optionally `showSizeChanger`.",
      "Search results pages where the backend accepts `page` + `per_page` query parameters and returns a total count — wire `value` and `pageSize` to URL search params so the URL is shareable and browser-back works.",
      "Reports and filtered data grids where the user needs to export 'all selected pages': `showTotal` with a custom function lets you show '1–50 of 1 200 rows' so the user understands the scope before exporting.",
      "Compact modal or sheet footers with a long list (e.g. selecting from a product catalog inside a dialog) — use `simple` mode to save horizontal space while keeping navigation accessible.",
      "DataTable instances where the server returns an offset-based total and `DataTable.Pagination` is not being used: attach a standalone Pagination below the card and pass the same `page` / `pageSize` state to both the DataTable `data` prop and the API fetch."
    ]
  },
  {
    "example": "import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from \"@godxjp/ui/navigation\";\nimport { Button } from \"@godxjp/ui/general\";\n\n<DropdownMenu>\n  <DropdownMenuTrigger asChild><Button variant=\"outline\" size=\"sm\">操作</Button></DropdownMenuTrigger>\n  <DropdownMenuContent>\n    <DropdownMenuItem>編集</DropdownMenuItem>\n    <DropdownMenuSeparator />\n    <DropdownMenuItem variant=\"destructive\">削除</DropdownMenuItem>\n  </DropdownMenuContent>\n</DropdownMenu>\n\n// Right-click menu — what replaced the ContextMenu component (v23). Opens at the pointer,\n// suppresses the browser menu, and opens from Shift+F10 for keyboard users.\n<DropdownMenu trigger={[\"contextMenu\"]}>\n  <DropdownMenuTrigger>行 JE-0042</DropdownMenuTrigger>\n  <DropdownMenuContent>\n    <DropdownMenuItem>編集</DropdownMenuItem>\n    <DropdownMenuItem variant=\"destructive\">削除</DropdownMenuItem>\n  </DropdownMenuContent>\n</DropdownMenu>",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "DropdownMenu",
    "props": [
      {
        "description": "On DropdownMenuContent: match the trigger, fit content, or use a token width. Omit to keep the default minimum.",
        "name": "width",
        "type": "\"trigger\" | \"auto\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Controlled open state (Ant Design `open`).",
        "name": "open",
        "type": "boolean"
      },
      {
        "defaultValue": "[\"click\"]",
        "description": "On the ROOT. Ant Design `trigger` — the gestures that open the menu, and more than one may be live at once. `contextMenu` opens AT the pointer and suppresses the browser's own menu; it is the replacement for the ContextMenu component deleted in v23, and antd expresses right-click menus the same way. The default is `[\"click\"]`, not antd's `[\"hover\"]`. The keyboard opener survives every mode: Enter/Space/ArrowDown for click and hover, Shift+F10 or the ContextMenu key for contextMenu — so a gesture list can never produce a pointer-only menu (WCAG 2.1.1).",
        "name": "trigger",
        "type": "(\"click\" | \"hover\" | \"contextMenu\")[]"
      },
      {
        "description": "On the ROOT. Ant Design `disabled` — no gesture opens the menu (antd implements it by emptying the trigger list). The trigger's own `disabled` still works for the button case.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "0.15",
        "description": "On the ROOT, in SECONDS (antd's unit). Hover dwell before a `trigger={[\"hover\"]}` menu opens.",
        "name": "mouseEnterDelay",
        "type": "number"
      },
      {
        "defaultValue": "0.1",
        "description": "On the ROOT, in SECONDS. Grace period after the pointer leaves before a hover menu closes — it is what lets the reader cross the gap from the trigger onto the menu, so do not set it to 0.",
        "name": "mouseLeaveDelay",
        "type": "number"
      },
      {
        "description": "On the ROOT. A page-level key that OPENS the menu — `\"f\"` for a filter menu, `\"mod+shift+v\"` for a view menu (gh#1264). Same combo syntax and guard as SearchInput/CommandPalette: a bare key is ignored while focus is in an editable element or an IME is composing. Off by default; ignored while `disabled`. The trigger gets `aria-keyshortcuts` automatically (an explicit consumer value wins); show the key visibly too (e.g. `DropdownMenuShortcut`). Never fires from inside a dialog, menu or listbox that does not contain the menu (gh#1267).",
        "name": "shortcut",
        "type": "string"
      },
      {
        "description": "Open-state change handler (Ant Design `onOpenChange`).",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "On DropdownMenuContent. Ant Design `placement`, spelled on the LOGICAL inline axis (antd's `bottomLeft` is `bottomStart` here), so an RTL app anchors on the correct edge with no second value. It is sugar over Radix's `side` + `align`, and an explicitly passed `side`/`align` still wins. antd's inline-side placements (`left*`/`right*`) are deliberately absent — Radix's `side` is physical and this library ships no DirectionProvider, so they would open on the wrong edge in RTL; pass Radix's own `side` if you knowingly want a physical one.",
        "name": "placement",
        "type": "\"top\" | \"topStart\" | \"topEnd\" | \"bottom\" | \"bottomStart\" | \"bottomEnd\""
      },
      {
        "description": "On DropdownMenuContent. Ant Design `arrow` — paints the pointer at the anchored edge (default off). Sized from --dropdown-arrow-{width,height,background} rather than Radix's built-in 10×5.",
        "name": "arrow",
        "type": "boolean"
      },
      {
        "description": "On DropdownMenuItem (gh#1246). A muted second line under the label — the two-line row, parity with `MegaMenuLinkProp.description`. Rendered as react-aria `<Text slot=\"description\">` (`data-slot=\"dropdown-menu-item-description\"`), so the row's accessible NAME stays the label and the description becomes its `aria-describedby`. A string label is handed to the collection as `textValue`, so typeahead still reaches the row by its label; pass `textValue` yourself when the label is not a plain string. Sized by --menu-item-description-{font-size,space-block}; rows without one keep the one-line height.",
        "name": "description",
        "type": "React.ReactNode"
      }
    ],
    "related": [
      "Popover — use Popover when the floating panel needs arbitrary layout (filter forms, date pickers, rich content grids). Use DropdownMenu only for a list of discrete clickable actions or toggle items; DropdownMenu has no layout flexibility beyond label/separator/group.",
      "Command — use Command (cmdk) when the list is large, needs fuzzy-search filtering, or acts as a keyboard-driven command palette. DropdownMenu has no built-in search input; once the list exceeds ~8 items or needs filtering, switch to Command (often inside a Popover).",
      "Select — use Select when the purpose is choosing a value to submit in a form field (has a name prop for native form submission, renders a hidden select for a11y). Use DropdownMenu when the purpose is triggering actions, not picking a form value.",
      "Sidebar — use Sidebar for persistent left-rail navigation. DropdownMenu is transient (opens on click, dismisses on select); Sidebar is always-visible structural navigation."
    ],
    "rules": [],
    "storyPath": "navigation/dropdown-menu.tsx",
    "subParts": [
      "DropdownMenuCheckboxItem",
      "DropdownMenuContent",
      "DropdownMenuGroup",
      "DropdownMenuItem",
      "DropdownMenuLabel",
      "DropdownMenuPortal",
      "DropdownMenuRadioGroup",
      "DropdownMenuRadioItem",
      "DropdownMenuSeparator",
      "DropdownMenuShortcut",
      "DropdownMenuSub",
      "DropdownMenuSubContent",
      "DropdownMenuSubTrigger",
      "DropdownMenuTrigger"
    ],
    "tagline": "Dropdown menu (react-aria). Compose DropdownMenu/DropdownMenuTrigger/DropdownMenuContent/DropdownMenuItem/DropdownMenuSeparator. `trigger` picks the gestures — click (default), hover, or contextMenu, which is what replaced the deleted ContextMenu component.",
    "usage": [
      "DO compose the full sub-part tree: DropdownMenu (root) → DropdownMenuTrigger (with asChild to delegate to your Button/icon) → DropdownMenuContent → DropdownMenuItem / DropdownMenuSeparator / DropdownMenuLabel / DropdownMenuGroup. Omitting any level (e.g. rendering DropdownMenuContent without DropdownMenu as ancestor) breaks Radix context and the menu will not open.",
      "DO use DropdownMenuTrigger with asChild and pass a godx-ui Button or icon Button as the child — never render a raw <button> or <div> as the trigger, and never omit asChild when the child is already a button-like element (double-button nesting breaks a11y).",
      "DO use variant='destructive' on DropdownMenuItem for irreversible actions (delete, revoke, void) — this applies the semantic destructive colour token automatically without any className override.",
      "DO use DropdownMenuSub + DropdownMenuSubTrigger + DropdownMenuSubContent for nested sub-menus (e.g. 'Export' → 'CSV', 'PDF'). The ChevronRight icon is rendered automatically by DropdownMenuSubTrigger — do not add your own.",
      "DO use DropdownMenuCheckboxItem (with checked + onCheckedChange) or DropdownMenuRadioGroup + DropdownMenuRadioItem for toggle/selection menus such as column visibility or active view. These items manage their own checked indicator — do not layer a Checkbox or RadioGroup inside a plain DropdownMenuItem.",
      "DON'T put anything but collection nodes directly inside DropdownMenuContent / DropdownMenuGroup / DropdownMenuRadioGroup — only DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuSub, or a component that returns one. A leading icon or a bare text label goes INSIDE an item. react-aria builds the menu through a stand-in document that cannot create SVG or text nodes, so an icon placed beside the items throws during render (`createElementNS is not a function`); the library contains that throw so the page survives, but the menu comes up EMPTY and the console says why (gh#637).",
      "DON'T use DropdownMenu for form submission — items fire onSelect callbacks, not form field values. There is no name prop for native form submission. If a menu selection must feed a form field, lift state into a controlled value and wire a hidden Input or use Select instead.",
      "DO use `trigger={[\"contextMenu\"]}` for a right-click menu on a row, card or tile — there is no ContextMenu component any more (deleted in v23). Give that target something focusable and an interactive role: the menu must be reachable with Shift+F10, and react-aria warns in dev when an `asChild` trigger has neither.",
      "DON'T put actions ONLY behind `contextMenu`. Right-click is undiscoverable for new users and absent on touch (it falls back to long-press), so mirror anything critical in a visible click trigger.",
      "DO keep `mouseLeaveDelay` non-zero on `trigger={[\"hover\"]}`: closing the instant the pointer leaves the trigger makes the menu impossible to reach, because the pointer has to cross the gap to get there."
    ],
    "useCases": [
      "Row action menu in a DataTable: a '...' icon Button opens a DropdownMenu with Edit, Duplicate, DropdownMenuSeparator, then Delete (variant='destructive') — keeps the row compact and avoids inline button clutter.",
      "Topbar / avatar chip: a user-avatar Button triggers a DropdownMenu with Profile, Settings, DropdownMenuSeparator, Sign out — standard app-shell pattern for account actions.",
      "Bulk-action toolbar: after selecting rows, an 'Actions' Button opens a DropdownMenu with Approve, Reject, Export — prevents the toolbar from overflowing with individual buttons.",
      "Column visibility toggle in a report table: a 'Columns' Button opens a DropdownMenu whose items are DropdownMenuCheckboxItem entries, letting users show/hide columns without a Dialog.",
      "Quick status change on an accounting entry: a Badge-like trigger opens a DropdownMenu with DropdownMenuRadioGroup items (Draft, Posted, Voided) so the user can transition status without navigating away.",
      "Right-click actions on a DataTable row or a file tile: `trigger={[\"click\", \"contextMenu\"]}` puts the same menu behind the row's kebab AND a right-click anywhere on the row, so the accelerator and the discoverable affordance stay one menu.",
      "Hover-opened menu on a toolbar entry (`trigger={[\"hover\"]}`) where the pointer is already travelling — the keyboard still opens it with Enter."
    ]
  },
  {
    "example": "import { Steps } from \"@godxjp/ui/navigation\";\n\n<Steps value={1} items={[{ title: \"申請\" }, { title: \"審査中\" }, { title: \"完了\" }]} />",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "Steps",
    "props": [
      {
        "description": "Array of { title, subtitle?, description?, icon?, status? }.",
        "name": "items",
        "type": "StepItemProp[]"
      },
      {
        "defaultValue": "0",
        "description": "Active step index (0-based).",
        "name": "value",
        "type": "number"
      },
      {
        "defaultValue": "0",
        "description": "Base offset for the first rendered step index.",
        "name": "defaultValue",
        "type": "number"
      },
      {
        "defaultValue": "\"horizontal\"",
        "description": "Layout direction.",
        "name": "orientation",
        "type": "\"horizontal\" | \"vertical\""
      },
      {
        "description": "Status of the CURRENT step (drives the active step colour).",
        "name": "status",
        "type": "\"wait\" | \"process\" | \"finish\" | \"error\""
      },
      {
        "description": "Render full markers, compact dots, a numbered inline auth progress row, or Ant Design's `navigation` bar — slab steps with a chevron between them (the default hairline connector is switched off there so it is not drawn through the glyph). `dot` IS antd's `progressDot`; antd 6.6.2 deprecates that prop in favour of exactly this value.",
        "name": "type",
        "type": "\"default\" | \"dot\" | \"inline\" | \"navigation\""
      },
      {
        "description": "Ant Design `percent` — completion of the CURRENT (`process`) step only, 0–100 (out-of-range values are clamped). Draws a determinate arc around that step's marker and exposes it as a real `progressbar` with aria-valuenow/min/max. Ignored by `type=\"inline\"`, which has no marker to draw into.",
        "name": "percent",
        "type": "number"
      },
      {
        "description": "Step size.",
        "name": "size",
        "type": "\"md\" | \"sm\""
      },
      {
        "description": "Lay step titles beside or below the step icons.",
        "name": "titlePlacement",
        "type": "\"horizontal\" | \"vertical\""
      },
      {
        "defaultValue": "\"chevron\"",
        "description": "`chevron` (›) is the breadcrumb-flavoured original; `arrow` (→) is the canonical hosted-identity progression marker — a chevron reads \"drill into\", an arrow reads \"then\", which is what a step row means. Both flip under dir=\"rtl\". Ignored by every other `type`.",
        "name": "separator",
        "type": "\"chevron\" | \"arrow\""
      },
      {
        "description": "Fires with the clicked step index (0-based).",
        "name": "onValueChange",
        "type": "(value: number) => void"
      }
    ],
    "related": [
      "Timeline — use Timeline (from @godxjp/ui) when you need a chronological event log with timestamps and variable content per entry; use Steps when the number of stages is fixed and forward-progress is the semantic.",
      "Tabs / Tabs — use Tabs when each section has its own rendered panel and users switch freely between them; use Steps when stages are ordered and the indicator communicates completion state rather than just selection.",
      "Progress — use Progress for a single continuous percentage (file upload, quota fill); use Steps for discrete named stages with individual pass/fail status.",
      "Breadcrumb — use Breadcrumb for hierarchical location within a page tree; use Steps for sequential workflow progress where order and completion matter."
    ],
    "rules": [],
    "storyPath": "navigation/steps.tsx",
    "tagline": "Multi-step progress indicator — horizontal or vertical, default or dot style.",
    "usage": [
      "DO: Pass all steps via the `items` array (each `{ title, subtitle?, description?, icon?, status?, disabled? }`) — Steps is a single-component API with no child sub-components to compose manually.",
      "DO: Control the active step with `value` (0-based index). For async operations, set the top-level `status` prop (`'process'|'error'|'finish'`) to override the current step's icon — e.g. `status='error'` turns the active step red without touching `items`.",
      "DO: Use per-item `status` to pin individual steps independently of `value` (e.g. a skipped or already-errored step). Per-item `status` takes precedence over the derived status from `value`.",
      "DO: Use type='inline' for compact hosted-auth/device progress. It retains process/finish/error/wait and aria-current semantics without the tall icon rail; never rebuild the row from Text and arrows in a consumer.",
      "DO: Pick the inline progression marker with `separator` (`\"arrow\"` for the canonical hosted-identity row, `\"chevron\"` for the default), and express the step-number emphasis through `--steps-inline-index-{color,font-weight}` + `--steps-inline-separator-color` in the service theme. The canonical treatment is an accent TINT at normal weight; the library default is bold at the inherited colour. Both are one token away — never fork `.ui-steps-inline-index` in app CSS.",
      "DON'T: Use Steps for navigation that needs URL routing or tab-switching — it has no built-in panel rendering. Pair it with your own conditional panel or a `Tabs`/`Tabs` body; Steps only renders the indicator bar.",
      "DON'T: Wire `onValueChange` unless you actually support non-linear navigation. `onValueChange` makes every non-disabled step clickable (rendered as `<button>`); omitting it makes all steps non-interactive (`cursor-default`). Never set `disabled` on an item without also providing `onValueChange`, or the prop is meaningless.",
      "A11y: The `<ol>` is given `aria-label='Progress'` automatically. Individual steps render as `<button type='button'>` when `onValueChange` is present — ensure each `item.title` is descriptive enough to serve as the button label; avoid icon-only steps without a visible title.",
      "DO: Put a `data-testid` (or an `id`, or any other DOM attribute) straight on `<Steps>` — the rest of an `<ol>`'s attributes ride through to the list element. NEVER bind a test to `.ui-steps-list` or to a `[data-status]` on an item: those are internal names this package renames without notice, and a suite that binds to them breaks on an upgrade that changed nothing about the component's contract. A caller-supplied `aria-label` also wins over the localized default.",
      "NOTE: `type` on Steps is the MARKER APPEARANCE (`default` | `dot` | `inline` | `navigation`), not an `<ol>`'s numbering style — the native attribute of that name is deliberately not forwarded."
    ],
    "useCases": [
      "Multi-step form wizard (entity onboarding, invoice creation): render Steps above a form, drive `current` from local state, advance on validated submit — use `status='error'` on the current step when server validation fails.",
      "Async background job tracker: display steps for a long-running import/export pipeline; poll job status and map job phases to `StepStatusProp` values (`'process'` with spinner for in-flight, `'finish'` for done, `'error'` for failed).",
      "Document approval workflow (accounting, contracts): map approval stages (Draft → Review → Approved → Archived) to `items` with per-item `status` reflecting the real state from the server — use `orientation='vertical'` for a sidebar timeline feel.",
      "Onboarding checklist sidebar: `orientation='vertical'` + `type='dot'` + `size='sm'` for a compact sidebar progress guide alongside a multi-section settings page.",
      "Non-linear step navigation (e.g. revisit a previous step to correct data): provide `onValueChange` and leave only future steps `disabled`; past and current steps become clickable buttons."
    ]
  },
  {
    "docPath": "navigation/toolbar",
    "example": "import { Toolbar, ToolbarGroup } from \"@godxjp/ui/navigation\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\n\n<Toolbar sticky hasActiveFilters={hasFilters} onClear={clearAll}>\n  <SearchInput placeholder=\"氏名・メールで検索\" value={q} onSearch={setQ} />\n  <ToolbarGroup label=\"ステータス\">\n    <Select value={status} onValueChange={setStatus}>\n      <SelectTrigger aria-label=\"ステータス\"><SelectValue /></SelectTrigger>\n      <SelectContent>\n        <SelectItem value=\"all\">すべて</SelectItem>\n        <SelectItem value=\"active\">有効</SelectItem>\n      </SelectContent>\n    </Select>\n  </ToolbarGroup>\n</Toolbar>",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "Toolbar",
    "props": [
      {
        "description": "Filter controls. Place SearchInput directly; wrap each labelled Select/DatePicker in a ToolbarGroup.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Clear-all handler. When provided AND hasActiveFilters is true, Toolbar renders the trailing 'Clear filters' button.",
        "name": "onClear",
        "type": "() => void"
      },
      {
        "defaultValue": "true",
        "description": "Whether any filter is applied — gates the clear-all button visibility.",
        "name": "hasActiveFilters",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Pin the strip to the top of its scroll container while the list scrolls beneath it. Opt-in; tune offset/fill via the --filter-bar-sticky-offset / --filter-bar-sticky-background theme knobs.",
        "name": "sticky",
        "type": "boolean"
      },
      {
        "description": "Typed model — canonical search slot rendered FIRST in the strip with the token-owned consistent width (--filter-bar-search-width, full-width below 640px). Controlled triad value/defaultValue/onValueChange plus SearchInput's debounced onSearch. ANY model prop (search/filters/chips/onChipRemove/actions/resultCount/loading/disabled/error) switches the bar to the canonical model layout; with none of them the bar stays the plain children-composition toolbar unchanged.",
        "name": "search",
        "type": "FilterBarSearchProp"
      },
      {
        "description": "Typed model — labelled domain-neutral Select filters rendered after the search slot in array order. Each: { value (stable identity), label (rendered as the control's REAL <label htmlFor> — WCAG 2.5.3), options, selected/defaultSelected/onSelectedChange, placeholder, disabled }. Width is token-owned (--filter-bar-filter-width).",
        "name": "filters",
        "type": "FilterBarFilterProp[]"
      },
      {
        "description": "Typed model — applied-filter chips as pure consumer data ({ value, label, disabled }), rendered in a labelled row under the strip. Lifecycle: ADD = include the chip, REMOVE = onChipRemove(value) via each chip's × button, CLEAR-ALL = onClear. Keep label a string so the remove button's accessible name can quote it.",
        "name": "chips",
        "type": "FilterBarChipProp[]"
      },
      {
        "description": "Remove ONE chip by its value. Required for chips to render their × remove button (each gets a localized 'Remove filter: {label}' accessible name).",
        "name": "onChipRemove",
        "type": "(value: string) => void"
      },
      {
        "description": "Typed model — trailing action slot (e.g. the primary 'Add' Button) parked at the inline end of the strip, after the reset button in DOM/keyboard order.",
        "name": "actions",
        "type": "ReactNode"
      },
      {
        "description": "Typed model — localized, CLDR-pluralized result count ('{count} results' / '{count} 件の結果' / '{count} kết quả') announced politely via a role='status' line under the strip. 0 renders '0 results' as the visible empty state.",
        "name": "resultCount",
        "type": "number"
      },
      {
        "description": "Typed model — marks the strip aria-busy (and sets data-loading on the root) while results are being (re)fetched.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Typed model — disables every model-rendered control: search, filter Selects, the reset button and the chip remove buttons. Custom children stay consumer-owned.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Typed model — consumer error content announced via role='alert' in the meta line, replacing the result count while present.",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"label\"",
        "description": "How the clear-all button shows itself (gh#1264). `label` is ✕ + the localized \"Clear filters\"; `icon` is the ✕ alone — same accessible name, a tooltip with the text, a control-height square instead of ~150px — so a bar with a few active filters keeps one row.",
        "name": "clearDisplay",
        "type": "\"label\" | \"icon\""
      },
      {
        "description": "Typed model — the saved-VIEW switcher rendered ABOVE the filter strip (e.g. `<Tabs>` of Open / Mine / All), the GitHub/Linear layout (gh#1264). A view selects a whole filter set, so it is not one more filter inside the strip; do not pass view tabs as children.",
        "name": "views",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"above\"",
        "description": "Where `views` sits (gh#1267). `above` is its own row over the strip; `inline` puts it at the START of the strip's first row so an idle bar stays ONE row (with `above`, an idle bar measured 55 → 95px and one table row fewer above the fold at 1440×900).",
        "name": "viewsPlacement",
        "type": "\"above\" | \"inline\""
      },
      {
        "description": "Extra classes on the root element.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "ToolbarGroup — the labelled wrapper for each individual filter control inside a Toolbar (SearchInput is placed directly, without a group).",
      "DataTable.Toolbar — the in-table strip for column/density/bulk-action controls; Toolbar (this) is the page-level filter strip that sits ABOVE the table.",
      "SearchInput — the debounced free-text control placed as the first child of Toolbar.",
      "Badge — compose Badge + a sibling icon Button to render each active-filter chip; Badge itself is a non-interactive leaf.",
      "Sheet — holds the filters beyond the 2–4 inline ones, opened by ONE 'More filters' Button inside the bar (pattern `list-filters`, rule #51). Not a ScrollArea: a filter strip never scrolls sideways.",
      "SearchForm — the submit-driven 「検索条件フォーム」 for many conditions or an explicit 「検索」 button (rule #51 decision table); FilterBar is the live strip."
    ],
    "rules": [
      23,
      44,
      45,
      46,
      51
    ],
    "storyPath": "navigation/toolbar.tsx",
    "subParts": [
      "ToolbarGroup",
      "FilterBar",
      "FilterBarGroup"
    ],
    "tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. It WRAPS and never scrolls sideways; many filters → 2–4 inline + ONE 'More filters' Sheet (rule #51, pattern `list-filters`). `FilterBar`/`FilterBarGroup` are the same components under their alias names. Eleven layouts (inline, Filter menu + chips, More-filters Sheet, saved views, two rows, actions + sticky, faceted sidebar, compact in a card, date range, phone, docked panel) → pattern `filter-bar-layouts`.",
    "usage": [
      "DO prefer the typed model on a canonical list page — `search`/`filters`/`chips`/`onChipRemove`/`actions`/`resultCount` (+ `loading`/`disabled`/`error`) make the bar own layout, widths, chip lifecycle and keyboard order through --filter-bar-* tokens while ALL state stays consumer data. Children composition remains fully supported and unchanged when no model prop is passed.",
      "DO place Toolbar ABOVE the table Card, never inside a `CardContent flush`. Put SearchInput as a direct child (it self-labels) and wrap every other control in a ToolbarGroup with a `label`.",
      "DO drive the clear-all button with `hasActiveFilters` + `onClear` — it only renders when both are truthy. The strip collapses to a single stacked column below 640px automatically.",
      "DO set `sticky` for long list pages so the filters stay reachable while scrolling; if a topbar sits above the list, raise `--filter-bar-sticky-offset` so the strip parks below it.",
      "DO pass `controlId` on a ToolbarGroup that wraps ONE control and give that control the same `id` — the visible caption then becomes the control's real `<label htmlFor>` (WCAG 2.5.3 label-in-name). Without it the caption only names the group wrapper and the control itself is NAMELESS (axe: select-name / button-name), so you must fall back to an `aria-label` that repeats the caption.",
      "DO let the strip WRAP — cardinal rule #51 (`get_rule 51`). It stacks below 640px and wraps onto extra rows above; it never scrolls sideways, and neither may anything you wrap it in (`ScrollArea orientation=\"horizontal\"`, an `overflow-x-auto` box): the `overflow` prop and its `scroll` strategy were removed in 33.0.0 because a sideways strip hid filters off-screen and clipped the first label. When the filters are many, keep the 2–4 most-used ones inline and put the rest behind ONE 'More filters' Button (`count` = hidden filters applied) that opens a Sheet holding them as FilterBarGroup + control; every applied filter, inline or hidden, is a chip in `chips` (the chips row wraps too). Copy-paste composition: pattern `list-filters` (`get_pattern list-filters`); there is no dedicated component. Never re-implement the geometry in the page: no page-local flex/grid/width/overflow rules on a filter strip.",
      "DO keep filters PACKED at the start of the bar with its one gap. Never lay them out yourself in a Flex/div/Card/ResponsiveGrid of FormFields above the table, and never spread them with `justify=\"between\"|\"around\"|\"evenly\"` — the only far-end element of a filter bar is its `actions` slot (with the clear button). ui-audit `hand-rolled-filter-bar` flags both. For a layout other than one strip (saved views, a Filter menu with chips, a faceted sidebar, a docked side panel, a compact bar inside a table card, a date range with quick ranges, a phone Sheet), copy pattern `filter-bar-layouts` (docs page navigation/filter-bar-layouts).",
      "DON'T build active-filter chips by nesting a Button inside a Badge (invalid markup + broken focus). Render each chip as a Badge label with a SIBLING icon Button (`aria-label` = 'clear <filter>'); a ghost `size='sm'` Button clears all.",
      "DON'T hand-roll a debounced search box or a raw `<select>` — compose SearchInput and Select. Toolbar is layout + clear-all only; the controls own their own state and a11y.",
      "Prefer the typed model for a canonical list page: pass `search` + `filters` + `chips` + `resultCount` and the bar owns the layout, widths, chip lifecycle, keyboard order (search → filters → children → reset → actions → chip removes) and responsive stacking through --filter-bar-* tokens. Everything stays consumer data — the bar renders state, never owns it.",
      "Compose real controls as children when the model doesn't fit; filter state and URL synchronization remain consumer-owned. Children also render INSIDE the model layout (after the typed filters) for one-off custom controls like a date-range picker.",
      "Give each FilterBarGroup a `controlId` matching its single control's `id` so the visible caption is that control's real <label>; otherwise the control is nameless to a screen reader. Typed `filters` wire this automatically.",
      "A mobile sheet presentation is a COMPOSITION, not a prop: put the same typed FilterBar inside a Sheet triggered from a compact toolbar when a screen wants drawer-style filters. The bar's own responsive behavior is stack (below 640px) + wrap (above), token-owned."
    ],
    "useCases": [
      "Master list screens (members, organizations, subscriptions, invoices) that need free-text search plus a few dropdown filters above a DataTable.",
      "Sticky filter strip over a tall list where the filters must remain visible as the user scrolls the results.",
      "Filtered views that surface applied conditions as removable chips (clear-one via each chip's × Button, clear-all via `onClear`).",
      "Filter layout choices — saved views filter tabs above the bar, a search-first bar with one Filter menu, a faceted filter sidebar with per-facet counts, a docked filter panel beside the table, a compact table toolbar, a date range filter with quick ranges, a phone filter Sheet: pattern `filter-bar-layouts`."
    ]
  },
  {
    "absorbed": [
      "QueryFilter",
      "SearchPanel"
    ],
    "docPath": "navigation/search-form",
    "example": "import { SearchForm } from \"@godxjp/ui/navigation\";\nimport { FormField, Input, Select, DatePicker } from \"@godxjp/ui/data-entry\";\n\n<SearchForm\n  controlWidth=\"100%\"\n  onSubmit={(e) => { e.preventDefault(); setApplied(draft); }}\n  onReset={() => { setDraft(EMPTY); setApplied(EMPTY); }}\n  summary=\"国籍: ベトナム / 状態: 実習中\"\n  resultCount={rows.length}\n>\n  <FormField label=\"氏名\" id=\"q-name\"><Input id=\"q-name\" value={draft.name} onChange={(e) => set({ name: e.target.value })} /></FormField>\n  <FormField label=\"国籍\" id=\"q-nat\"><Select id=\"q-nat\" options={nationalities} value={draft.nationality} onValueChange={(v) => set({ nationality: v })} /></FormField>\n  <FormField label=\"入国日\" id=\"q-entry\"><DatePicker id=\"q-entry\" range /></FormField>\n  <FormField label=\"職種\" id=\"q-occ\"><Select id=\"q-occ\" options={occupations} value={draft.occupation} onValueChange={(v) => set({ occupation: v })} /></FormField>\n</SearchForm>",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "SearchForm",
    "props": [
      {
        "description": "One FormField per condition, in reading order. Each direct child is one grid cell; a range (「日付」 From〜To, 「金額」 「下限〜上限」) is ONE FormField holding both controls.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "defaultValue": "{ base: 1, md: 2, lg: 3 }",
        "description": "Grid columns per container breakpoint — one column on a phone, aligned columns above. Conditions never spread to the edges.",
        "name": "columns",
        "type": "ResponsiveGridColumnsProp"
      },
      {
        "description": "Conditions shown while collapsed (antd `defaultFormItemsNumber`). Default: the widest `columns` step, i.e. the first row.",
        "name": "visibleFields",
        "type": "number"
      },
      {
        "description": "Controlled: whether the conditions past `visibleFields` are shown.",
        "name": "open",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Uncontrolled initial state — collapsed to the first row by default.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Fires when the 「詳細条件を開く」 / 「閉じる」 toggle is pressed.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Search (「検索」) — the native form submit. Call `preventDefault()` and copy the draft conditions into the applied query; the form never searches on each keystroke.",
        "name": "onSubmit",
        "type": "FormEventHandler<HTMLFormElement>"
      },
      {
        "description": "Clear (「クリア」). The button renders only when this is set.",
        "name": "onReset",
        "type": "() => void"
      },
      {
        "description": "The conditions the CURRENT results were searched with, shown after the localized 「検索条件」 label in the status line (e.g. 「国「籍」: 「ベトナム」 / 「状態」: 「実習」中」).",
        "name": "summary",
        "type": "ReactNode"
      },
      {
        "description": "Localized, pluralized result count in the same `role=\"status\"` line.",
        "name": "resultCount",
        "type": "number"
      },
      {
        "description": "Puts the Search button in its loading state while the query runs.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Overrides the localized Search (「検索」) text.",
        "name": "searchLabel",
        "type": "ReactNode"
      },
      {
        "description": "Overrides the localized Clear (「クリア」) text.",
        "name": "resetLabel",
        "type": "ReactNode"
      },
      {
        "description": "Accessible name of the `search` landmark (default: localized 'Search conditions').",
        "name": "label",
        "type": "string"
      },
      {
        "description": "Passed to the underlying Form: `layout=\"vertical\"` (default) is label-top; `layout=\"horizontal\"` + `labelWidth` is the label-left table style of Japanese business systems. Set `controlWidth` once here so Selects fill their column.",
        "name": "layout / labelWidth / controlWidth / labelAlign",
        "type": "Form props"
      }
    ],
    "related": [
      "Toolbar — FilterBar, the live filter strip; use it for a few filters applied as the user changes them.",
      "Form — the layout container SearchForm renders (`role=\"search\"`); its `layout`, `labelWidth` and `controlWidth` pass through.",
      "FormField — one per condition; a range is one FormField holding two controls.",
      "DataTable — the results under the form."
    ],
    "rules": [
      23,
      51
    ],
    "storyPath": "navigation/search-form.tsx",
    "tagline": "Submit-driven 「検索条件フォーム」 (Ant Design ProComponents QueryFilter): a `search` landmark holding a grid of labelled conditions aligned in columns, Search 「検索」 + Clear 「クリア」, the conditions past the first row behind ONE 「詳細条件を開く」 toggle that names how many are hidden, and a 「検索条件」 summary + result count in a role=\"status\" line after the search. Use it instead of a live FilterBar when the conditions are many, the query is heavy, or users expect an explicit 「検索」 button (rule #51 decision table).",
    "usage": [
      "DO pick the shape from rule #51's decision table: a few filters applied instantly → FilterBar; many conditions / a heavy query / users who expect an explicit 「検索」 button → SearchForm; arbitrary field × operator conditions saved as views → pattern `condition-builder`.",
      "DO put one FormField per condition as a direct child. Ranges (「入国日」 From〜To, 「金額」 「下限〜上限」) are ONE FormField with both controls; 「部分一致」 / 「完全一致」 is a RadioGroup inside the name field.",
      "DO keep a draft and an applied copy of the conditions: inputs edit the draft, onSubmit copies it to the applied query, onReset clears both. Pass the applied conditions as `summary` and the hit count as `resultCount`.",
      "DO NOT wrap it in another Form, and do not rebuild it as a Form + ResponsiveGrid + Collapsible by hand — the collapse, the hidden-count toggle, the search landmark and the status line are what this component owns. Hidden conditions stay mounted, so a typed value survives collapsing.",
      "A SearchForm (or any `<Form role=\"search\" onSubmit>`) above a table is NOT flagged by ui-audit `form-on-list-page` or `hand-rolled-filter-bar`; a submitting create form without role=\"search\" still is."
    ],
    "useCases": [
      "「検索条件フォーム」 on a Japanese business-system list (「基幹系」 / 「業務システム」): 6–12 conditions, 「検索」 and 「クリア」 buttons, 「詳細条件を開く」.",
      "Search screens over a heavy query where searching on every keystroke would be too expensive.",
      "Label-left (「表形式」) or label-top search panels above a DataTable, with the applied conditions summarized after the search."
    ]
  },
  {
    "example": "import { AppProvider } from \"@godxjp/ui/app\";\n\n<AppProvider defaultLocale=\"ja\" defaultTimezone=\"Asia/Tokyo\" defaultDateFormat=\"iso\" defaultTimeFormat=\"24h\">\n  {children}\n</AppProvider>",
    "group": "providers",
    "importPath": "@godxjp/ui/app",
    "name": "AppProvider",
    "props": [
      {
        "description": "Initial locale when nothing is stored. Resolution, first match wins: this prop (when registered — `de-AT` is served by a registered `de`) → `<html lang>` (client only) → the preset's `defaultLocale` → `en`. Never `navigator.language` (the server cannot see it, so SSR would hydrate differently). An SSR host passes the request's locale here. v32: the default was `vi`.",
        "name": "defaultLocale",
        "type": "string (BCP-47)"
      },
      {
        "description": "Product defaults the host opts into — `import { godxPreset } from \"@godxjp/ui/themes/godx\"` (pair it with `@godxjp/ui/themes/godx.css` after the base stylesheet; a shadow root that embeds `@godxjp/ui/styles/core` takes the font-free `@godxjp/ui/themes/godx-tokens.css` instead, since `@font-face` must live in the document). Its `defaultLocale` fills only what the props leave unset — it pins NO `timeZone` since 32.4.1 (gh#1282: it pinned `Asia/Ho_Chi_Minh` and every app showed Vietnam time to users in Japan; pass `defaultTimezone` for a fixed zone), its `brandMark` is what an `AuthIdentity` with no `brand` draws, and its `name` is written to `<html data-preset>`. Omit for the neutral defaults (ink primary, `en`, no product mark).",
        "name": "preset",
        "type": "AppPreset"
      },
      {
        "defaultValue": "\"browser\"",
        "description": "Initial IANA timezone.",
        "name": "defaultTimezone",
        "type": "string | \"browser\" | \"system\""
      },
      {
        "defaultValue": "\"locale\"",
        "description": "Initial date display format.",
        "name": "defaultDateFormat",
        "type": "\"iso\" | \"ymd\" | \"dmy\" | \"mdy\" | \"locale\""
      },
      {
        "defaultValue": "\"locale\"",
        "description": "Initial clock format.",
        "name": "defaultTimeFormat",
        "type": "\"24h\" | \"12h\" | \"locale\""
      },
      {
        "defaultValue": "\"light\"",
        "description": "Theme axis → <html data-theme>. Equal alias of the legacy .dark class. \"system\" defers to prefers-color-scheme and is followed LIVE (matchMedia listener), so <html data-theme> flips with the OS; the persisted value stays \"system\" (the CHOICE), never the resolved light/dark. Persisted; change via setTheme / <AppSettingPicker kind=\"theme\"> / <Segmented>.",
        "name": "theme",
        "type": "\"light\" | \"dark\" | \"system\""
      },
      {
        "defaultValue": "null",
        "description": "Brand-palette axis → <html data-brand> (sets --primary/--ring/--accent). OPT-IN: null keeps the --primary your own theme.css defines. \"dxs\" is THE CANONICAL DXS PRESET and is more than a tint: it also binds the canonical hosted-identity surface contract (36px auth controls, 22.5rem auth card measure, 16px page inset / 15px below 30rem), so a DXS surface needs ZERO page CSS for auth geometry, density, insets, logo colour, divider or footer. Stylesheet-only apps (no provider) import \"@godxjp/ui/theme/dxs.canonical.css\" instead — same contract, guarded against drift by a test.",
        "name": "brand",
        "type": "\"brand\" | \"crm\" | \"logistics\" | \"partner\" | \"slate\" | \"dxs\" | null"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Density axis → <html data-density>. A named preset of the global --scaling factor (compact .92 / default 1 / comfortable 1.08): every size token rescales in proportion app-wide. PageContainer density= overrides locally.",
        "name": "density",
        "type": "\"compact\" | \"default\" | \"comfortable\""
      },
      {
        "defaultValue": "null",
        "description": "Continuous global size multiplier → inline --scaling on <html> (Radix model). Scales spacing, control/table/checkbox/switch heights, radius in proportion. null defers to the density preset; a number (e.g. 0.95) overrides it. Type is NOT scaled (separate fontSize axis).",
        "name": "scaling",
        "type": "number | null"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Base type-size axis → <html data-font-size>. A preset sets --font-size-base and the whole golden scale rescales. Orthogonal to --scaling.",
        "name": "fontSize",
        "type": "\"sm\" | \"default\" | \"lg\""
      },
      {
        "defaultValue": "true",
        "description": "Which viewer preferences survive a reload: `true` every axis, `false` none, or a LIST — `persist={[\"theme\", \"density\", \"fontSize\"]}`. The list exists because THE AXES DO NOT SHARE AN OWNER. theme/brand/density/fontSize/scaling are the VIEWER's and belong in this browser; locale/timezone/timeFormat/dateFormat are frequently the SERVER's, resolved per request from a cookie, an account row or a header — and a stored copy then WINS over the value the server just sent, because storage is read after the props. Given one all-or-nothing flag, that consumer sets `persist={false}` and loses the viewer's theme along with it (measured: a hosted Inertia app whose theme toggle reset on every full page load). Naming the axes keeps both owners.",
        "name": "persist",
        "type": "boolean | readonly AppPreferenceAxis[]"
      },
      {
        "description": "The languages the app stores USER CONTENT in, in order, source language first — e.g. `[\"ja\", \"en\", \"vi\"]` for a Japanese-first catalogue (gh#1347). A different fact from the UI locale (`defaultLocale`) and from the registered UI catalogs: a Japanese UI can edit content in three languages, and content may be stored in a language the UI has no catalog for. Every `TranslatableInput` / `TranslatableTextarea` without its own `locales` takes its tabs from this list and opens on its first entry; a field's `locales` / `defaultLocale` still override. Omit (or `[]`) to keep the field default (`getRegisteredLocales()`, first tab = the UI locale). App configuration: not persisted, no setter. Read it from `useAppContext().contentLocales` (`undefined` when unset).",
        "name": "contentLocales",
        "type": "readonly string[]"
      },
      {
        "defaultValue": "false",
        "description": "OFF by default and deliberately opt-in: `name` decides what a native <form> submit sends, so switching it on for every consumer of a shared package would make apps start posting new keys to their backend on a library upgrade. Turn it on for apps that need native form posts or a screen-automation (RPA) contract on their controls. The inert `data-field` companion attribute is emitted regardless; a `name` written on the control itself always wins. NOT persisted to localStorage: it is the app's configuration, not a user choice.",
        "name": "emitFieldNames",
        "type": "boolean"
      }
    ],
    "related": [
      "AppSettingPicker — `kind=\"locale\"` is the language selector that reads/writes AppProvider locale context automatically when used with no value props. Prefer it over calling setLocale from useAppContext() directly in UI.",
      "AppSettingPicker `kind=\"timezone\"` — the timezone selector; inherits `timezoneOptions` from AppProvider context. Every kind needs AppProvider in the tree unless value + onValueChange are passed.",
      "formatDate — the MANDATORY date/time formatter that reads locale, timezone, timeFormat, and dateFormat from AppProvider context. Do NOT call date-fns or Intl.DateTimeFormat directly; formatDate is the single source of truth for display.",
      "AppShell — the authenticated layout (sidebar + topbar + main). It does NOT mount AppProvider: mount AppProvider once at the application root, ABOVE AppShell (and above any CenteredShell/MobileShell), so every shell and page reads the same context.",
      "OverlayPortalProvider — the other root-level provider, and the one to reach for when this library is mounted inside a SHADOW ROOT. Every overlay here (Popover, Dialog, Sheet, Tooltip, DropdownMenu, HoverCard) portals to `document.body` by default, which is correct on an ordinary page and wrong in a shadow root: the panel lands outside the tree carrying the stylesheet and renders with none of it — measured on an embedded bar as border 0, radius 0, a transparent background, and the anchor maths 40px off. `<OverlayPortalProvider container={shadowRoot}>` moves all of them at once; a per-component prop cannot, because a component that owns its own overlay (AppLauncher) is unreachable from the outside."
    ],
    "rules": [
      5
    ],
    "storyPath": "providers/app-provider.tsx",
    "subParts": [
      "AppPresetContext"
    ],
    "tagline": "Root locale/timezone/date-time context — wrap the app ONCE. All pickers + formatDate read from it. Import from @godxjp/ui/app.",
    "usage": [
      "`AppPresetContext` is the React context AppProvider's `preset` prop fills; read it with `useAppPreset()` (the active `AppPreset`, or undefined for the neutral defaults). Don't provide it yourself — pass `preset` to AppProvider.",
      "DO drive the four theme axes (theme / brand / density / fontSize) from AppProvider props ONLY — they are written to <html data-*> and read by every component via tokens. Never hand-set --font-size-base or .ui-density-* in app CSS; that bypasses persistence + the runtime switchers. For runtime switching mount `<AppSettingPicker kind=\"density\" | \"fontSize\" | \"theme\" | \"brand\" >` or call setDensity/setFontSize/setTheme/setBrand from useAppContext().",
      "DO mount AppProvider ONCE at the application root (e.g. in app.tsx or the Inertia layout), wrapping ALL children — every `AppSettingPicker` (kind=\"locale\" / \"timezone\" / \"dateFormat\" / \"timeFormat\"), every formatDate call, and the Toaster all rely on the single context it provides. Nesting two AppProviders creates split contexts; inner pickers silently read the wrong one.",
      "DO NOT omit AppProvider and then try to use AppSettingPicker or formatDate standalone — useAppContext() throws 'useAppContext must be used within <AppProvider>' at runtime. The only exception is using those pickers in fully controlled mode (value + onChange) which reads useOptionalAppContext() and returns null safely.",
      "DO use `persist={false}` on AppProvider for isolated tests and standalone settings forms where localStorage must not be read or written. With the default `persist={true}` the provider reads localStorage key `godxjp.app` on mount (after first render), so initial state may differ between SSR and client.",
      "DO NOT reach for `persist={false}` because the SERVER owns the locale — pass the axes the BROWSER owns instead: `persist={[\"theme\", \"brand\", \"density\", \"fontSize\", \"scaling\"]}`. Storage is read after the props, so a stored `locale` overrides the one the server just resolved; turning persistence off wholesale fixes that and silently takes the viewer's theme with it, which is how a hosted app shipped a theme toggle that reset on every reload.",
      "DO set `defaultTimezone='system'` together with `systemTimezone={serverTimezone}` when your backend knows the legal entity's canonical timezone (e.g. 'Asia/Ho_Chi_Minh'). Use `defaultTimezone='browser'` (the default) only when you want the user's browser clock. Do NOT pass a raw IANA string to `defaultTimezone` if the user may be in a different zone — use the named aliases.",
      "DO wire `onLocaleChange`, `onTimezoneChange`, `onTimeFormatChange`, `onDateFormatChange` to persist changes server-side (e.g. patch user profile via Inertia router) in addition to the automatic localStorage write. These callbacks fire after state is set, so the new value is already reflected in context.",
      "DO set `emitFieldNames` on AppProvider when the app is driven by screen automation (RPA) or posts native forms — every control under a FormField then carries a real `name` taken from the field key, and legacy automation that addressed controls by `name` keeps working after a rewrite. Leave it off (the default) otherwise: it changes what a native submit sends. The `data-field` attribute is emitted either way, so e2e selectors do not depend on this flag.",
      "DO pass `contentLocales` when the app edits translatable content (`TranslatableInput` / `TranslatableTextarea`) — `contentLocales={[\"ja\", \"en\", \"vi\"]}` — so the tab order and the source-language first tab are stated once, not repeated as `locales` on every field. It is NOT the UI language list: leave `defaultLocale` / `registerLocale` to the UI.",
      "DO restrict the timezone dropdown by passing `timezoneOptions={APP_TIMEZONE_PRESET}` (an exported constant) to AppProvider — every `AppSettingPicker kind=\"timezone\"` inherits this restricted list from context. Without it the picker offers the full IANA list (~600 entries)."
    ],
    "useCases": [
      "App bootstrap in a multi-locale SaaS admin (ja/en/vi) — mount AppProvider at the root with the tenant's preferred locale and IANA timezone so every DataTable date column, every formatDate call, and every picker renders consistently in the user's locale without any per-component configuration.",
      "User settings page — render `<AppSettingPicker kind=\"locale\" />`, `kind=\"timezone\"`, `kind=\"dateFormat\"` and `kind=\"timeFormat\"` inside the existing AppProvider; each reads and writes context automatically. Wire `onLocaleChange` to an Inertia form submit to persist the change to the server profile.",
      "Server-rendered Inertia app with SSR hydration — pass `defaultTimezone='system'` and `systemTimezone={sharedProps.timezone}` (injected via HandleInertiaRequests) so the initial render is timezone-deterministic and avoids hydration mismatches caused by browser-timezone detection.",
      "Multi-entity accounting dashboard — use `timezoneOptions` to restrict the picker to the legal entity's permissible zones (e.g. Southeast Asian IANA ids only), preventing users from accidentally switching to an out-of-scope timezone that would misrepresent transaction timestamps.",
      "Isolated preview / Storybook story — wrap a single component in `<AppProvider persist={false} defaultLocale='en'>` to give it a stable context without polluting localStorage between stories.",
      "Test harness — wrap the component under test in `<AppProvider persist={false} defaultLocale='ja' defaultDateFormat='iso'>` to assert locale-sensitive formatting output deterministically, independent of whatever the browser or stored preferences report."
    ]
  },
  {
    "example": "import { OverlayPortalProvider } from \"@godxjp/ui/app\";\n\n// The React root lives inside a shadow root, so every overlay must portal there too.\nconst shadowRoot = host.attachShadow({ mode: \"open\" });\n\n<OverlayPortalProvider container={shadowRoot}>\n  <AppProvider defaultLocale=\"ja\">{children}</AppProvider>\n</OverlayPortalProvider>",
    "group": "providers",
    "importPath": "@godxjp/ui/app",
    "name": "OverlayPortalProvider",
    "props": [
      {
        "description": "The element overlays render into — pass the shadow root, or any element inside it. `undefined` restores the document.body default, so a subtree can opt back out.",
        "name": "container",
        "required": true,
        "type": "Element | undefined"
      },
      {
        "description": "The subtree whose overlays are relocated.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      }
    ],
    "related": [
      "AppProvider — the other root-level provider (locale, timezone, theme axes). Both are mounted once at the root; neither replaces the other.",
      "Popover — one of the overlays this provider relocates; its own `container` has no prop equivalent.",
      "Dialog — likewise portals to document.body unless this provider names another container."
    ],
    "rules": [
      5
    ],
    "tagline": "Moves EVERY overlay in this library (Popover, Dialog, Sheet, Tooltip, DropdownMenu, HoverCard, Select, Cascader) into a container you name — the one thing an app mounted inside a SHADOW ROOT cannot do with props.",
    "usage": [
      "DO mount it when this library is rendered inside a shadow root (an embedded bar, a widget injected into a host page, a Web Component wrapper). Overlays default to document.body, which is outside the tree carrying this library's stylesheet — measured on an embedded bar as border 0, radius 0, a transparent background and the anchor maths 40px off.",
      "DO wrap the whole embedded subtree ONCE, the same way AppProvider is mounted once. Where overlays render is a fact about the tree, not about any one control, and a component that owns its own overlay (AppLauncher) has no prop a consumer could reach it through.",
      "DO expect it to fix DISMISSAL and FOCUS too, not just paint: mounting it switches react-aria onto its shadow-DOM code paths, without which the click that opened an overlay reads as a click outside it (an embedded launchpad that opened and shut on one press) and a focus scope believes focus never entered the dialog.",
      "DON'T mount it on an ordinary page. document.body is the right destination there, and the provider is not a styling knob — it changes nothing you would want changed outside a shadow root.",
      "DON'T look for a per-overlay container prop instead. There is none by design; a consumer asked to remember six of them gets five right."
    ],
    "useCases": [
      "A GoDX bar or launchpad injected into a third-party page inside a shadow root, where its Popover and DropdownMenu must stay styled and dismissable.",
      "A Web Component wrapper around an app built with this library, mounting the React tree in its own shadow DOM.",
      "A widget embedded in a host that happens to load this library too — the case where the default is not obviously broken, only subtly wrong, until the first host that does not load it."
    ]
  },
  {
    "example": "import { ThemeScope, tenantTheme } from \"@godxjp/ui/app\";\n\n// The customer's colour on one region — and on every overlay that region opens.\n<ThemeScope style={tenantTheme(customer.brandHex).vars} data-tenant={customer.slug}>\n  <Dialog>\n    <DialogTrigger asChild><Button>Review</Button></DialogTrigger>\n    <DialogContent><DialogTitle>Review</DialogTitle></DialogContent>\n  </Dialog>\n</ThemeScope>",
    "group": "providers",
    "importPath": "@godxjp/ui/app",
    "name": "ThemeScope",
    "props": [
      {
        "description": "The themed region. Overlays opened anywhere below it follow its tokens.",
        "name": "children",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Classes on the scope element — this is where `dark` goes when a REGION is dark rather than the whole page.",
        "name": "className",
        "required": false,
        "type": "string"
      },
      {
        "description": "DOM id of the scope element.",
        "name": "id",
        "required": false,
        "type": "string"
      }
    ],
    "related": [
      "OverlayPortalProvider — the other half of the same statement: it decides WHERE an overlay lands, ThemeScope decides which TOKENS it inherits there. They compose; a ThemeScope inside one hosts itself in that container.",
      "tenantTheme — the function that turns a customer hex into the declarations you put ON a ThemeScope (`style={tenantTheme(hex).vars}`). It computes the colours; ThemeScope is what carries them past the portal boundary.",
      "AppProvider — page-level theme axes (theme/brand/density/fontSize). Use it for the whole app; ThemeScope is for one region that differs from it."
    ],
    "rules": [
      5
    ],
    "storyPath": "providers/theme-scope.tsx",
    "tagline": "Makes a themed REGION reach the overlays it opens. Every overlay portals to document.body, so custom-property inheritance stops at the portal boundary and a tenant-themed region's Dialog, Select listbox, Popover and Toast paint the package defaults. Wrap the region in ThemeScope and they carry the region's tokens.",
    "usage": [
      "DO wrap the region, then theme it the way you already do — `style={tenantTheme(hex).vars}`, `data-tenant=\"acme\"`, `className=\"dark\"`, or a stylesheet rule that never mentions React. ThemeScope reads the COMPUTED tokens at its own element, so all of those paths behave identically; it has no theme prop and needs none.",
      "DO nest it. An inner ThemeScope inside an outer one wins for the overlays opened below it, because its scope already inherits the outer's tokens and it diffs against the document root.",
      "DO mount it inside an OverlayPortalProvider when you have one. It puts its host INSIDE that container, so a shadow-rooted app can be tenant-themed as well — the two providers compose rather than compete.",
      "DON'T reach for `OverlayPortalProvider container={themedWrapper}` to solve this. It works until the wrapper sits inside an `overflow: hidden`, a `transform` or a `contain` ancestor, and then the region clips its own overlays — a colour bug traded for a layout bug that is harder to see. `container` stays for the shadow-DOM case it was built for.",
      "DON'T expect it on a page with no scoped theme. With nothing themed it carries nothing, and with no ThemeScope at all every overlay behaves exactly as before.",
      "DON'T assume a CLASS-keyed rule travels. What crosses the boundary is the custom-property delta — the tokens. A rule written as `.dark .my-thing { background: #111 }` in app CSS is not a token and does not follow the overlay."
    ],
    "useCases": [
      "A multi-tenant screen where one region wears a customer's brand colour from `tenantTheme(hex)` — the button was already right, and this is what makes the dialog it opens right too.",
      "A dark region on a light page (a preview pane, an editor canvas): `className=\"dark\"` on the region, and its Select listbox and Popover stay dark instead of flashing the page's light popover surface.",
      "A `[data-tenant]` theme written entirely in the consumer's own stylesheet, with no React theming provider anywhere — the documented way to theme a region in this package, and the case the design was chosen to cover."
    ]
  },
  {
    "example": "import { formatDate } from \"@godxjp/ui/datetime\";\n\nformatDate(coupon.validFrom);                       // \"2026-05-01\"\nformatDate(order.createdAt, { kind: \"relative\" });  // \"3日前\"",
    "group": "providers",
    "importPath": "@godxjp/ui/datetime",
    "name": "formatDate",
    "props": [
      {
        "description": "ISO date, ISO datetime, HH:mm, or Date.",
        "name": "value",
        "required": true,
        "type": "string | Date | null | undefined"
      },
      {
        "defaultValue": "\"auto\"",
        "description": "Output preset; auto infers from the value.",
        "name": "options.kind",
        "type": "\"auto\" | \"date\" | \"datetime\" | \"time\" | \"long\" | \"relative\""
      }
    ],
    "related": [
      "AppProvider — required peer that seeds locale, timezone, dateFormat, and timeFormat into the module-level context that `formatDate` reads. Must be mounted once at app root; omitting it means `formatDate` silently falls back to Vietnamese/Ho Chi Minh City defaults.",
      "DatePicker — the corresponding input control for calendar dates. Use `DatePicker` to capture a date from the user; use `formatDate(value, { calendar: true })` to display the picked `Date` object back as a string.",
      "AppSettingPicker — `kind=\"dateFormat\" | \"timeFormat\" | \"timezone\"` updates AppProvider context, and subsequent `formatDate` calls pick the selection up with no extra wiring.",
      "TimePicker — the corresponding input control for HH:mm time values. Use `TimePicker` to capture time; use `formatDate(hhmm)` (auto-detects `'time'` kind) to display the stored `HH:mm` string respecting the user's 12h/24h preference."
    ],
    "rules": [
      5
    ],
    "storyPath": "providers/format-date.tsx",
    "tagline": "MANDATORY for all date/time display. Auto-detects ISO date / HH:mm / instant; reads AppProvider context. Import from @godxjp/ui/datetime.",
    "usage": [
      "DO import from `@godxjp/ui/datetime` — NOT from `date-fns` or any other datetime utility. `formatDate` is the single mandatory display entry point; calling `date-fns/format` directly bypasses AppProvider locale/timezone/dateFormat/timeFormat context and produces inconsistent output across the app.",
      "DO ensure `AppProvider` is mounted at the app root before the first `formatDate` call. The function reads a module-level context synced by `AppProvider` via `syncDatetimeContext`. Without it the fallback locale is `'vi'` / timezone `'Asia/Ho_Chi_Minh'` / `'24h'`, which will silently produce wrong output in Japanese or English apps.",
      "DO pass `null` or `undefined` safely — `formatDate` returns an em-dash `'—'` for null/undefined/empty string values. Never guard with a ternary before calling it.",
      "DO use `options.kind` when auto-detection is wrong: pass `kind: 'date'` for an ISO datetime string you want displayed as date-only, `kind: 'relative'` for age display (e.g. `'3日前'`), `kind: 'long'` for full PPP format in modals/detail panels. Auto-detection maps plain `yyyy-MM-dd` → `'date'`, `HH:mm` → `'time'`, everything else → `'datetime'`.",
      "DO pass `{ calendar: true }` when the `Date` object came from a react-day-picker calendar pick — this prevents timezone shift that would occur if the Date were treated as a UTC instant.",
      "DON'T hand-roll per-call locale/timezone resolution with `Intl.DateTimeFormat` or raw `date-fns/format`. The `options.locale` / `options.timezone` overrides exist for one-off per-cell display differences (e.g. showing a partner's local time), not as a substitute for AppProvider context."
    ],
    "useCases": [
      "Rendering all date/time columns in a DataTable — invoice due dates (`kind: 'date'`), transaction timestamps (`kind: 'datetime'`), and elapsed time since last sync (`kind: 'relative'`) all go through `formatDate` so the locale/timezone/12h-24h setting from AppProvider is respected everywhere.",
      "Displaying a single date/time field in a Descriptions or Card detail panel, e.g. `formatDate(invoice.issuedAt)` for the issued-at row — no extra formatting logic needed, null is handled as `'—'` automatically.",
      "Formatting a stored `HH:mm` string (24h canonical storage) for display according to the user's timeFormat preference — pass the raw `'14:30'` string and auto-detection routes it through `formatTimeOfDay`, outputting `'2:30 PM'` or `'14:30'` based on context.",
      "Rendering a 'last modified' timestamp with relative wording in an activity feed or audit log row — `formatDate(entry.updatedAt, { kind: 'relative' })` produces locale-correct relative strings like `'3日前'` / `'3 days ago'`.",
      "Converting a `Date` selected from `DatePicker` (react-day-picker) back to a display string — pass `{ calendar: true }` to avoid the UTC midnight shift that the default instant path would apply."
    ]
  },
  {
    "example": "import { FormField, TimePicker } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { useState } from \"react\";\n\n// Controlled usage inside a React form\nexport function ShiftStartField() {\n  const [startTime, setStartTime] = useState(\"09:00\");\n\n  return (\n    <FormField id=\"shift-start\" label=\"Shift start\" controlWidth=\"9rem\">\n      <TimePicker\n        id=\"shift-start\"\n        name=\"shift_start\"\n        value={startTime}\n        onValueChange={setStartTime}\n        minuteStep={15}\n      />\n    </FormField>\n  );\n}\n\n// Uncontrolled usage inside a native form\nexport function CutoffTimeForm() {\n  return (\n    <form method=\"post\" action=\"/settings/cutoff\">\n      <TimePicker\n        id=\"cutoff\"\n        name=\"cutoff_time\"\n        defaultValue=\"17:00\"\n        minuteStep={30}\n        placeholder=\"hh:mm\"\n      />\n      <Button type=\"submit\">Save</Button>\n    </form>\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "TimePicker",
    "props": [
      {
        "description": "Opt-in wheel selection, skipping disabled entries without closing the panel. Defaults off.",
        "name": "changeOnScroll",
        "type": "boolean"
      },
      {
        "description": "Controlled panel visibility.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Initial panel visibility.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Panel visibility changes.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Validation appearance; error announces invalid state.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "Shared control surface.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "Shared control sizing.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Disable typing while preserving panel selection.",
        "name": "inputReadOnly",
        "type": "boolean"
      },
      {
        "description": "Keep invalid draft text on blur; never submit it as a committed value.",
        "name": "preserveInvalidOnBlur",
        "type": "boolean"
      },
      {
        "description": "Logical popup placement.",
        "name": "placement",
        "type": "\"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\""
      },
      {
        "description": "Additional panel footer content.",
        "name": "renderExtraFooter",
        "type": "() => ReactNode"
      },
      {
        "description": "Ref to the editable input (range start).",
        "name": "ref",
        "type": "Ref<HTMLInputElement>"
      },
      {
        "description": "Hour column step, default 1.",
        "name": "hourStep",
        "type": "number"
      },
      {
        "description": "Second column step, default 1.",
        "name": "secondStep",
        "type": "number"
      },
      {
        "description": "Enable seconds in the panel and canonical value.",
        "name": "showSeconds",
        "type": "boolean"
      },
      {
        "description": "Override the locale hour cycle.",
        "name": "use12Hours",
        "type": "boolean"
      },
      {
        "description": "HH:mm, HH:mm:ss, h:mm A or date-fns a. Canonical value stays HH:mm or HH:mm:ss.",
        "name": "format",
        "type": "string"
      },
      {
        "description": "Forbid individual hours, minutes and seconds. Without it a 開始/終了 pair has no way to stop the end time being set before the start — the columns will happily offer it. Applies to BOTH routes into the value: a disabled option cannot be clicked, is skipped by the arrow keys, and a forbidden time typed into the field is rejected.",
        "name": "disabledTime",
        "type": "TimePickerDisabledTimeProp"
      },
      {
        "defaultValue": "false",
        "description": "Drop disabled options from the columns instead of greying them out (antd's default too). Greyed-out is usually better — a visible-but-refused option tells the reader the rule exists — but a column that is mostly forbidden reads better short.",
        "name": "hideDisabledOptions",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Offer a \\u201cnow\\u201d action in the panel footer (antd's `showNow`). It is REFUSED, not hidden, when `disabledTime` forbids the current time — the same treatment a forbidden column option gets.",
        "name": "showNow",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Hold the panel's choices as a DRAFT until a confirm action commits them (antd's `needConfirm`). Default `false` diverges from antd deliberately: this library has always committed on select and closed, and flipping the default would silently add a click to every existing time field. Opt in where the value is expensive to change (a saved shift, a published slot).",
        "name": "needConfirm",
        "type": "boolean"
      },
      {
        "description": "Controlled value in HH:mm (24h) format. When provided the component is fully controlled — you must update it via `onValueChange`.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial value in HH:mm format. Used only when `value` is not provided.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Called with the canonical HH:mm string whenever the user commits a time (picks from columns or types and blurs/presses Enter). Not called for every keystroke.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Native form field name. Formatted or staged controls manage a hidden canonical value internally; do not add a second hidden field.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "HTML id for the visible input — use with a `<label htmlFor>` for accessibility.",
        "name": "id",
        "type": "string"
      },
      {
        "defaultValue": "hh:mm (i18n fallback)",
        "description": "Placeholder text shown when the input is empty. Falls back to the i18n key `dataEntry.timePicker.placeholder`.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Disables both the visible input and the clock-icon popover trigger.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "5",
        "description": "Step for the minute column (1–60). Only multiples of this step appear in the picker; typed values are still free-form and normalized on blur.",
        "name": "minuteStep",
        "type": "number"
      },
      {
        "description": "Extra Tailwind classes applied to the outer wrapper `<div>`. Use for width overrides (e.g. `w-32`).",
        "name": "className",
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Inline ✕ on the trigger that resets the value when one is set (Ant-style). Pass `false` to hide it (e.g. a required field).",
        "name": "allowClear",
        "type": "boolean"
      }
    ],
    "related": [
      "DatePicker — use for calendar date selection; combine with TimePicker in a flex row when you need a full datetime. DatePicker emits an ISO date string; TimePicker emits HH:mm.",
      "Input — the raw primitive TimePicker wraps internally. Use Input directly only when you need a plain text field with no time semantics or popover.",
      "ColorPicker — another popover-backed input primitive in the same group; structurally similar pattern but for hex colour values."
    ],
    "rules": [
      3,
      6,
      13,
      23
    ],
    "storyPath": "data-entry/time-picker.tsx",
    "tagline": "Time combobox with hours, minutes, optional seconds, confirmation and 12-hour formatting. Native submission stays canonical 24-hour time.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "One trailing icon: empty shows calendar/clock; filled and clearable shows only ×; allowClear=false retains the picker icon. Click the input or ArrowDown to open a filled picker.",
      "DO give it a name in native forms. It submits HH:mm or HH:mm:ss, regardless of display format; the component manages any hidden canonical field.",
      "DO use the controlled pattern (`value` + `onValueChange`) in React-managed forms (e.g. useForm). For simple HTML forms without React state, omit `value` and use `defaultValue` for the uncontrolled pattern.",
      "DON'T pass a raw `<input type='time'>` alongside or instead — this component IS the input, fully accessible (role='combobox', aria-expanded, aria-haspopup) and e2e-testable by filling the text input directly.",
      "DO pair with a `<label htmlFor={id}>` for screen-reader accessibility — the component renders a plain `<input>` internally that `id` connects to.",
      "DON'T expect `onValueChange` on every keystroke — it fires only when a valid HH:mm is committed (column pick closes popover; typed value normalised on blur or Enter). Guard downstream logic accordingly.",
      "DO adjust `minuteStep` for domain needs (e.g. `minuteStep={15}` for scheduling, `minuteStep={1}` for precise entry) — the minute column only shows multiples, but the type-in field accepts any valid HH:mm."
    ],
    "useCases": [
      "Shift/schedule entry forms where workers select start and end times from a scrollable hour/minute grid (use `minuteStep={15}` or `minuteStep={30}`).",
      "Invoice or transaction timestamp fields that require a 24h HH:mm time alongside a DatePicker — pair the two in a flex row.",
      "Logistics cut-off time configuration (e.g. 'last order by') where the default `minuteStep={5}` aligns with typical operational granularity.",
      "Admin settings panels that persist a canonical HH:mm string to the database — the `name` prop makes native form submission trivial.",
      "Time-range pickers (from/to) — render two TimePicker instances side-by-side with separate controlled values and validate that `to > from` in `onValueChange`.",
      "E2E-tested forms — test helpers can fill the text input directly (it accepts typed HH:mm) without needing to interact with the popover columns."
    ]
  },
  {
    "example": "{`import { Cascader } from \"@godxjp/ui/data-entry\";\n\nconst REGIONS = [\n  {\n    value: \"jp\",\n    label: \"日本\",\n    children: [\n      {\n        value: \"tokyo\",\n        label: \"東京都\",\n        children: [\n          { value: \"shinjuku\", label: \"新宿区\" },\n          { value: \"shibuya\", label: \"渋谷区\" },\n        ],\n      },\n    ],\n  },\n  {\n    value: \"vn\",\n    label: \"Việt Nam\",\n    children: [\n      {\n        value: \"hcm\",\n        label: \"TP. Hồ Chí Minh\",\n        children: [\n          { value: \"q1\", label: \"Quận 1\" },\n          { value: \"q3\", label: \"Quận 3\" },\n        ],\n      },\n    ],\n  },\n];\n\n// Controlled single-path\nfunction RegionPicker() {\n  const [path, setPath] = React.useState<string[]>([]);\n\n  return (\n    <Cascader\n      options={REGIONS}\n      value={path}\n      onValueChange={(v) => setPath(v as string[])}\n      showSearch\n      placeholder=\"Select region…\"\n    />\n  );\n}\n\n// Multi-path (multiple selection)\nfunction MultiRegionPicker() {\n  const [paths, setPaths] = React.useState<string[][]>([]);\n\n  return (\n    <Cascader\n      options={REGIONS}\n      multiple\n      value={paths}\n      onValueChange={(v) => setPaths(v as string[][])}\n      showSearch\n    />\n  );\n}\n\n// With custom field names (data uses 'name'/'id'/'nodes')\n<Cascader\n  options={rawApiData}\n  fieldNames={{ label: \"name\", value: \"id\", children: \"nodes\" }}\n  defaultValue={[\"dept-1\", \"team-3\"]}\n/>\n\n// changeOnSelect: lets user pick a branch node (not only leaves)\n<Cascader\n  options={REGIONS}\n  changeOnSelect\n  onValueChange={(v) => console.log(\"path\", v)}\n/>\n`}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Cascader",
    "props": [
      {
        "description": "Native form field name; repeated values for multiple paths.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Prevent edits while preserving the displayed value.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "The hierarchical option tree. Each node has { value: string; label: ReactNode; disabled?: boolean; isLeaf?: boolean; children?: TreeOptionProp[] }. Normalised internally via fieldNames.",
        "name": "options",
        "required": true,
        "type": "TreeOptionProp[]"
      },
      {
        "description": "Controlled value. Single mode: string[] path (e.g. ['vn','hcm','q1']). Multiple mode: string[][] array of paths.",
        "name": "value",
        "type": "string[] | string[][]"
      },
      {
        "description": "Initial value for uncontrolled mode. Same shape as value.",
        "name": "defaultValue",
        "type": "string[] | string[][]"
      },
      {
        "description": "Fires when selection changes. First arg is the selected path(s); second is the matching node objects. On clear, called with [].",
        "name": "onValueChange",
        "type": "(value: string[] | string[][], selectedOptions?: TreeOptionProp[] | TreeOptionProp[][]) => void"
      },
      {
        "defaultValue": "false",
        "description": "Enable multi-path selection. Renders checkboxes in columns and search results. Panel stays open on each pick. value/defaultValue become string[][].",
        "name": "multiple",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "When true, clicking any node (including branch nodes with children) commits that path immediately instead of waiting for a leaf selection.",
        "name": "changeOnSelect",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Renders a CommandInput at the top of the popover. Filters to matching leaf paths across the whole tree when a query is typed; reverts to cascade columns when the query is cleared.",
        "name": "showSearch",
        "type": "boolean"
      },
      {
        "description": "Trigger button placeholder text when no value is selected. Defaults to the i18n key dataEntry.cascader.placeholder.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Disables the trigger button and prevents the popover from opening.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "\"click\"",
        "description": "How child columns are expanded. 'hover' expands on mouseenter and collapses back on mouseleave.",
        "name": "expandTrigger",
        "type": "\"click\" | \"hover\""
      },
      {
        "description": "Remap custom data keys: { label?: string; value?: string; children?: string }. Use when your data uses e.g. 'name' and 'id' instead of 'label' and 'value'.",
        "name": "fieldNames",
        "type": "TreeFieldNamesProp"
      },
      {
        "defaultValue": "true",
        "description": "Shows an X icon on the trigger when a value is selected. Clicking it calls onChange([]) and resets to placeholder. On by default, as antd Cascader; pass `false` on a required field.",
        "name": "allowClear",
        "type": "boolean"
      },
      {
        "description": "Extra Tailwind classes applied to the trigger button.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "HTML id forwarded to the trigger button. Use to associate a <label htmlFor>.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "antd `status`. `error` recolours the control AND sets aria-invalid (a colour-only error fails WCAG 2.2 SC 1.4.1); `warning` recolours only. An aria-invalid injected by FormField always wins.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "antd `variant` — the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "antd `loading` — the FIELD is in flight: the trailing chevron becomes a spinner and the trigger reports aria-busy. Distinct from the list-level spinner an async loadOptions shows inside the popup.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "antd `size` — height tier on the shared --control-height ladder.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "antd `open` — controlled panel state. The consumer is the authority: nothing internal closes a pinned panel.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "antd `defaultOpen` — uncontrolled initial panel state.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "antd `onOpenChange` — fires for both controlled and uncontrolled panels.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "antd `maxTagCount` — how many selected values stay visible before the rest collapse into the overflow node. antd's `\"responsive\"` is not supported (see the parity PR).",
        "name": "maxTagCount",
        "type": "number"
      },
      {
        "description": "antd `maxTagPlaceholder` — the node standing in for what maxTagCount hid. Defaults to a localized `+N`.",
        "name": "maxTagPlaceholder",
        "type": "React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)"
      },
      {
        "description": "antd `notFoundContent` — the node shown when the popup has nothing to list. Outranks the string-only emptyMessage.",
        "name": "notFoundContent",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `autoClearSearchValue` (default true) — clear the search box after a pick / on close. Set false to resume the same filtered list on the next open.",
        "name": "autoClearSearchValue",
        "type": "boolean"
      },
      {
        "description": "antd `showCheckedStrategy` (multiple only). SHOW_PARENT collapses a fully-checked branch into the branch itself; SHOW_CHILD (default) lists the leaves.",
        "name": "showCheckedStrategy",
        "type": "\"SHOW_CHILD\" | \"SHOW_PARENT\""
      },
      {
        "description": "antd `loadData` — lazy children. Called when a branch that has no children and isLeaf !== true is expanded, and never again once its promise resolves; push the fetched children into options. A rejected promise is forgotten, so the next activation of the branch asks again (gh#1041).",
        "name": "loadData",
        "type": "(selectedOptions: TreeOptionProp[]) => void | Promise<void>"
      },
      {
        "description": "antd `displayRender` — owns the trigger label built from the selected path.",
        "name": "displayRender",
        "type": "(labels: string[], selectedOptions?: TreeOptionProp[]) => React.ReactNode"
      },
      {
        "description": "antd `optionRender` — owns a column row's body. The checkbox, check mark and chevron stay with the component.",
        "name": "optionRender",
        "type": "(option: TreeOptionProp) => React.ReactNode"
      },
      {
        "description": "Controlled search query (antd `showSearch.searchValue`).",
        "name": "search",
        "type": "string"
      },
      {
        "description": "Search query change (antd `showSearch.onSearch`).",
        "name": "onSearchChange",
        "type": "(query: string) => void"
      },
      {
        "description": "Accessible name for the COMBOBOX TRIGGER — the element a keyboard user lands on, not the panel. Inside a FormField it is injected for you and you do not pass it; on a bare Cascader (a toolbar scope filter, a compact drilldown with no label row) it is the only name the control has, and cardinal rule 227 requires one. Route it through t(). The rest of the field-a11y contract — aria-labelledby / describedby / errormessage / invalid / required — is accepted on every form-capable component here and is FormField's to wire.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "TreeSelect — use when the hierarchy is a collapsible tree (expand/collapse nodes) rather than side-by-side columns, and when a single flat value string (node key) is sufficient instead of a full ancestor path. TreeSelect also supports treeCheckable for multi-select.",
      "Select — use for a flat (non-hierarchical) list of options. Cascader is only needed when items have meaningful parent–child levels.",
      "Transfer — use when the user needs to shuttle multiple items between two panels; not for hierarchical path selection."
    ],
    "rules": [
      3,
      6,
      23,
      31
    ],
    "storyPath": "data-entry/cascader.tsx",
    "tagline": "Multi-level hierarchical path picker (Popover + cascading columns); value is always a string[] path, never a flat ID — passing a bare string breaks it.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "DO pass a string[] path as value in single mode (e.g. ['country','region','city']). DON'T pass a flat string ID — the component treats value as an ordered path array and will render nothing if you pass a bare string.",
      "DO use value + onChange together for controlled mode, or defaultValue alone for uncontrolled. DON'T mix both — providing value without onChange makes the field read-only (the internal state won't update).",
      "DO set multiple={true} and pass value as string[][] (array of paths) for multi-selection. onChange receives string[][] in that mode. Mixing single-mode shape with multiple={true} silently produces no selection.",
      "DON'T hand-roll a search input next to Cascader. Use showSearch={true} — it adds a built-in CommandInput that filters leaf paths across the full tree and reverts to cascade columns when cleared.",
      "DO use fieldNames to remap data keys ({label:'name', value:'id', children:'nodes'}) rather than pre-transforming your API data. This keeps options in their original shape.",
      "For form submission, Cascader has no 'name' prop. Wrap in a controlled pattern and store the path array in your form state (useForm/useState). For Inertia useForm, keep the field as an array (e.g. data.categoryPath = ['a','b','c'])."
    ],
    "useCases": [
      "Geographic drilldown (Country → Prefecture → City) for address or branch-office pickers in accounting or logistics forms.",
      "Expense category selection (e.g. Operating Expenses → Marketing → Digital Ads) where the full classification path is required for the general ledger.",
      "Product taxonomy navigation (Department → Category → Sub-category) in inventory or invoice line-item entry.",
      "Organisational unit picker (Company → Division → Department) in budget allocation or approval-routing configurations.",
      "Multi-region filter in a report or dashboard filter bar, using multiple={true} to allow selecting several leaf locations at once.",
      "Any deeply nested classification where the relationship between levels is meaningful and must be captured — not just the leaf value."
    ]
  },
  {
    "example": "import { useState } from \"react\";\nimport { FormField, TreeSelect } from \"@godxjp/ui/data-entry\";\n\nconst accountTree = [\n  {\n    value: \"assets\",\n    label: \"Assets\",\n    children: [\n      { value: \"current-assets\", label: \"Current Assets\", children: [\n          { value: \"cash\", label: \"Cash\" },\n          { value: \"ar\", label: \"Accounts Receivable\" },\n        ],\n      },\n      { value: \"fixed-assets\", label: \"Fixed Assets\", children: [\n          { value: \"equipment\", label: \"Equipment\" },\n        ],\n      },\n    ],\n  },\n  {\n    value: \"liabilities\",\n    label: \"Liabilities\",\n    children: [\n      { value: \"ap\", label: \"Accounts Payable\" },\n    ],\n  },\n];\n\n// Single-select (returns string | undefined)\nexport function AccountPicker() {\n  const [account, setAccount] = useState<string | undefined>();\n  return (\n    <FormField id=\"account-picker\" label=\"GL Account\">\n      <TreeSelect\n        id=\"account-picker\"\n        treeData={accountTree}\n        value={account}\n        onValueChange={(v) => setAccount(v as string | undefined)}\n        showSearch\n        treeDefaultExpandAll\n        placeholder=\"Select account…\"\n        allowClear\n      />\n    </FormField>\n  );\n}\n\n// Multi-select with checkboxes + cascade + SHOW_PARENT display\nexport function DepartmentFilter() {\n  const [selected, setSelected] = useState<string[]>([]);\n  return (\n    <TreeSelect\n      id=\"dept-filter\"\n      treeData={accountTree}\n      value={selected}\n      onValueChange={(v) => setSelected(v as string[])}\n      treeCheckable\n      showCheckedStrategy={TreeSelect.SHOW_PARENT}\n      showSearch\n      placeholder=\"Filter by department…\"\n    />\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "TreeSelect",
    "props": [
      {
        "description": "Native form field name; repeated values for multiple selections.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Prevent edits while preserving value.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "The tree data. Each node: `{ value: string; label: ReactNode; disabled?: boolean; disableCheckbox?: boolean; isLeaf?: boolean; children?: TreeOptionProp[] }`. Use `fieldNames` to remap custom keys.",
        "name": "treeData",
        "required": true,
        "type": "TreeOptionProp[]"
      },
      {
        "description": "Controlled selected value(s). Pass `string` in single mode, `string[]` in multi/checkable mode. When undefined the component is uncontrolled.",
        "name": "value",
        "type": "string | string[]"
      },
      {
        "description": "Initial value for uncontrolled usage. Ignored once `value` is provided.",
        "name": "defaultValue",
        "type": "string | string[]"
      },
      {
        "description": "Called on selection change. Returns `string` in single mode, `string[]` in multi/checkable mode, or `undefined` when cleared.",
        "name": "onValueChange",
        "type": "(value: string | string[] | undefined) => void"
      },
      {
        "defaultValue": "false",
        "description": "Enable multi-select without checkboxes. When true, `onValueChange` always fires with `string[]`.",
        "name": "multiple",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Render Checkbox controls beside each node. Implies multi-select; cascade-selects all descendants by default unless `treeCheckStrictly` is set.",
        "name": "treeCheckable",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "When true (only with `treeCheckable`), parent and child selections are independent — checking a parent does NOT auto-check its children.",
        "name": "treeCheckStrictly",
        "type": "boolean"
      },
      {
        "defaultValue": "\"SHOW_CHILD\"",
        "description": "Controls which values appear in the trigger label when checkboxes are used. `SHOW_CHILD` (default) — show only leaf nodes selected; `SHOW_PARENT` — show nearest ancestor when all children selected; `SHOW_ALL` — show every checked node. Use the exported constants `TreeSelect.SHOW_CHILD`, `TreeSelect.SHOW_PARENT`, `TreeSelect.SHOW_ALL` instead of raw strings.",
        "name": "showCheckedStrategy",
        "type": "\"SHOW_CHILD\" | \"SHOW_PARENT\" | \"SHOW_ALL\""
      },
      {
        "defaultValue": "false",
        "description": "Show a labelled SearchInput at the top of the dropdown. Filters visible tree nodes by label text; the trigger combobox controls the tree.",
        "name": "showSearch",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Expand all nodes when the dropdown first opens. Initialised once; does not re-expand on re-render.",
        "name": "treeDefaultExpandAll",
        "type": "boolean"
      },
      {
        "description": "Trigger button placeholder text when nothing is selected. Defaults to the i18n key `dataEntry.treeSelect.placeholder`.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Disables the trigger button and all interactions.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Show an `X` icon in the trigger to clear the selection. Off by default, as antd TreeSelect; pass `allowClear` on an optional field.",
        "name": "allowClear",
        "type": "boolean"
      },
      {
        "description": "Additional Tailwind classes applied to the trigger Button.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "HTML `id` placed on the trigger Button — use this to associate a `<label htmlFor>` for accessibility.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Accessible name for the combobox trigger when no visible label is available.",
        "name": "aria-label",
        "type": "string"
      },
      {
        "description": "ID of the element containing the current validation error message.",
        "name": "aria-errormessage",
        "type": "string"
      },
      {
        "description": "Marks the semantic combobox trigger invalid for assistive technology.",
        "name": "aria-invalid",
        "type": "boolean | 'true' | 'false'"
      },
      {
        "description": "Marks the semantic combobox trigger required for assistive technology.",
        "name": "aria-required",
        "type": "boolean | 'true' | 'false'"
      },
      {
        "description": "Remap data object keys. Example: `{ label: 'name', value: 'id', children: 'items' }` so you don't have to transform your API response before passing it to `treeData`.",
        "name": "fieldNames",
        "type": "{ label?: string; value?: string; children?: string }"
      },
      {
        "description": "antd `status`. `error` recolours the control AND sets aria-invalid (a colour-only error fails WCAG 2.2 SC 1.4.1); `warning` recolours only. An aria-invalid injected by FormField always wins.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "antd `variant` — the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "antd `loading` — the FIELD is in flight: the trailing chevron becomes a spinner and the trigger reports aria-busy. Distinct from the list-level spinner an async loadOptions shows inside the popup.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "antd `size` — height tier on the shared --control-height ladder.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "antd `open` — controlled panel state. The consumer is the authority: nothing internal closes a pinned panel.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "antd `defaultOpen` — uncontrolled initial panel state.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "antd `onOpenChange` — fires for both controlled and uncontrolled panels.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "antd `maxTagCount` — how many selected values stay visible before the rest collapse into the overflow node. antd's `\"responsive\"` is not supported (see the parity PR).",
        "name": "maxTagCount",
        "type": "number"
      },
      {
        "description": "antd `maxTagPlaceholder` — the node standing in for what maxTagCount hid. Defaults to a localized `+N`.",
        "name": "maxTagPlaceholder",
        "type": "React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)"
      },
      {
        "description": "antd `notFoundContent` — the node shown when the popup has nothing to list. Outranks the string-only emptyMessage.",
        "name": "notFoundContent",
        "type": "React.ReactNode"
      },
      {
        "description": "antd `autoClearSearchValue` (default true) — clear the search box after a pick / on close. Set false to resume the same filtered list on the next open.",
        "name": "autoClearSearchValue",
        "type": "boolean"
      },
      {
        "description": "antd `loadData` — lazy children. Called when a node that has no children and isLeaf !== true is expanded, and never again once its promise resolves; push the fetched children into treeData. A rejected promise folds the branch shut and the next expand asks again, up to 10 attempts (rc-tree, gh#1041). Such a node still reads as expandable (aria-expanded + a working expander).",
        "name": "loadData",
        "type": "(node: TreeOptionProp) => void | Promise<void>"
      },
      {
        "description": "antd `treeTitleRender` — owns a node's title only; the checkbox, expander and row ARIA stay with the component.",
        "name": "treeTitleRender",
        "type": "(node: TreeOptionProp) => React.ReactNode"
      },
      {
        "description": "Controlled search query (antd `showSearch.searchValue`).",
        "name": "search",
        "type": "string"
      },
      {
        "description": "Search query change (antd `showSearch.onSearch`).",
        "name": "onSearchChange",
        "type": "(query: string) => void"
      }
    ],
    "related": [
      "Select — flat single/multi picker; use when data has no parent-child hierarchy. Pick TreeSelect as soon as items have `children`.",
      "Cascader — also renders tree data but in a multi-column panel where the user drills down column by column; pick Cascader for strict path selection (select a full path Country→Region→City). Pick TreeSelect when the user may select any node at any level or needs checkboxes.",
      "Checkbox / CheckboxGroup — use for a small, always-visible flat list of options. Use TreeSelect when options are hierarchical or the list is long enough to warrant a dropdown.",
      "Command / CommandInput — low-level search primitive; TreeSelect already embeds this internally. Do NOT compose your own tree dropdown out of Command — use TreeSelect."
    ],
    "rules": [
      3,
      6,
      13,
      23
    ],
    "storyPath": "data-entry/tree-select.tsx",
    "tagline": "Hierarchical tree picker in a Popover (single or multi-select with checkboxes) — `onValueChange` receives `string` in single mode and `string[]` in multi/checkable mode; never use a raw `<select>` for tree-structured data.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "DO pair with a `<label htmlFor={id}>` and pass the matching `id` prop so screen readers announce the control correctly. The underlying trigger is a `<Button role='combobox'>` — not a native `<select>` — so an explicit label is required.",
      "DO use `treeCheckable` (+ optionally `showCheckedStrategy`) for selecting multiple nodes with parent–child cascade; use `multiple` only when you want multi-select WITHOUT the checkbox cascade behaviour.",
      "DO use the static constants `TreeSelect.SHOW_CHILD`, `TreeSelect.SHOW_PARENT`, `TreeSelect.SHOW_ALL` (or the named exports `SHOW_CHILD`/`SHOW_PARENT`/`SHOW_ALL` from the same import path) instead of raw string literals for `showCheckedStrategy`.",
      "DON'T pass `value` and `defaultValue` simultaneously — pick controlled (`value` + `onValueChange`) OR uncontrolled (`defaultValue` only). Mixing them causes the component to silently prefer the controlled path.",
      "DON'T hand-roll `onValueChange` type narrowing: in single mode the callback receives `string | undefined`; in multi/checkable mode it receives `string[]`. Branch on `multiple || treeCheckable` if you need to handle both shapes in the same handler.",
      "DON'T use a raw `<select>` or a flat `Select` component for hierarchical/nested data — TreeSelect is the correct primitive. If hierarchy is irrelevant and data is flat, use `Select` instead."
    ],
    "useCases": [
      "Chart-of-accounts picker in an accounting app where accounts belong to groups (Assets > Current Assets > Cash) and the user must select one leaf account.",
      "Multi-select department or cost-centre filter where selecting a parent division should auto-select all child departments (treeCheckable + SHOW_PARENT).",
      "Category assignment on invoice line items where categories have up to 3 levels of nesting and users can assign a parent or a leaf.",
      "Permission scope selector where roles are structured in a tree and selecting a parent role should cascade to all child scopes (treeCheckable + treeCheckStrictly=false).",
      "Location picker (Country > Prefecture > City) in a form where only leaf-level cities are valid selections (single mode, no checkboxes).",
      "Large GL hierarchy browser with showSearch enabled so users can type to filter thousands of account codes instead of manually expanding nodes."
    ]
  },
  {
    "example": "import { useState } from \"react\";\nimport { Transfer } from \"@godxjp/ui/data-entry\";\n\nconst ALL_ACCOUNTS = [\n  { key: \"1010\", title: \"Cash\", description: \"Asset\" },\n  { key: \"1020\", title: \"Accounts Receivable\", description: \"Asset\" },\n  { key: \"2010\", title: \"Accounts Payable\", description: \"Liability\" },\n  { key: \"3010\", title: \"Revenue\", description: \"Income\" },\n  { key: \"4010\", title: \"Cost of Goods Sold\", description: \"Expense\", disabled: true },\n];\n\nexport function AccountMapping() {\n  const [targetKeys, setTargetKeys] = useState<string[]>([\"1010\"]);\n\n  return (\n    <Transfer\n      dataSource={ALL_ACCOUNTS}\n      targetKeys={targetKeys}\n      onValueChange={(nextKeys) => setTargetKeys(nextKeys)}\n      titles={[\"Available Accounts\", \"Mapped Accounts\"]}\n      showSearch\n    />\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Transfer",
    "props": [
      {
        "description": "Canonical controlled target keys; takes precedence over targetKeys.",
        "name": "value",
        "type": "string[]"
      },
      {
        "description": "Initial uncontrolled target keys.",
        "name": "defaultValue",
        "type": "string[]"
      },
      {
        "description": "Compatibility name for uncontrolled initial target keys.",
        "name": "defaultTargetKeys",
        "type": "string[]"
      },
      {
        "description": "Native form name; repeats once per target key.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Preserve assignment while preventing edits.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Independent pane pages; select-all affects visible enabled rows.",
        "name": "pagination",
        "type": "boolean | { pageSize?: number }"
      },
      {
        "description": "Show each pane select-all control; defaults true.",
        "name": "showSelectAll",
        "type": "boolean"
      },
      {
        "description": "Custom search predicate.",
        "name": "filterOption",
        "type": "(query: string, item: TransferItemProp) => boolean"
      },
      {
        "description": "Custom non-interactive row content; checkbox labels remain associated.",
        "name": "render",
        "type": "(item: TransferItemProp) => ReactNode"
      },
      {
        "description": "Full flat list of all items (both source and target). Each item needs a unique `key` string, a `title` (ReactNode rendered in the list row), an optional `description` (shown as a secondary line), and an optional `disabled` boolean to lock individual items.",
        "name": "dataSource",
        "required": true,
        "type": "TransferItemProp[]"
      },
      {
        "description": "Keys of items currently in the right (target) panel. Items whose key is NOT in this array appear in the left (source) panel. This is the primary controlled state — you must update it inside `onChange`.",
        "name": "targetKeys",
        "required": true,
        "type": "string[]"
      },
      {
        "description": "Called after the user clicks a move button. Receives the new full targetKeys array, the direction of movement ('right' = source→target, 'left' = target→source), and the keys that were actually moved. Update your targetKeys state here.",
        "name": "onChange",
        "type": "(targetKeys: string[], direction: 'left' | 'right', moveKeys: string[]) => void"
      },
      {
        "description": "Panel header labels. Index 0 = left/source panel, index 1 = right/target panel. Defaults to i18n strings (dataEntry.transfer.source / dataEntry.transfer.target).",
        "name": "titles",
        "type": "[React.ReactNode, React.ReactNode]"
      },
      {
        "defaultValue": "false",
        "description": "When true, renders a SearchInput inside each panel that filters items by title and description text (debounce=0). Does not affect the underlying data; purely a client-side filter.",
        "name": "showSearch",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "When true, hides the left-pointing move button so items can only flow source → target. Useful for append-only assignment flows.",
        "name": "oneWay",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Disables the entire component: all checkboxes, the search input (pointer-events-none), and both move buttons.",
        "name": "disabled",
        "type": "DisabledProp (boolean)"
      },
      {
        "description": "Extra Tailwind classes applied to the outer flex wrapper. Use to constrain width or add margin.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Lands on the `role=\"group\"` shuttle container, not on any one input — a two-pane shuttle has no single labelable control, so this is what a FormField label points at. FormField injects it; pass it yourself only for a bare Transfer.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Controlled selection state as a tuple: index 0 = keys checked in the source panel, index 1 = keys checked in the target panel. Omit to use internal (uncontrolled) selection state. Must be paired with `onSelectChange` when provided.",
        "name": "selectedKeys",
        "type": "[string[], string[]]"
      },
      {
        "description": "Called whenever the checked selection in either panel changes. Provides updated arrays for source and target selections. Required when `selectedKeys` is controlled.",
        "name": "onSelectChange",
        "type": "(sourceSelectedKeys: string[], targetSelectedKeys: string[]) => void"
      },
      {
        "description": "antd `locale`, ported for `notFoundContent` only: what an empty or search-emptied pane shows (default: localized `dataEntry.transfer.empty`). A `[source, target]` pair sets each pane separately, as in antd.",
        "name": "locale",
        "type": "{ notFoundContent?: ReactNode | [ReactNode, ReactNode] }"
      },
      {
        "description": "Fires when items move between panels; you own `targetKeys` state.",
        "name": "onValueChange",
        "type": "(targetKeys: string[], direction: \"left\" | \"right\", moveKeys: string[]) => void"
      }
    ],
    "related": [
      "Select mode=\"multiple\" — picks multiple values from a dropdown; prefer it when the option set is large and a two-panel layout is not needed.",
      "Checkbox (list) — use for a simple flat multi-select without a shuttle/move metaphor.",
      "Select (compound) — single or multi-value dropdown; not a dual-panel component.",
      "Tree — hierarchical item display; combine with Transfer's dataSource if items have a tree structure but the shuttle UX is still needed."
    ],
    "rules": [
      23,
      31
    ],
    "storyPath": "data-entry/transfer.tsx",
    "tagline": "Dual-list shuttle that moves items between source and target via Checkbox selection — you own targetKeys state; never hand-roll a two-panel picker.",
    "usage": [
      "DO own `targetKeys` in state and update it inside `onChange`: `const [targetKeys, setTargetKeys] = useState<string[]>([]); onValueChange={(next) => setTargetKeys(next)}`.",
      "DO NOT hand-roll a two-panel checkbox picker — Transfer ships the full shuttle UX (select-all header, indeterminate state, search, move buttons, empty state) out of the box.",
      "DO enable `showSearch` for lists longer than ~10 items; the built-in SearchInput filters by both `title` and `description` text content, including ReactNode content via `reactNodeText`.",
      "DO use `oneWay={true}` for append-only flows (e.g. adding permissions to a role) where items must never be moved back.",
      "DO control `selectedKeys` / `onSelectChange` only when you need to read which items are currently checked (e.g. for a bulk-action toolbar outside the component). For most cases, leave both props out and let Transfer manage selection internally.",
      "AVOID using Transfer for simple single-select or toggle scenarios — use a Checkbox list or Select multiple instead. Transfer is specifically for shuttle/dual-panel assignment flows."
    ],
    "useCases": [
      "Assigning roles or permissions to a user: source panel shows available roles, target panel shows assigned roles; `oneWay={false}` allows removal.",
      "Building a report column picker: source = all available columns, target = columns included in the report, user orders and moves them across.",
      "Account mapping in an accounting app: map external chart-of-accounts entries (source) to canonical internal accounts (target) in a bulk import wizard.",
      "Tag / label assignment in a CMS: move content tags from an available pool into a 'selected' set for a document.",
      "Feature-flag targeting: move user segments from an 'all segments' list into the 'targeted segments' panel for a flag.",
      "Permission set builder in an admin UI: shuttle individual API scopes from 'available' to 'granted' for an API key or OAuth client."
    ]
  },
  {
    "example": "import { useState } from \"react\";\nimport { Upload, type UploadFileItem, collectUploadCommitActions } from \"@godxjp/ui/data-entry\";\n\n// Example: avatar picker with server upload\nexport function AvatarUploadForm() {\n  const [items, setItems] = useState<UploadFileItem[]>([]);\n\n  async function handleUpload(file: File, _item: UploadFileItem) {\n    const fd = new FormData();\n    fd.append(\"file\", file);\n    const res = await fetch(\"/api/media/upload\", { method: \"POST\", body: fd });\n    const { mediaId, previewUrl } = await res.json();\n    return { mediaId, previewUrl };\n  }\n\n  function handleSubmit(e: React.FormEvent) {\n    e.preventDefault();\n    const { deleteMediaIds, promoteMediaIds } = collectUploadCommitActions(items);\n    // Send to your API — never send raw File objects\n    console.log({ deleteMediaIds, promoteMediaIds });\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <Upload\n        variant=\"avatar-crop\"\n        value={items}\n        onValueChange={setItems}\n        onUpload={handleUpload}\n        maxSizeBytes={5 * 1024 * 1024}\n      />\n      <button type=\"submit\">Save Profile</button>\n    </form>\n  );\n}\n\n// Example: multi-file dropzone\nexport function DocumentUploadDropzone() {\n  const [items, setItems] = useState<UploadFileItem[]>([]);\n\n  return (\n    <Upload\n      variant=\"dropzone\"\n      value={items}\n      onValueChange={setItems}\n      accept=\".pdf,.xlsx\"\n      maxCount={10}\n      maxSizeBytes={20 * 1024 * 1024}\n      onUpload={async (file) => {\n        const res = await fetch(\"/api/media/upload\", {\n          method: \"POST\",\n          body: Object.assign(new FormData(), { file }),\n        });\n        return res.json();\n      }}\n    />\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Upload",
    "props": [
      {
        "defaultValue": "\"dropzone\"",
        "description": "Controls the visual rendering mode. dropzone = large dashed drop area + file list; button = compact outline button + file list; picture-card = grid of 96×96 image thumbnails; picture = single image preview with change/remove actions; avatar = circular single-image picker; avatar-crop = avatar with an in-dialog crop step before the item is staged.",
        "name": "variant",
        "type": "\"dropzone\" | \"button\" | \"picture-card\" | \"picture\" | \"avatar\" | \"avatar-crop\""
      },
      {
        "defaultValue": "\"picture\" for variant=\"picture\", otherwise \"text\"",
        "description": "HOW THE CHOSEN FILES ARE LISTED — antd's listType, and an axis of its own: variant decides how files are PICKED, listType decides how the picked ones are DRAWN. text = name, size and actions (the classic dropzone/button row). picture = a leading box on every row: the thumbnail when the item has a previewUrl, otherwise the glyph for its file kind (image / pdf / archive / text / generic), both boxes the same size so a mixed list keeps one row height. The glyph is decorative (aria-hidden) and carries its kind as data-file-kind for theming. antd's picture-card is variant='picture-card' here — the tile grid IS the picker there, so it is not offered as a listType.",
        "name": "listType",
        "type": "\"text\" | \"picture\""
      },
      {
        "description": "Controlled list of file items. When provided the component is controlled — you own the state. Omit to run uncontrolled.",
        "name": "value",
        "type": "UploadFileItem[]"
      },
      {
        "description": "Initial list of file items for uncontrolled usage. Ignored once value is provided.",
        "name": "defaultValue",
        "type": "UploadFileItem[]"
      },
      {
        "description": "MIME / extension accept string passed to the hidden <input type=\"file\">. avatar/avatar-crop/picture/picture-card default to \"image/*\"; dropzone and button default to unrestricted.",
        "name": "accept",
        "type": "string"
      },
      {
        "description": "Allow multi-file selection. Auto-derived: false when maxCount is 1 (or when variant is avatar/avatar-crop/picture); otherwise true.",
        "name": "multiple",
        "type": "boolean"
      },
      {
        "description": "Hard upper bound on the number of items. avatar/avatar-crop/picture auto-default to 1. Once the limit is reached the add button is hidden (picture-card) or additions are rejected; maxCount=1 replaces the current item.",
        "name": "maxCount",
        "type": "number"
      },
      {
        "description": "Files larger than this limit are rejected with ONE localized message that states the limit (「big.pdf: ファイルが大きすぎます（上限 10.0 MB）。」, gh#1280) and onReject.",
        "name": "maxSizeBytes",
        "type": "number"
      },
      {
        "description": "Disables all interactive surfaces (drop zone, buttons). Visual opacity + pointer-events-none applied.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Show the remove/delete control on each item. Set false to make uploads permanent within the session.",
        "name": "removable",
        "type": "boolean"
      },
      {
        "description": "Called immediately after a file is picked (before form submit). Transitions the item to status='uploading', then 'done' on resolve or 'error' on reject. Wire this to your media-service issue/PUT/complete cycle. If omitted files stay in status='idle' and the raw File object remains in item.file.",
        "name": "onUpload",
        "type": "(file: File, item: UploadFileItem, context: UploadRequestContext) => Promise<UploadResult>"
      },
      {
        "description": "Extra CSS class applied to the outer wrapper div.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Lands on the native `<input type=\"file\">` (so `#id` file attachment in e2e works), NOT on the wrapper. Since gh#1280 that input is plumbing — `aria-hidden`, `tabIndex={-1}`, and it forwards focus to the visible trigger, so a FormField label click lands on the dropzone/button. The FormField contract (name = label + the trigger's own action text, helper, aria-invalid/errormessage) is on that trigger, and the \"nothing picked\" state is read as 「未選択」/\"Chưa chọn tệp\" in the APP's locale — the browser's own 「選択されていません」 is no longer exposed.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Custom button label for variant='button'. Falls back to the i18n 'Upload file' string.",
        "name": "children",
        "type": "React.ReactNode"
      },
      {
        "description": "Fires with the current file list. If it (or `previewFile` / `onRemove`) THROWS, Upload reports the error through `globalThis.reportError` — the console and the window `error` event — and shows a message in the upload area; it is never swallowed (gh#1123).",
        "name": "onValueChange",
        "type": "(items: UploadFileItemProp[]) => void"
      },
      {
        "description": "`variant=\"button\"` only — size of the visible trigger, forwarded to Button. An icon size renders it icon-only and moves the label to `aria-label`: a 32px square beside other icon buttons rather than a 147px labelled one that outweighs them.",
        "name": "triggerSize",
        "type": "\"default\" | \"md\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\""
      },
      {
        "defaultValue": "\"outline\"",
        "description": "`variant=\"button\"` only — visual weight of the visible trigger, forwarded to Button. Default `outline` suits a standalone form field. Pass `ghost` when the trigger sits in a toolbar row beside other icon buttons — inside a chat composer, say — where a bordered square reads as the odd one out.",
        "name": "triggerVariant",
        "type": "\"default\" | \"destructive\" | \"outline\" | \"dashed\" | \"secondary\" | \"ghost\" | \"link\" | \"bare\""
      },
      {
        "defaultValue": "the lucide upload arrow",
        "description": "`variant=\"button\"` only — the glyph the trigger draws. Pass the COMPONENT (`triggerIcon={Plus}`), not an element, exactly as `Icon`'s `as` takes it; every lucide icon qualifies. It exists because the glyph was hard-coded and `triggerVariant` only moves emphasis, so a \"create new\" action that HAPPENS to upload could not carry a plus and had to announce itself as an upload (gh#734). The library still owns the class, the label spacing and the `aria-hidden`, so a swapped glyph renders at the same `--upload-row-icon-size` and never reaches the accessible name — the trigger's metrics cannot drift with the icon.",
        "name": "triggerIcon",
        "type": "React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>"
      },
      {
        "description": "Displays existing files, blocks changes, preserves staged form data.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Select a folder; each item preserves relativePath.",
        "name": "directory",
        "type": "boolean"
      },
      {
        "description": "Paste clipboard files while focus is inside this Upload; text paste is untouched.",
        "name": "pastable",
        "type": "boolean"
      },
      {
        "description": "Defaults true. Disable native dialog activation for drop-only surfaces.",
        "name": "openFileDialogOnClick",
        "type": "boolean"
      },
      {
        "description": "Multipart file field name; named staged files also join native FormData.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Multipart upload URL; onUpload takes precedence.",
        "name": "action",
        "type": "string | ((file: File) => string | Promise<string>)"
      },
      {
        "description": "Request method, default POST.",
        "name": "method",
        "type": "\"POST\" | \"PUT\" | \"PATCH\""
      },
      {
        "description": "Request headers such as CSRF tokens; multipart boundaries remain browser-owned.",
        "name": "headers",
        "type": "Record<string, string>"
      },
      {
        "description": "Additional multipart fields, optionally resolved per file.",
        "name": "data",
        "type": "Record<string, string | Blob> | ((file: File) => Record<string, string | Blob> | Promise<Record<string, string | Blob>>)"
      },
      {
        "description": "Send credentials with the default transport.",
        "name": "withCredentials",
        "type": "boolean"
      },
      {
        "description": "Validate or transform before upload; false stages manually; UPLOAD_LIST_IGNORE excludes. Rejections are reported.",
        "name": "beforeUpload",
        "type": "(file: File, files: File[]) => boolean | File | Blob | typeof UPLOAD_LIST_IGNORE | Promise<boolean | File | Blob | typeof UPLOAD_LIST_IGNORE>"
      },
      {
        "description": "Reports accept, size, count, or preflight rejection.",
        "name": "onReject",
        "type": "(rejection: UploadRejection) => void"
      },
      {
        "description": "Returning false or rejecting vetoes removal. Accepted removal aborts active upload.",
        "name": "onRemove",
        "type": "(item: UploadFileItem) => boolean | void | Promise<boolean | void>"
      },
      {
        "description": "Preview action callback.",
        "name": "onPreview",
        "type": "(item: UploadFileItem) => void"
      },
      {
        "description": "Download action callback.",
        "name": "onDownload",
        "type": "(item: UploadFileItem) => void"
      },
      {
        "description": "Asynchronously generate a custom thumbnail.",
        "name": "previewFile",
        "type": "(file: File) => Promise<string>"
      },
      {
        "description": "Observe drop events.",
        "name": "onDrop",
        "type": "React.DragEventHandler<HTMLElement>"
      },
      {
        "description": "Show the default file list, default true.",
        "name": "showUploadList",
        "type": "boolean"
      },
      {
        "description": "Customize a file row while preserving its default actions.",
        "name": "itemRender",
        "type": "(node: React.ReactElement, item: UploadFileItem, items: UploadFileItem[], actions: UploadItemActions) => React.ReactNode"
      }
    ],
    "related": [
      "Input (type='file') — never hand-roll a raw file input; use Upload instead. Upload provides drag-drop, preview, upload lifecycle, and soft-delete draft.",
      "Avatar (display-only) — the godx-ui Avatar component renders a user's existing image; use Upload variant='avatar' or 'avatar-crop' when you need the user to change it.",
      "DataTable — unrelated to Upload but both appear together in bulk-import flows: Upload (button variant) triggers the import, DataTable shows the result."
    ],
    "rules": [
      3,
      23
    ],
    "storyPath": "data-entry/upload.tsx",
    "tagline": "Drag-and-drop / button / avatar / picture file uploader in six variants — wire onUpload to your media-service and call collectUploadCommitActions on form submit; supports multipart forms and custom media storage.",
    "usage": [
      "DO provide onUpload to auto-upload on pick. The callback must return { mediaId, previewUrl? } — the component transitions item.status through uploading → done/error automatically. Without onUpload the File object sits in item.file until you manually process it.",
      "DO call collectUploadCommitActions(items) on form submit to get { deleteMediaIds, promoteMediaIds } for your media-service. For multipart endpoints submit File objects via FormData; never submit blob URLs as persisted media.",
      "DO use createUploadItem(file) to build UploadFileItem objects when pre-populating value from server data (e.g. edit forms). Set status='done' and mediaId on existing server media so the draft/undo machinery tracks them correctly.",
      "For native multipart or Inertia Form submissions, set name: staged local files are appended during the formdata event. Completed media uploads use collectUploadCommitActions instead.",
      "Avatar/picture variants (maxCount=1) use internal soft-delete draft logic: removing an item marks it pendingDelete so the user can undo before committing. On form submit, collectUploadCommitActions converts pendingDelete → deleteMediaIds and done mediaIds → promoteMediaIds.",
      "For avatar-crop: a crop dialog opens after pick. The cropped Blob is staged as a new UploadFileItem. The original file never enters the list — only the cropped version is passed to onUpload.",
      "For a drawer or panel listing MIXED attachments (.png beside .json and .txt), keep variant='dropzone' and set listType='picture': the image rows draw their previewUrl as a thumbnail and every other row draws the glyph for its kind, on one row height. Do NOT switch to variant='picture' to get thumbnails — that variant also changes the picker, the default accept to image/* and maxCount to 1."
    ],
    "useCases": [
      "Profile / user avatar editor: use variant='avatar-crop' so users can crop the image before upload; wire onUpload to your media-service; call collectUploadCommitActions on profile form submit to promote or delete.",
      "Invoice / document attachment list: use variant='dropzone' with accept='.pdf,.xlsx' and maxSizeBytes to let accountants drag-drop supporting documents; show the file list with status indicators below the drop zone.",
      "Product gallery (multiple images): use variant='picture-card' with maxCount to display a grid of thumbnails; each item gets an individual remove ✕ button; collectUploadCommitActions on product save.",
      "Single cover-image picker on a content form: use variant='picture' with maxCount=1 to show a preview rectangle with change/remove controls and undo-delete support.",
      "CSV / bulk-import button in an admin table header: use variant='button' with accept='.csv' and custom children label ('Import CSV') to keep the UI compact; process item.file in the onChange handler.",
      "Inline document replacement on an accounting record (replace, not append): use variant='avatar' (single-slot logic) or picture; onUpload returns the new mediaId; collectUploadCommitActions delivers replacesMediaId → deleteMediaIds."
    ]
  },
  {
    "example": "{`import { useState } from \"react\";\nimport { UploadCropDialog } from \"@godxjp/ui/data-entry\"; // internal — prefer Upload variant=\"avatar-crop\" instead\n\nexport function AvatarField() {\n  const [cropFile, setCropFile] = useState<File | null>(null);\n\n  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0] ?? null;\n    setCropFile(file);\n    e.target.value = \"\"; // reset so re-selecting same file fires onChange\n  };\n\n  const handleConfirm = (cropped: File) => {\n    // cropped is always image/jpeg 256×256\n    const form = new FormData();\n    form.append(\"avatar\", cropped);\n    fetch(\"/api/avatar\", { method: \"POST\", body: form });\n  };\n\n  return (\n    <>\n      <input type=\"file\" accept=\"image/*\" onChange={handleFileChange} />\n      <UploadCropDialog\n        open={cropFile !== null}\n        onOpenChange={(open) => { if (!open) setCropFile(null); }}\n        file={cropFile}\n        onConfirm={handleConfirm}\n      />\n    </>\n  );\n}`}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "UploadCropDialog",
    "props": [
      {
        "description": "Controls dialog visibility. Drive this with useState; the component never auto-opens.",
        "name": "open",
        "required": true,
        "type": "boolean"
      },
      {
        "description": "Called when the dialog requests close (Cancel button or overlay click). Set your open state to false here.",
        "name": "onOpenChange",
        "required": true,
        "type": "(open: boolean) => void"
      },
      {
        "description": "The raw File object to crop. An object URL is created internally and revoked on cleanup. Pass null when no file is selected (dialog renders empty).",
        "name": "file",
        "required": true,
        "type": "File | null"
      },
      {
        "description": "Called with the cropped result — always a JPEG File (256x256, quality 0.92) regardless of the original format. The dialog closes itself after calling this.",
        "name": "onConfirm",
        "required": true,
        "type": "(cropped: File) => void"
      }
    ],
    "related": [
      "Upload (variant='avatar-crop') — the preferred way to get crop-on-upload for avatars; it wraps UploadCropDialog automatically. Use UploadCropDialog directly only when you need a custom file-picking trigger or a non-avatar crop flow.",
      "Upload (variant='avatar') — same circular avatar UI without the crop step; the file is used as-is.",
      "Upload (variant='picture') — rectangular single-image upload without crop; no dialog.",
      "Upload (variant='picture-card') — multi-image grid upload without crop."
    ],
    "rules": [
      3,
      13,
      23
    ],
    "storyPath": "data-entry/upload-crop-dialog.tsx",
    "tagline": "Modal crop dialog for a single image file — always controlled (open + file + onConfirm required); do NOT use standalone when Upload variant=\"avatar-crop\" already embeds it.",
    "usage": [
      "DO pass a File object selected by the user (e.g. from an <input type='file'> or drag-drop handler) to `file`; the dialog creates and revokes its own object URL — never create one yourself before passing.",
      "DO close the dialog in onOpenChange: `onOpenChange={(open) => !open && setCropFile(null)}` — always clear cropFile state on close to avoid a stale image on the next open.",
      "DO handle the cropped File in onConfirm and then upload or store it; the output is always image/jpeg 256×256 named `<originalName>.jpg`.",
      "DON'T use UploadCropDialog directly if you are already using `<Upload variant='avatar-crop'>` — that variant already embeds UploadCropDialog internally. Using both will double-mount the dialog.",
      "DON'T try to control the zoom slider from outside — scale state is fully internal; the user controls zoom in-dialog via a Slider (1–2.5×, step 0.05).",
      "DON'T submit the dialog's output File as a form field directly; pass it to your upload handler (e.g. onUpload prop on Upload, or a manual FormData POST), since File objects cannot survive a standard HTML form serialisation."
    ],
    "useCases": [
      "Avatar / profile photo upload flow: show a file picker, pass the chosen File to UploadCropDialog, upload the cropped 256×256 JPEG to the server on confirm.",
      "Admin user management: let admins set or replace a team member's avatar with consistent square crop instead of accepting arbitrary-shaped originals.",
      "Legal-entity logo upload in an accounting app: enforce a square, web-ready JPEG from any source image before storing it as the entity's icon.",
      "Any single-image form field that needs browser-side crop before upload — avoids a round-trip to a server-side image processor.",
      "Building a custom avatar picker UI on top of the godx-ui Upload primitives when the built-in variant='avatar-crop' does not fit your layout."
    ]
  },
  {
    "example": "import { useState } from \"react\";\nimport { ColorPicker, Form, FormField } from \"@godxjp/ui/data-entry\";\n\nexport function BrandColorField() {\n  const [color, setColor] = useState(\"#2563eb\");\n\n  return (\n    <Form>\n      <FormField id=\"brand-color\" label=\"Brand color\">\n        <ColorPicker id=\"brand-color\" value={color} onValueChange={setColor} />\n      </FormField>\n    </Form>\n  );\n}\n\n// Compact swatch-only variant (no hex input)\nexport function SwatchOnly() {\n  const [color, setColor] = useState(\"#16a34a\");\n  return <ColorPicker value={color} onValueChange={setColor} showHexInput={false} />;\n}\n\n// Fixed palette only (gh#1055): tags pick from presets, never free hex\nexport function TagColorField() {\n  const [color, setColor] = useState(\"#2563eb\");\n  return (\n    <Form>\n      <FormField id=\"tag-color\" label=\"Tag colour\">\n        <ColorPicker\n          id=\"tag-color\"\n          value={color}\n          onValueChange={setColor}\n          presets={[{ label: \"Palette\", colors: [\"#dc2626\", \"#16a34a\", \"#2563eb\", \"#9333ea\"] }]}\n          panelRender={(_, { components: { Presets } }) => <Presets />}\n        />\n      </FormField>\n    </Form>\n  );\n}\n\n// Disabled state\nexport function DisabledColor() {\n  return <ColorPicker value=\"#6b7280\" disabled />;\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "ColorPicker",
    "props": [
      {
        "description": "Initial uncontrolled color.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Native form field name.",
        "name": "name",
        "type": "string"
      },
      {
        "defaultValue": "\"#2563eb\"",
        "description": "The current hex color string (3- or 6-digit, with leading #). Component auto-prepends # if missing. Invalid hex values are discarded and the previous valid value is kept.",
        "name": "value",
        "type": "string"
      },
      {
        "defaultValue": "undefined",
        "description": "Called with the normalized, validated hex string whenever the user commits a new color — via the native swatch picker or by pressing Enter / blurring the hex input. Not called for invalid hex drafts.",
        "name": "onValueChange",
        "type": "(hex: string) => void"
      },
      {
        "defaultValue": "undefined",
        "description": "Disables both the swatch input and the hex text input, preventing all user interaction.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "When true (default), renders an editable godx-ui Input alongside the swatch that shows the current hex value and lets the user type a hex string. Set to false for a compact swatch-only control.",
        "name": "showHexInput",
        "type": "boolean"
      },
      {
        "defaultValue": "undefined",
        "description": "Extra CSS class(es) applied to the root wrapper div. Use for layout sizing; avoid overriding design-token colours.",
        "name": "className",
        "type": "string"
      },
      {
        "defaultValue": "undefined",
        "description": "DOM id applied to the hidden native <input type='color'>. Pass the FormField id here so the label's htmlFor targets this control correctly.",
        "name": "id",
        "type": "string"
      },
      {
        "defaultValue": "undefined",
        "description": "antd `presets` (gh#1055) — named groups of fixed hex swatches under the picker. Each group is collapsible (`defaultOpen`, default true) and is an APG radio group: one tab stop, arrow keys move and select, each swatch is named by its hex and the one equal to `value` is checked. An empty `colors` shows the localized 'Empty' line. Hex only (#rgb/#rrggbb) — antd also accepts colour objects and gradients, which this picker has no model for.",
        "name": "presets",
        "type": "{ label: ReactNode; colors: string[]; defaultOpen?: boolean; key?: Key }[]"
      },
      {
        "defaultValue": "undefined",
        "description": "antd `panelRender` — replaces the picker body. `panel` is the default (swatch + hex row, then presets). PRESETS-ONLY mode (a fixed palette, no free hex): `panelRender={(_, { components: { Presets } }) => <Presets />}`. The hidden `name` input always renders outside the panel.",
        "name": "panelRender",
        "type": "(panel: ReactNode, extra: { components: { Picker: FC; Presets: FC } }) => ReactNode"
      }
    ],
    "related": [
      "Input — use for plain text/number entry; use ColorPicker when the value is specifically a color hex code and you want a visual swatch.",
      "Select / SearchSelect — use for choosing from a list of colour NAMES (e.g. 'Red', 'Blue'); for a fixed palette of visible swatches use ColorPicker `presets` (presets-only via `panelRender`)."
    ],
    "rules": [
      2,
      3,
      6,
      13
    ],
    "storyPath": "data-entry/color-picker.tsx",
    "tagline": "Native color-swatch picker with an optional editable hex input — always pass a valid 3- or 6-digit hex `value`; invalid hex is silently ignored and the previous value is restored.",
    "usage": [
      "DO wrap in FormField when a label or validation message is needed — pass the same id to both FormField and ColorPicker so htmlFor wires up correctly: `<FormField id='brand' label='Brand color'><ColorPicker id='brand' value={v} onValueChange={setV} /></FormField>`.",
      "Use it controlled (value + onValueChange) or uncontrolled (defaultValue + onValueChange); both paths commit only validated hex.",
      "DON'T pass an invalid or empty string to value — the component will flash the invalid color on the preview swatch. Always initialize state to a valid 3- or 6-digit hex (e.g. '#2563eb').",
      "The hex Input is a live draft field — onValueChange is NOT called until the user presses Enter or blurs; only then is the value validated and the parent notified. Do not rely on onValueChange firing on every keystroke.",
      "Set showHexInput={false} only for compact/inline contexts (icon pickers, table cells) where space is tight and keyboard hex entry is not needed.",
      "DO use `presets` when the value must come from a fixed palette (tag / label colours) — add `panelRender={(_, { components: { Presets } }) => <Presets />}` to remove free hex entry entirely. NEVER rebuild a palette from a ToggleGroup of swatches.",
      "NEVER hand-roll a color picker with raw <input type='color'> — always use this component; it normalizes hex, debounces draft state, and respects the design-token control styles."
    ],
    "useCases": [
      "Brand / campaign color selection in settings or campaign creation forms where users need to pick or type an exact hex value.",
      "Invoice or document theme customization — letting users pick accent colors that are stored and applied to PDF output.",
      "Accounting dashboard category tagging — assigning a color code to GL account categories or chart-of-accounts nodes for visual grouping.",
      "Product or inventory label colors in admin panels where a compact swatch (showHexInput={false}) fits inside a table cell or sidebar.",
      "Design-token or CSS variable editor pages where the user needs precise hex entry alongside a visual swatch preview.",
      "User-profile or team avatar color personalization forms."
    ]
  },
  {
    "example": "{`import { FormField, Slider } from \"@godxjp/ui/data-entry\";\nimport { useState } from \"react\";\n\n// Money in a label goes through Intl, never a hand-written symbol: the symbol, the grouping and\n// the number of minor units all change with the locale and the currency.\nconst money = new Intl.NumberFormat(\"ja-JP\", { style: \"currency\", currency: \"JPY\" });\n\n// Single thumb, antd spelling: a plain number in, a plain number out.\nfunction VolumeSlider() {\n  const [volume, setVolume] = useState(70);\n  return (\n    <FormField id=\"volume\" label={`音量: ${volume}%`}>\n      <Slider\n        id=\"volume\"\n        value={volume}\n        onChange={setVolume}\n        tooltip={{ formatter: (value) => `${value}%` }}\n      />\n    </FormField>\n  );\n}\n\n// Range: onChange carries both thumbs, onChangeComplete fires once on release.\nfunction PriceRangeSlider({ onApply }: { onApply: (range: number[]) => void }) {\n  const [price, setPrice] = useState([2000, 8000]);\n  return (\n    <FormField\n      id=\"price-range\"\n      label={`価格: ${money.format(price[0])} 〜 ${money.format(price[1])}`}\n    >\n      <Slider\n        id=\"price-range\"\n        range\n        value={price}\n        onChange={setPrice}\n        onChangeComplete={onApply}\n        min={0}\n        max={10000}\n        step={500}\n        minStepsBetweenThumbs={1}\n      />\n    </FormField>\n  );\n}\n\n// Marks-only: the thumb can rest on a named point and nowhere else.\nfunction PlanSlider() {\n  return (\n    <FormField id=\"plan\" label=\"料金プラン\">\n      <Slider\n        id=\"plan\"\n        step={null}\n        marks={{ 0: \"無料\", 30: \"標準\", 70: \"上位\", 100: \"特別\" }}\n        defaultValue={30}\n        dots\n      />\n    </FormField>\n  );\n}`}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Slider",
    "props": [
      {
        "description": "A plain number for one thumb (antd), or an array. An array with no `range` still draws a thumb per entry — the Radix-era inference — and with nothing given the slider is ONE thumb at `min`.",
        "name": "value / defaultValue",
        "type": "number | number[]"
      },
      {
        "description": "antd `onChange` — every change. The payload follows `range`: a number without it, an array with it. A `range` held in a boolean variable cannot say which at compile time, so that spelling takes `onValueChange` instead.",
        "name": "onChange",
        "type": "(value: number) => void | (value: number[]) => void"
      },
      {
        "description": "antd `onChangeComplete` — once per finished gesture (pointer release, key press), and only when the value actually changed. Use it for the expensive work: a refetch, a save.",
        "name": "onChangeComplete",
        "type": "(value: number) => void | (value: number[]) => void"
      },
      {
        "description": "The Radix-era callback, valid in every spelling: every change, always as the full array of thumbs.",
        "name": "onValueChange",
        "type": "(value: number[]) => void"
      },
      {
        "description": "`onChangeComplete`'s array form. Fires exactly once per gesture.",
        "name": "onValueCommit",
        "type": "(value: number[]) => void"
      },
      {
        "defaultValue": "false",
        "description": "Two or more thumbs bounding a span. A DECLARATION, not an inference: a range whose value is still loading spans [min, max] instead of rendering as a point. The object form is antd's — `editable` lets a press on the rail ADD a thumb and Delete/Backspace remove one (between `minCount` and `maxCount`), `draggableTrack` drags the painted span and every thumb with it. `editable` wins over `draggableTrack` and switches off while any thumb is disabled, exactly as antd.",
        "name": "range",
        "type": "boolean | { editable?: boolean; minCount?: number; maxCount?: number; draggableTrack?: boolean }"
      },
      {
        "defaultValue": "0",
        "description": "The low end of the scale.",
        "name": "min",
        "type": "number"
      },
      {
        "defaultValue": "100",
        "description": "The high end of the scale.",
        "name": "max",
        "type": "number"
      },
      {
        "defaultValue": "1",
        "description": "Granularity. `null` is antd's marks-only mode: a thumb may then rest only on a mark, on `min` or on `max` — by pointer (nearest) and by arrow key (the next mark along).",
        "name": "step",
        "type": "number | null"
      },
      {
        "defaultValue": "0",
        "description": "The fewest steps two neighbouring thumbs may be apart. The Radix-era prop; it has no antd equivalent.",
        "name": "minStepsBetweenThumbs",
        "type": "number"
      },
      {
        "description": "antd `marks` — labelled ticks along the rail, keyed by the value each sits on. A press on a mark's label moves the thumb to exactly that value. Prefer styling the label node (it is a ReactNode) over the object form's `style`.",
        "name": "marks",
        "type": "Record<number, ReactNode | { label?: ReactNode; style?: CSSProperties }>"
      },
      {
        "defaultValue": "false",
        "description": "antd `dots` — a tick at every `step`, or at every mark when `step={null}`. Ticks inside the painted span read as active.",
        "name": "dots",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "antd `included` — whether the painted span runs from the start to the thumb. `false` for a rail that only holds marks, where a filled span would assert a magnitude that is not there.",
        "name": "included",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd `tooltip` — the value bubble, rendered INSIDE the thumb so it rides every drag with no repositioning code. `open` forces it on or off, `formatter: null` removes it, `autoAdjustOverflow` (default true) flips it to the opposite side when the viewport leaves no room. A string/number `formatter` result is ALSO the thumb's `aria-valuetext`, so a ¥ or % slider is announced the way it is painted. `placement` `left`/`right` are the INLINE sides and mirror under RTL. antd's `getPopupContainer` has no counterpart: nothing is portalled.",
        "name": "tooltip",
        "type": "boolean | { open?: boolean; placement?: 'top' | 'bottom' | 'left' | 'right'; autoAdjustOverflow?: boolean; formatter?: ((value: number) => ReactNode) | null }"
      },
      {
        "defaultValue": "false",
        "description": "The whole slider, or one thumb each (antd's array form): `[true, false]` locks the first thumb of a range. A disabled thumb leaves the tab order and does not submit.",
        "name": "disabled",
        "type": "boolean | boolean[]"
      },
      {
        "defaultValue": "'horizontal'",
        "description": "Layout axis. Wins over `vertical` when both are given, as in antd 6. A vertical slider fills its container's height (and never collapses below --slider-vertical-min-block-size).",
        "name": "orientation",
        "type": "'horizontal' | 'vertical'"
      },
      {
        "defaultValue": "false",
        "description": "antd's boolean spelling of `orientation=\"vertical\"`.",
        "name": "vertical",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd `reverse` — run the scale the other way: `min` at the inline end of a horizontal rail, at the top of a vertical one. The arrow keys turn over with it.",
        "name": "reverse",
        "type": "boolean"
      },
      {
        "description": "The Radix-era name for `reverse`. When both are given, `inverted` wins.",
        "name": "inverted",
        "type": "boolean"
      },
      {
        "description": "Reading direction of a horizontal slider — which end is `min`, and which arrow increases. Defaults to the locale's direction (the I18nProvider AppProvider sets), so pass it only to pin one slider against its page.",
        "name": "dir",
        "type": "'ltr' | 'rtl'"
      },
      {
        "description": "Native form field name. One thumb submits `name`, several submit `name[]` — one entry per thumb, no extra wiring.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Associate the thumbs' inputs with a form elsewhere in the document, by id.",
        "name": "form",
        "type": "string"
      }
    ],
    "related": [
      "Progress — use Progress (not a disabled Slider) to show read-only progress; Slider with disabled is semantically a control, not a status indicator.",
      "Input (type number) / NumberInput — free-form numeric entry; Slider is for a bounded range where dragging is the expected gesture.",
      "Switch — for boolean on/off; Slider is for continuous or stepped numeric ranges.",
      "Rating — for a small ordinal scale with symbols (1–5 stars) instead of a rail."
    ],
    "rules": [
      3,
      6,
      31
    ],
    "storyPath": "data-entry/slider.tsx",
    "tagline": "Numeric slider on react-aria-components with the antd 6 API — one thumb over a point, or `range` for a span. `value` takes a plain number (antd) or a number[] (the v22-era array spelling); both still work.",
    "usage": [
      "DO: Use `onChange` for live UI and `onChangeComplete` for the expensive work — a refetch, a save. Commit fires once per gesture, and only when the value moved.",
      "DO: Declare `range` when you want two thumbs. Leaving it to an array's length makes a range that is still loading render as a single point, and `onChange` then reports a number rather than a pair.",
      "DO: Give `tooltip.formatter` to a slider whose number is not a bare count (¥, %, 件) — it formats the bubble AND becomes the spoken value, so a screen reader stops reading `50000`.",
      "DO: Set `name` inside a native `<form>`; the slider submits without a form library.",
      "DON'T: Reach for `step={null}` without `marks` — it leaves `min` and `max` as the only values a thumb can take.",
      "DON'T: Hand-roll Track/Range/Thumb sub-parts — `<Slider />` is one leaf element and composes them itself.",
      "DO: For a11y supply `aria-label` (or `aria-labelledby`, or a FormField label) — it names every thumb, with an index suffix once there is more than one."
    ],
    "useCases": [
      "Budget / price-range filter: `range` with `onChangeComplete` so the list refetches on release, not on every pixel.",
      "Single numeric setting: volume, zoom, a threshold — `defaultValue={50}` with a live readout beside it.",
      "A scale whose valid points are named rather than regular: `marks` + `step={null}` (料金プラン, 温度帯, 段階評価).",
      "Percentage allocation with `step={5}`, `dots` and `marks` at the decision points.",
      "A tiered picker a user can shape: `range={{ editable: true, minCount: 1, maxCount: 4 }}` for splitting a scale into bands.",
      "Read-only visual indicator — `disabled` with a controlled value; prefer Progress when nothing is ever adjustable."
    ]
  },
  {
    "example": "import { useState } from \"react\";\nimport { Calendar } from \"@godxjp/ui/data-entry\";\nimport { ja } from \"react-day-picker/locale\";\n\n// --- Single-date example (controlled) ---\nexport function InvoiceDateCalendar() {\n  const [date, setDate] = useState<Date | undefined>(new Date());\n\n  return (\n    <Calendar\n      mode=\"single\"\n      selected={date}\n      onSelect={setDate}\n      locale={ja}\n      disabled={{ before: new Date(2024, 0, 1) }}\n      footer={date ? `選択日: ${date.toLocaleDateString(\"ja-JP\")}` : \"日付を選択してください\"}\n      aria-label=\"発行日カレンダー\"\n    />\n  );\n}\n\n// --- Range example inside a Popover (mirrors DatePicker range internals) ---\nimport { Popover, PopoverContent, PopoverTrigger } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport type { DateRange } from \"react-day-picker\";\n\nexport function ReportRangeFilter() {\n  const [range, setRange] = useState<DateRange | undefined>();\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\">期間を選択</Button>\n      </PopoverTrigger>\n      {/* flush drops the panel's padding and width=\"auto\" lets the calendar set the measure —\n          the panel's own 18rem would clip two months. Never a w-auto/p-0 utility on className:\n          those are per-call-site constants no service theme can retune. */}\n      <PopoverContent flush width=\"auto\" align=\"end\">\n        <Calendar\n          mode=\"range\"\n          selected={range}\n          onSelect={setRange}\n          locale={ja}\n          numberOfMonths={2}\n          disabled={{ after: new Date() }}\n          autoFocus\n        />\n      </PopoverContent>\n    </Popover>\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Calendar",
    "props": [
      {
        "description": "OVERRIDE only — omit it and the calendar follows AppProvider, like every other component here. It did not always: `locale` used to arrive solely through the react-day-picker spread, so with nothing passed the library's own en-US default won and a Japanese page rendered Su Mo Tu We Th Fr Sa inside an otherwise Japanese card, with no error and no warning. Pass it to PIN one market (a booking screen that must stay Japanese wherever it is opened); the locale also decides which weekday the grid starts on, not only the labels.",
        "name": "locale",
        "type": "DayPickerLocale"
      },
      {
        "description": "Decorate a day cell — 祝日, a booked day, a deadline. It WRAPS the library's own day button rather than replacing it: `originNode` already carries the selection state, `aria-selected`, the disabled handling and its place in the grid's roving tabindex. Write `<>{originNode}<Badge …/></>` — decorate, never rebuild, or every marker re-derives all of that and most get it wrong.",
        "name": "cellRender",
        "type": "(date: Date, info: { originNode: ReactNode }) => ReactNode"
      },
      {
        "defaultValue": "true",
        "description": "Rule the grid: one line between every pair of days, weekday header included (the header row is also tinted --muted). ON BY DEFAULT — with no ruling the month read as a cloud of numbers and was reported as very hard to read. Pass `bordered={false}` for floating day buttons. It is NOT a box around the calendar — that is Card's job, and the Card/popover edge is the OUTER FRAME tier. The INSIDE grid is the `--calendar-grid-border-color` knob, default `hsl(var(--border))` (L* 93.80 / 1.149:1 on the popover in light, L* 20.76 / 1.270:1 in dark) — the same tier as the frame around it, as a DataTable row rule and as the RangeTimeline grid, so the ruling can never out-weigh the surface it is drawn on. It was `hsl(var(--input) / 0.5)` (L* 78.67 / 1.738:1) through 27.6.0 and gh#730 reported that still darker than the --border every card and table uses. Retint with `--calendar-grid-border-color: hsl(var(--input) / 0.5)` for the 27.6.0 weight or `hsl(var(--input))` for the heavy line. DatePicker forwards the same prop to its popup calendar.",
        "name": "bordered",
        "type": "boolean"
      },
      {
        "defaultValue": "\"auto\"",
        "description": "How the grid claims horizontal space. `auto` (default) shrink-wraps to seven fixed day columns — the shape a picker popover needs, because the panel is shrink-to-fit and takes ITS width from the calendar inside it. Use `full` for an EMBEDDED calendar (a shift board, a booking month) that is the content of a Card rather than a dropdown: it stacks the months and lets the day cells share the row, each reading from the start edge with an inset (`--calendar-full-day-padding-inline` / `-block`, default --space-2 / --space-1, so the numeral clears the `text-edge-inset` audit; gh#1258). Measured at a 1200px container: auto → root 248px / cells 32px; full → root 1200px / cells 168px; the DatePicker popover stays 250px either way. Do NOT reach for `className=\"w-full\"` instead — it widens the root and leaves the grid at 224px pinned left.",
        "name": "width",
        "type": "\"auto\" | \"full\""
      },
      {
        "defaultValue": "false",
        "description": "Footer action that jumps to the current month and selects today (single), fills the open end of the range (range) or adds today (multiple). Disabled when today is outside startMonth / endMonth or matches `disabled`.",
        "name": "showToday",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Footer action that calls `onClose`.",
        "name": "showClose",
        "type": "boolean"
      },
      {
        "description": "Called by the Close footer action.",
        "name": "onClose",
        "type": "() => void"
      },
      {
        "description": "Selection mode. 'single' picks one day, 'multiple' picks several, 'range' picks a from/to span. Omit or set undefined for a display-only calendar with no selection.",
        "name": "mode",
        "type": "'single' | 'multiple' | 'range' | undefined"
      },
      {
        "description": "Controlled selected value. Shape depends on mode: Date for 'single', Date[] for 'multiple', { from?: Date; to?: Date } for 'range'.",
        "name": "selected",
        "type": "Date | Date[] | DateRange | undefined"
      },
      {
        "description": "Callback fired when the user clicks a day. Receives the new selection, the clicked date, active modifiers, and the event.",
        "name": "onSelect",
        "type": "(date: Date | Date[] | DateRange | undefined, triggerDate: Date, modifiers: Modifiers, e: MouseEvent) => void"
      },
      {
        "description": "Uncontrolled initial month shown. For controlled month navigation use month + onMonthChange.",
        "name": "defaultMonth",
        "type": "Date"
      },
      {
        "description": "Controlled currently-displayed month. Pair with onMonthChange to drive navigation programmatically.",
        "name": "month",
        "type": "Date"
      },
      {
        "description": "Fired when the user navigates to a different month.",
        "name": "onMonthChange",
        "type": "(month: Date) => void"
      },
      {
        "defaultValue": "1",
        "description": "Number of month grids to show side-by-side.",
        "name": "numberOfMonths",
        "type": "number"
      },
      {
        "description": "Earliest month reachable via navigation. Also constrains the dropdown range when captionLayout includes 'dropdown'.",
        "name": "startMonth",
        "type": "Date | undefined"
      },
      {
        "description": "Latest month reachable via navigation.",
        "name": "endMonth",
        "type": "Date | undefined"
      },
      {
        "description": "Days to disable. Accepts a Date, Date[], { before: Date }, { after: Date }, { from: Date; to: Date }, { dayOfWeek: number[] }, or an array of any of these.",
        "name": "disabled",
        "type": "Matcher | Matcher[] | undefined"
      },
      {
        "description": "Days to hide entirely from the grid.",
        "name": "hidden",
        "type": "Matcher | Matcher[] | undefined"
      },
      {
        "defaultValue": "true",
        "description": "Show greyed-out days from adjacent months. godx-ui defaults this to true (overrides react-day-picker's false default).",
        "name": "showOutsideDays",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Show ISO/locale week-number column on the left.",
        "name": "showWeekNumber",
        "type": "boolean"
      },
      {
        "description": "Always render 6 weeks per month, padding with days from the next month.",
        "name": "fixedWeeks",
        "type": "boolean"
      },
      {
        "defaultValue": "'label'",
        "description": "Caption area layout. 'dropdown' shows month/year select dropdowns for faster large-range navigation.",
        "name": "captionLayout",
        "type": "'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years'"
      },
      {
        "description": "Day index (0=Sun) for the first column of the week grid. Overrides locale default.",
        "name": "weekStartsOn",
        "type": "0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined"
      },
      {
        "description": "Custom named modifiers applied to matched days. Pair with modifiersClassNames or modifiersStyles to style them.",
        "name": "modifiers",
        "type": "Record<string, Matcher | Matcher[] | undefined>"
      },
      {
        "description": "CSS class names keyed by modifier name.",
        "name": "modifiersClassNames",
        "type": "ModifiersClassNames"
      },
      {
        "description": "Content rendered below the grid as a live ARIA region. Use a string to communicate selection status to screen readers. Set it to replace the built-in showToday / showClose actions.",
        "name": "footer",
        "type": "React.ReactNode | string"
      },
      {
        "description": "Focus the first selected day (or today) when the calendar mounts — recommended when opening inside a Popover.",
        "name": "autoFocus",
        "type": "boolean"
      },
      {
        "description": "Override the 'today' date used for the today modifier and default navigation. Defaults to new Date().",
        "name": "today",
        "type": "Date"
      },
      {
        "description": "Extra class added to the root wrapper element (adds to the built-in p-3 padding).",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Override individual part class names (months, weekday, day, selected, today, range_start, range_end, range_middle, disabled, outside, etc.). Merged with godx-ui defaults.",
        "name": "classNames",
        "type": "Partial<ClassNames>"
      },
      {
        "description": "Swap out internal sub-components (Chevron is already replaced by lucide icons). Use for advanced custom rendering.",
        "name": "components",
        "type": "Partial<CustomComponents>"
      },
      {
        "description": "Animate month-to-month navigation transitions (react-day-picker >=9.6).",
        "name": "animate",
        "type": "boolean"
      },
      {
        "description": "'ltr' (default) or 'rtl' for right-to-left layouts.",
        "name": "dir",
        "type": "string"
      },
      {
        "description": "aria-label on the container. Provide a meaningful label when the calendar is not described by a visible heading.",
        "name": "aria-label",
        "type": "string"
      },
      {
        "description": "ARIA role for the container element. Use 'dialog' when rendering inside a modal Popover.",
        "name": "role",
        "type": "'application' | 'dialog' | undefined"
      },
      {
        "description": "When true the user cannot deselect the currently selected day (mode must also be set).",
        "name": "required",
        "type": "boolean | undefined"
      },
      {
        "description": "When numberOfMonths > 1, advance all visible months at once instead of one at a time.",
        "name": "pagedNavigation",
        "type": "boolean"
      },
      {
        "description": "Render months newest-first (right-to-left reading order) when numberOfMonths > 1.",
        "name": "reverseMonths",
        "type": "boolean"
      },
      {
        "description": "Hide the prev/next navigation buttons without disabling keyboard navigation.",
        "name": "hideNavigation",
        "type": "boolean"
      },
      {
        "description": "Disable month navigation entirely (buttons hidden and keyboard navigation locked).",
        "name": "disableNavigation",
        "type": "boolean"
      },
      {
        "description": "Hide the row of weekday abbreviation headers (Mon, Tue, ...).",
        "name": "hideWeekdays",
        "type": "boolean"
      },
      {
        "description": "Use ISO week numbering (Monday first week, ignores weekStartsOn and firstWeekContainsDate).",
        "name": "ISOWeek",
        "type": "boolean"
      }
    ],
    "related": [
      "DatePicker — the complete single-date form control (typeable ISO input + calendar icon + Popover). Use DatePicker instead of Calendar when you need a form-submittable field with an input box.",
      "DatePicker — the complete date form control (typeable ISO input + calendar icon + Popover); add `range` for the two-input from/to pair. Use it instead of Calendar when you need form fields.",
      "Popover / PopoverContent — the shell you must provide when you want Calendar inside a trigger. Pass `flush` and `width=\"auto\"` so the panel neither double-pads nor clips the grid."
    ],
    "rules": [
      3,
      5,
      6,
      23
    ],
    "storyPath": "data-entry/calendar.tsx",
    "subParts": [
      "DayButton"
    ],
    "tagline": "A styled react-day-picker grid for picking single dates, multiple dates, or date ranges — always embed it inside a Popover for full date-picker UX; use DatePicker (add `range` for a from/to pair) instead when you need a form-submittable input. `components={{ DayButton }}` is the documented seam for marking a day (a shift, a price, a badge): import `DayButton` and its types from `@godxjp/ui/data-entry` rather than from react-day-picker, so the type is this package's copy and not a second one (gh#797).",
    "usage": [
      "DO set mode explicitly ('single', 'multiple', 'range') — omitting it renders a display-only grid with no selection. The value passed to selected and the argument shape of onSelect both depend on mode.",
      "DO embed Calendar inside a Popover + PopoverContent when building a date-picker UI — `<PopoverContent flush width=\"auto\">`. `flush` drops the panel padding (Calendar brings its own) and `width=\"auto\"` lets the calendar set the measure, which the panel's own 18rem would otherwise clip at two months. For form-submittable single-date or range inputs prefer the higher-level DatePicker (with `range` for a from/to pair) — it owns the input, icon, locale wiring, and ISO form submission natively.",
      "DO pass a locale object imported from 'react-day-picker/locale' (e.g. import { ja } from 'react-day-picker/locale') for i18n — weekday names, month names, and first-day-of-week all come from the locale.",
      "DO use the disabled prop with Matcher objects ({ before: minDate }, { after: maxDate }, { dayOfWeek: [0, 6] }) to restrict selectable days — never render your own disabled overlay on top.",
      "DON'T zero the panel padding or pin its width with utilities on className. Those are per-call-site constants no service theme can retune; `flush` and `width=\"auto\"` set the same two tokens the panel already reads.",
      "DON'T hand-roll a calendar grid — Calendar wraps react-day-picker which is keyboard-navigable, ARIA-annotated, and screen-reader friendly out of the box. Provide a footer string for screen-reader status announcements when the selection changes.",
      "The month frame is two knobs, not a stack of them (gh#730). `--calendar-space-inset` is the ONE inset the caption row, the `‹`/`›` nav buttons and the day grid all sit at — `--calendar-nav-space-inline` is declared as that same value, because the nav is absolutely positioned and would otherwise get its own (measured 4px against the grid's 12px, so the header read wider than the body). `--calendar-grid-space-block-start` is the ONE caption→weekday-row step (space-3, 12px; it used to stack with the month column gap for 32px). Retune those tokens rather than adding padding on the call site."
    ],
    "useCases": [
      "Inline date picker within a form section where the calendar grid must always be visible (e.g., a booking page or a date-of-issue field on an invoice creation form).",
      "Date range selection inside a Popover triggered by a filter button on an accounting report page (use mode='range', pass selected={dateRange}, onSelect updates the filter state).",
      "Multi-date selection for picking recurring reminder dates or batch-action target dates (mode='multiple').",
      "Custom calendar with highlighted days (e.g., marking invoice due dates or shipment ETDs with a custom modifier + modifiersClassNames) overlaid on a standard single-select grid.",
      "Month navigator with year/month dropdowns for jumping to a historical accounting period quickly (captionLayout='dropdown', startMonth set to earliest fiscal year).",
      "Display-only calendar (no mode set) showing booked or blocked dates using the modifiers prop with read-only styling, embedded in a dashboard card."
    ]
  },
  {
    "example": "{`import {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@godxjp/ui/data-entry\";\n\nfunction AccountQuickPick({ onSelect }: { onSelect: (id: string) => void }) {\n  return (\n    <Command label=\"Quick pick account\" loop>\n      <CommandInput placeholder=\"Search accounts…\" />\n      <CommandList>\n        <CommandEmpty>No accounts found.</CommandEmpty>\n        <CommandGroup heading=\"Revenue\">\n          <CommandItem value=\"4001\" onSelect={onSelect}>\n            Sales Revenue\n          </CommandItem>\n          <CommandItem value=\"4002\" onSelect={onSelect}>\n            Service Revenue\n          </CommandItem>\n        </CommandGroup>\n        <CommandGroup heading=\"Expenses\">\n          <CommandItem value=\"6001\" onSelect={onSelect}>\n            Rent Expense\n          </CommandItem>\n          <CommandItem value=\"6002\" disabled onSelect={onSelect}>\n            Deprecated Account\n          </CommandItem>\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  );\n}`}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Command",
    "props": [
      {
        "description": "Accessible label for the command menu. Not shown visually — used by screen readers.",
        "name": "label",
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Set to false to disable automatic filtering and sorting. When false, you must conditionally render matching items yourself based on the search query.",
        "name": "shouldFilter",
        "type": "boolean"
      },
      {
        "description": "Custom filter function. Returns a score between 0 (hidden) and 1 (best match). Defaults to command-score library scoring.",
        "name": "filter",
        "type": "(value: string, search: string, keywords?: string[]) => number"
      },
      {
        "description": "Default selected item value on initial render (uncontrolled).",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Controlled state of the currently selected item value.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Called when the selected item changes.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "defaultValue": "false",
        "description": "When true, keyboard arrow-key navigation wraps from last item back to first and vice versa.",
        "name": "loop",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "When true, pointer events cannot select items — keyboard only.",
        "name": "disablePointerSelection",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Set to false to disable ctrl+n/j/p/k vim-style navigation shortcuts.",
        "name": "vimBindings",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd List `split`. Draws the list as ONE ruled box: group/list padding 0, rows full-bleed to the panel edge with a square highlight, a hairline border-block-end between rows and none after the last visible row, row content aligned with CommandInput's leading glyph. Sets `data-split` on the root; the parts need no prop. Tokens: --command-list-split-padding, --command-list-split-inset, --command-item-divider-width, --command-item-divider-color. Select does NOT render its popup split (it is a picker; compact unruled rows).",
        "name": "split",
        "type": "boolean"
      },
      {
        "description": "Additional CSS classes merged onto the root div via cn().",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Controlled search string for the input.",
        "name": "CommandInput.value",
        "type": "string"
      },
      {
        "description": "Called when the search input text changes.",
        "name": "CommandInput.onValueChange",
        "type": "(search: string) => void"
      },
      {
        "description": "Accessible label for the list of suggestions. Not shown visually.",
        "name": "CommandList.label",
        "type": "string"
      },
      {
        "description": "Unique value for this item. If omitted, inferred from children textContent — must be stable; provide explicitly when text changes between renders.",
        "name": "CommandItem.value",
        "type": "string"
      },
      {
        "description": "Called when this item is selected via click or keyboard.",
        "name": "CommandItem.onSelect",
        "type": "(value: string) => void"
      },
      {
        "defaultValue": "false",
        "description": "Prevents this item from being selected.",
        "name": "CommandItem.disabled",
        "type": "boolean"
      },
      {
        "description": "Additional keywords matched during filtering that are not part of the visible label.",
        "name": "CommandItem.keywords",
        "type": "string[]"
      },
      {
        "defaultValue": "false",
        "description": "When true, renders this item regardless of filtering results.",
        "name": "CommandItem.forceMount",
        "type": "boolean"
      },
      {
        "description": "Visible heading rendered above items in this group.",
        "name": "CommandGroup.heading",
        "type": "React.ReactNode"
      },
      {
        "description": "Required unique identifier for the group when no heading is provided.",
        "name": "CommandGroup.value",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "When true, renders this group regardless of filtering.",
        "name": "CommandGroup.forceMount",
        "type": "boolean"
      },
      {
        "description": "Renders automatically only when there are no matching results. Place inside CommandList.",
        "name": "CommandEmpty",
        "type": "React.ReactNode (children)"
      },
      {
        "defaultValue": "true",
        "description": "CommandSeparator is cmdk's Command.Separator (data-slot=\"command-separator\", ref forwarded, className merged): a hairline between two groups of one list. Default true — the OPPOSITE of cmdk, which drops the separator once the search is non-empty; a `+ Create \"<q>\"` group only exists while the user types, so the rule above it must stay. Pass false for cmdk's palette behaviour. Hidden automatically beside a group cmdk has hidden (no matching rows) and under `split` (which already rules group boundaries). aria-hidden: decorative, since a separator is not an allowed listbox child. Tokens: --command-item-divider-width / --command-item-divider-color (→ --stroke-hairline / --border), margin-block --command-group-padding.",
        "name": "CommandSeparator.alwaysRender",
        "type": "boolean"
      }
    ],
    "related": [
      "Select — `showSearch` / `loadOptions` is the fully managed (async) combobox built on Command + Popover; use it instead of building your own with Command.",
      "Select — simple dropdown for static option lists without type-to-filter; use Select when there are fewer than ~10 options and no search is needed.",
      "SearchInput — standalone text input with a search icon; use SearchInput for filtering visible page content (tables, lists) not for selecting from a command menu.",
      "CommandInput (sub-part) — the styled search input that only works inside a Command root; never use it alone as a general search field."
    ],
    "rules": [
      2,
      3,
      6,
      23
    ],
    "storyPath": "data-entry/command.tsx",
    "subParts": [
      "CommandEmpty",
      "CommandGroup",
      "CommandInput",
      "CommandItem",
      "CommandList",
      "CommandSeparator"
    ],
    "tagline": "Accessible, keyboard-navigable command palette / combobox list built on cmdk — always pair CommandInput inside its own wrapper div, never render items outside CommandList.",
    "usage": [
      "DO compose the full tree: Command > CommandInput + CommandList > (CommandEmpty | CommandGroup > CommandItem | CommandItem). Every interactive element must live inside CommandList; items outside it are invisible to the keyboard engine.",
      "DO set shouldFilter={false} and manage filtering yourself when the options list comes from a server/async source (e.g. Select `loadOptions`). With shouldFilter=true the default client-side scoring runs over all rendered items automatically.",
      "DO always provide a stable explicit value prop on CommandItem when the item's text content can change between renders — relying on inferred textContent with dynamic labels causes selection bugs.",
      "DO include CommandEmpty inside CommandList to show a no-results message. It renders automatically only when the filtered count is zero; do not conditionally render it yourself.",
      "DO use split for checkbox/option lists (a filter facet, a picker: Popover + PopoverContent flush + Command split) — rows get a divider between them and 0 list padding, so they read as one box. Keep the default (unsplit) for a command palette. Never re-create the look with page CSS (border on items, padding 0 on the group): retune --command-item-divider-color / --command-item-divider-width instead.",
      "DO divide a result group from an action group with CommandSeparator — matches, then `<CommandSeparator />`, then `<CommandGroup forceMount><CommandItem forceMount value={`__create__${q}`}>+ Create \"{q}\"</CommandItem></CommandGroup>` rendered only while the query has no exact match. Give the create item an explicit value (never inferred from its changing text) and forceMount both it and its group so the filter cannot hide it. Never keep a local copy of the Command family for the divider — a compound must come from one cmdk instance.",
      "DON'T use CommandInput as a standalone search input — it is only meaningful inside a Command root (the root manages shared filter state). For a standalone search field use SearchInput instead.",
      "DON'T hand-roll keyboard navigation on a list of items; Command handles arrow keys, Enter, Escape, Home/End, and vim bindings. Adding your own keyDown handlers on top creates conflicts — use onSelect on CommandItem for selection logic."
    ],
    "useCases": [
      "Command palette / global action launcher (Cmd+K menu): wraps Command + CommandInput + grouped CommandItems for quick navigation across pages or actions.",
      "Popover-based combobox with server-side search: Command with shouldFilter={false} inside a Popover, managing the query state externally and filtering options before rendering — this is exactly how Select `showSearch` is built internally.",
      "Tree/cascader search panel: inject Command + CommandInput as a search header above a custom scroll area (no CommandList needed for the tree body) to get a styled, accessible search input — as Cascader and TreeSelect do.",
      "Multi-group option picker: use CommandGroup with heading to visually separate option categories (e.g. 'Accounts', 'Contacts', 'Documents') inside one dropdown with a single search box.",
      "Keyboard-first admin shortcut bar: embed Command with loop={true} and vimBindings={true} in a persistent sidebar for power-user keyboard navigation without page reloads."
    ]
  },
  {
    "example": "import { CheckboxGroup } from \"@godxjp/ui/data-entry\";\nimport { useState } from \"react\";\n\nconst PERMISSIONS = [\n  { label: \"View invoices\", value: \"invoices:read\" },\n  { label: \"Create invoices\", value: \"invoices:write\", description: \"Includes editing and deleting\" },\n  { label: \"Manage users\", value: \"users:manage\", disabled: true },\n];\n\n// Uncontrolled — use defaultValue\nexport function PermissionsForm() {\n  return (\n    <form method=\"post\">\n      <CheckboxGroup\n        name=\"permissions\"\n        options={PERMISSIONS}\n        defaultValue={[\"invoices:read\"]}\n        orientation=\"vertical\"\n      />\n    </form>\n  );\n}\n\n// Controlled — use value + onChange\nexport function ControlledExample() {\n  const [selected, setSelected] = useState<string[]>([\"invoices:read\"]);\n  return (\n    <CheckboxGroup\n      name=\"permissions\"\n      options={PERMISSIONS}\n      value={selected}\n      onValueChange={setSelected}\n    />\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "CheckboxGroup",
    "props": [
      {
        "description": "Id của nhóm; `FormField` tự truyền xuống để nối nhãn ↔ control.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Data-driven mode: array of { label, value, disabled?, description? }. When provided and non-empty, renders all checkboxes automatically. When omitted, renders `children` instead.",
        "name": "options",
        "type": "ChoiceOptionProp[]"
      },
      {
        "description": "Controlled selected values. When provided, the component is controlled and won't manage state internally — you must handle onChange to update it.",
        "name": "value",
        "type": "string[]"
      },
      {
        "defaultValue": "[]",
        "description": "Uncontrolled initial selection. Use this instead of `value` when you don't need to control state externally.",
        "name": "defaultValue",
        "type": "string[]"
      },
      {
        "description": "Called with the full updated selection array whenever any checkbox is toggled. Works in both controlled and uncontrolled modes.",
        "name": "onChange",
        "type": "(value: string[]) => void"
      },
      {
        "defaultValue": "\"vertical\"",
        "description": "Layout direction of the checkbox items. Vertical stacks items; horizontal places them in a row.",
        "name": "orientation",
        "type": "\"vertical\" | \"horizontal\""
      },
      {
        "description": "Disables the entire group. Individual options can also set their own disabled flag via ChoiceOptionProp.disabled.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "HTML form name applied to each underlying checkbox input. Required for native form submission — all checkboxes share the same name so the form collects multiple values.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Extra CSS class on the group container div.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Manual children mode: used when `options` is omitted or empty. Render Checkbox items directly as children. You are responsible for composing each Checkbox with a Field for correct label/description layout.",
        "name": "children",
        "type": "React.ReactNode"
      },
      {
        "description": "Fires with the checked values array.",
        "name": "onValueChange",
        "type": "(value: string[]) => void"
      },
      {
        "description": "`options` mode only. Renders a leading select-all box whose state is derived from the value — checked when every enabled option is selected, `mixed` (indeterminate) on a partial selection, clear otherwise — followed by a muted `{n} of {m} selected` count (`common.selectedOfTotal`, numbers via Intl.NumberFormat) and a Separator. One click is ONE `onValueChange`: from mixed or clear it adds every enabled value, from checked it removes them. Disabled options are never counted, added or removed. The box has no `name`, so a native submit never carries it. `{ label }` replaces the default `dataEntry.checkboxGroup.selectAll` text.",
        "name": "selectAll",
        "type": "boolean | { label?: React.ReactNode }"
      }
    ],
    "related": [
      "RadioGroup — use when only ONE selection is allowed at a time (mutually exclusive). CheckboxGroup = multiple, RadioGroup = single.",
      "Checkbox (standalone) — use a bare `Checkbox` for a single boolean toggle (e.g. 'I agree to terms'). Use CheckboxGroup when you have 2+ related choices.",
      "Checkbox.Group — alias; the same component is also accessible as `Checkbox.Group` (the Checkbox export attaches CheckboxGroup as `.Group`). Both are equivalent — prefer the named `CheckboxGroup` import for clarity in larger files.",
      "Switch / Field — for a single binary on/off toggle with immediate effect (not a form submission value). Do not use CheckboxGroup to fake toggle rows.",
      "Select (multi) — for a long list (10+ items) where space is limited; CheckboxGroup is better for ≤10 visible options that benefit from scanning all at once."
    ],
    "rules": [
      3,
      6,
      23,
      31
    ],
    "tagline": "Multi-select checkbox list from an options array or manual children — use `options` prop for the data-driven path, never hand-roll individual Checkbox items in a group.",
    "usage": [
      "DO pass `selectAll` instead of hand-wiring a parent checkbox above the group — the derived tri-state, the n-of-m count and the disabled-option rule are built in: `<CheckboxGroup options={channels} defaultValue={['email']} selectAll />`.",
      "DO use the `options` prop for any data-driven list — it auto-generates IDs, handles checked state, and wires up Field (label + description) for each item. NEVER hand-roll individual `<Checkbox>` elements inside a loop when you have an options array.",
      "DO pass `name` when inside an HTML form so each checkbox submits its value under the same field name, giving the server a multi-value array. Without `name`, native form submission silently drops all values.",
      "Controlled vs uncontrolled: pass `value` + `onChange` together for controlled usage (e.g. react-hook-form). Pass `defaultValue` alone for uncontrolled usage. Do NOT mix both — if `value` is provided, `defaultValue` is ignored and onChange must update value externally or the UI freezes.",
      "Each option's `description` renders as a secondary line below its label via Field — use it for help text or sub-copy; keep `label` short.",
      "Group-level `disabled` disables all checkboxes. Individual `options[n].disabled` disables only that item. Both can coexist.",
      "DO NOT wrap this inside another ARIA group or fieldset without removing the built-in `role=\"group\"` — it already provides the correct grouping semantics. Pair the group with a `<legend>` or visible heading for a11y."
    ],
    "useCases": [
      "Permission / role selectors in admin forms — e.g. 'Select applicable roles: Admin, Editor, Viewer' where users can pick multiple.",
      "Filter panels — e.g. 'Filter by status: Active, Pending, Archived' with horizontal orientation for compact toolbar layout.",
      "Feature flag or settings toggles where multiple independent boolean flags share a label/description pair, loaded from a config array.",
      "Multi-category tagging forms — e.g. 'Tag this invoice: Recurring, Billable, Internal' driven by an options array fetched from an API.",
      "Onboarding checklists or multi-step preference screens where selections persist across steps via controlled `value`.",
      "Accounting module: select which cost centres or account codes apply to a transaction, driven by a normalized options list."
    ]
  },
  {
    "example": "{`import { Radio } from \"@godxjp/ui/data-entry\";\n\n// --- Options-array API (recommended for most cases) ---\nconst PAYMENT_METHODS = [\n  { label: \"Credit Card\", value: \"card\", description: \"Charged immediately on save.\" },\n  { label: \"Bank Transfer\", value: \"bank\" },\n  { label: \"Invoice\", value: \"invoice\", disabled: true },\n];\n\nfunction PaymentMethodPicker() {\n  const [method, setMethod] = React.useState(\"card\");\n\n  return (\n    <Radio.Group\n      name=\"payment_method\"\n      value={method}\n      onValueChange={setMethod}\n      options={PAYMENT_METHODS}\n      orientation=\"vertical\"\n    />\n  );\n}\n\n// --- Manual composition (when you need custom layout) ---\nfunction CustomRadioGroup() {\n  return (\n    <Radio.Group name=\"account_type\" defaultValue=\"asset\">\n      <Radio.Item id=\"opt-asset\" value=\"asset\" />\n      {/* wrap each item in Field for label + description */}\n    </Radio.Group>\n  );\n}`}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Radio",
    "props": [
      {
        "defaultValue": "\"default\"",
        "description": "antd `optionType` — how each choice is DRAWN. `default` is a radio dot beside its label; `button` welds them into one segmented bar. It is paint, never semantics: the roles stay radiogroup/radio, so arrow-key traversal and native submission keep working. That is also why this is not a ToggleGroup: a ToggleGroup's single mode is a row of aria-pressed buttons that permits 'none chosen' unless it is given `disallowEmptySelection` (gh#744), and Radio is a form control with a `name` that submits.",
        "name": "optionType",
        "type": "\"default\" | \"button\""
      },
      {
        "defaultValue": "\"outline\"",
        "description": "antd `buttonStyle` — fill of the selected choice while `optionType` is `button`. Ignored otherwise.",
        "name": "buttonStyle",
        "type": "\"outline\" | \"solid\""
      },
      {
        "description": "Controlled selected value. Must be paired with onValueChange to update state.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial value. Use when you do not need to track selection in state.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Callback fired when the user selects a different option. Required when value is controlled.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Declarative option list: { label: ReactNode; value: string; disabled?: boolean; description?: ReactNode }[]. When provided, Radio.Group renders each option as a labelled Field automatically. Omit to compose children manually.",
        "name": "options",
        "type": "ChoiceOptionProp[]"
      },
      {
        "defaultValue": "\"vertical\"",
        "description": "Layout direction for the option list. Vertical stacks options; horizontal lays them side by side.",
        "name": "orientation",
        "type": "\"vertical\" | \"horizontal\""
      },
      {
        "description": "Disables the entire group when true. Individual options can also be disabled via options[].disabled.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "HTML form field name. Required for native form submission — each option is a real `<input type=\"radio\">` under this name, so the browser serialises the selected value with no extra wiring.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Additional CSS class applied to the group root.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Manual composition fallback — used only when options is not provided. Render Radio.Item (+ Field wrapper) children directly inside Radio.Group.",
        "name": "children",
        "type": "React.ReactNode"
      }
    ],
    "related": [
      "Checkbox.Group — use when users may select multiple options simultaneously; Radio.Group enforces single-selection only.",
      "Switch / Field — use for a single boolean on/off toggle (e.g. enable notifications); Radio.Group is for choosing one among three or more named options.",
      "Select — use when there are many options (5+) and vertical screen space is limited; Radio.Group is preferable for 2-4 short options where all choices should be visible at a glance."
    ],
    "rules": [
      3,
      6,
      13,
      23
    ],
    "subParts": [
      "RadioItem"
    ],
    "tagline": "Radio group on react-aria-components, with an options-array shorthand — always use Radio.Group, never a bare radio input. `role=\"radio\"` is the real `<input>`; the painted dot is the `<label>` around it and carries `data-state`.",
    "usage": [
      "DO use Radio.Group (not the bare Radio export) as the root — it wires up the group context, keyboard navigation, and the shared input name. A lone Radio.Item outside a Radio.Group has no context and will not function.",
      "DO prefer the options array API for static/data-driven option lists: pass options={[{ label, value, description?, disabled? }]} and Radio.Group renders each as a correctly-labelled Field automatically — no manual id/label wiring needed.",
      "DO pass name to Radio.Group when the selection must be submitted via a native HTML form — the options ARE `<input type=\"radio\" name={name}>`, so FormData/fetch pick the value up without extra wiring.",
      "DO use controlled mode (value + onValueChange) when the selection drives other UI (conditional fields, preview panels). Use defaultValue for fire-and-forget uncontrolled forms.",
      "DON'T hand-roll a label-plus-radio row with raw <input type='radio'> — use Radio.Group with options or compose Radio.Item inside Field for custom markup. Every option must be wrapped in Field (or equivalent) for the label htmlFor/id linkage.",
      "DON'T disable individual options inside the options array and ALSO set disabled on the group — group-level disabled wins and overrides all per-item disabled states."
    ],
    "useCases": [
      "Payment method selection (Credit Card / Bank Transfer / Invoice) on a checkout or invoice-creation form — mutually exclusive, 2-4 options, use options array + name for form submission.",
      "Account type picker (Asset / Liability / Equity / Revenue / Expense) on a chart-of-accounts create/edit page — use options with descriptions to explain each type.",
      "Report frequency chooser (Daily / Weekly / Monthly / Quarterly) in a scheduled-report settings panel — horizontal orientation when options are short labels.",
      "Tax regime selector on an entity or vendor profile form where exactly one option must always be active — controlled mode so adjacent fields can react to the selection.",
      "Approval workflow step type (Automatic / Manual / Conditional) in a workflow builder — use descriptions inside options to explain each mode without extra tooltip markup.",
      "Filter scope toggle (All entities / Current entity only) in an admin dashboard filter bar — horizontal orientation, no name needed (state managed in React, not submitted)."
    ]
  },
  {
    "absorbed": [
      "HoverCard",
      "HoverCardTrigger",
      "HoverCardContent"
    ],
    "example": "import {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverDescription,\n} from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\n\n// Uncontrolled — basic usage\nexport function InvoiceFilterPopover() {\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\">Advanced filters</Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" width=\"trigger\">\n        <PopoverHeader>\n          <PopoverTitle>Filter invoices</PopoverTitle>\n          <PopoverDescription>Narrow results by date range and status.</PopoverDescription>\n        </PopoverHeader>\n        {/* place form controls here */}\n      </PopoverContent>\n    </Popover>\n  );\n}\n\n// Controlled — programmatic open/close\nimport * as React from \"react\";\n\nexport function ControlledPopover() {\n  const [open, setOpen] = React.useState(false);\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\" aria-label=\"Show details\">\n          ?\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent side=\"right\" sideOffset={8}>\n        <PopoverHeader>\n          <PopoverTitle>About this field</PopoverTitle>\n          <PopoverDescription>\n            The MF ID is the unique identifier assigned by Money Forward.\n          </PopoverDescription>\n        </PopoverHeader>\n      </PopoverContent>\n    </Popover>\n  );\n}",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Popover",
    "props": [
      {
        "description": "Controls open state in controlled mode. Pair with onOpenChange.",
        "name": "open",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Initial open state for uncontrolled usage.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Callback fired when the popover open state changes. Required when using controlled mode (open prop).",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "defaultValue": "false",
        "description": "When true, interaction outside the popover is blocked and focus is trapped inside (React Aria modal popover).",
        "name": "modal",
        "type": "boolean"
      },
      {
        "defaultValue": "\"click\"",
        "description": "What opens the panel. \"click\": the trigger toggles a dialog panel. \"hover\": a HOVER CARD — rich, non-modal preview content that opens when the pointer rests on the trigger for `openDelay` and closes `closeDelay` after it leaves (moving onto the card cancels the close); keyboard focus opens it and blur closes it at once; touch never opens it. In hover mode the panel is not a dialog and takes no focus, and a trigger without `asChild` renders an `<a>`. This is what `HoverCard` was (v32).",
        "name": "openOn",
        "type": "\"click\" | \"hover\""
      },
      {
        "defaultValue": "200",
        "description": "`openOn=\"hover\"` only: ms the pointer rests on the trigger before opening.",
        "name": "openDelay",
        "type": "number"
      },
      {
        "defaultValue": "100",
        "description": "`openOn=\"hover\"` only: ms after the pointer leaves before closing.",
        "name": "closeDelay",
        "type": "number"
      },
      {
        "defaultValue": "\"center\"",
        "description": "PopoverContent prop. Horizontal alignment of the popover relative to the trigger.",
        "name": "align",
        "type": "'start' | 'center' | 'end'"
      },
      {
        "defaultValue": "4",
        "description": "PopoverContent prop. Distance in pixels between the popover panel and its anchor.",
        "name": "sideOffset",
        "type": "number"
      },
      {
        "defaultValue": "\"bottom\"",
        "description": "PopoverContent prop. Which side of the trigger the panel prefers to open on (auto-flips on overflow).",
        "name": "side",
        "type": "'top' | 'right' | 'bottom' | 'left'"
      },
      {
        "defaultValue": "false",
        "description": "PopoverTrigger prop. Merges trigger props onto the immediate child element (e.g. a Button) instead of rendering an extra DOM node. Strongly recommended to avoid a wrapping <button>.",
        "name": "asChild",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "PopoverContent prop. The panel's CONTENT owns its inset: the popover zeroes its own --popover-space-inset so a Command list, a menu or a table runs edge to edge and draws its separators across the full width. Reach for it whenever the child already paints its own rows; leave it off for prose panels, which want the panel padding. Ignored under openOn=\"hover\" (a hover card keeps its own measure).",
        "name": "flush",
        "type": "boolean"
      },
      {
        "defaultValue": "\"panel\"",
        "description": "PopoverContent prop. How the panel is MEASURED across the inline axis. \"panel\" (default) is the popover's own --popover-width (18rem) — right for prose and for a list the panel sizes itself. \"auto\" lets the CONTENT decide, the only correct answer for something with a width of its own (a two-month Calendar is far wider than 18rem and was being clipped by it). \"trigger\" matches the anchor. It is the counterpart of `flush`: both set a token the panel already reads, so a service theme keeps owning them, where a `w-*` utility on className is a per-call-site constant nothing can retune. Ignored under openOn=\"hover\" (a hover card keeps its own measure).",
        "name": "width",
        "type": "\"panel\" | \"auto\" | \"trigger\""
      },
      {
        "description": "PopoverContent prop. Extra Tailwind classes merged onto the panel (default: w-72 p-4 rounded-md border shadow-md z-50).",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Tooltip — use Tooltip for brief, non-interactive label-like hints (hover-only, no inputs). Use Popover when the floating content is interactive (buttons, inputs, forms).",
      "Dialog/Sheet — use Dialog or Sheet for full modal actions that require user confirmation or significant input. Use Popover for lightweight, anchor-relative panels that dismiss on outside click.",
      "DropdownMenu — use DropdownMenu for a flat list of clickable actions or links. Use Popover when the floating panel needs arbitrary layout (forms, grids, rich content) rather than a menu list."
    ],
    "rules": [
      3,
      23,
      31,
      35
    ],
    "storyPath": "data-display/popover.tsx",
    "subParts": [
      "PopoverAnchor",
      "PopoverContent",
      "PopoverDescription",
      "PopoverHeader",
      "PopoverTitle",
      "PopoverTrigger"
    ],
    "tagline": "React Aria floating panel anchored to a trigger — always compose with PopoverTrigger + PopoverContent; never use a raw div overlay.",
    "usage": [
      "HOVER CARD: `<Popover openOn=\"hover\"><PopoverTrigger asChild><a href=…>@yamada</a></PopoverTrigger><PopoverContent>…</PopoverContent></Popover>` for RICH preview content (an avatar + bio, an entity summary). For a short plain-text hint use Tooltip. DON'T put essential information only in a hover card — touch never opens it; make the same content reachable from the trigger's destination. There is no `HoverCard` component.",
      "DO compose: <Popover> → <PopoverTrigger asChild> → <Button/> and <PopoverContent>. All four parts are required for any popover to function; omitting PopoverTrigger or PopoverContent produces nothing.",
      "DO set `<PopoverContent width=\"auto\">` when the child brings its own measure — a Calendar, a chart, a fixed-width preview. The panel's own 18rem clips them, and a `w-*` utility on className is a per-call-site constant no service theme can retune, which is exactly what `flush` exists to avoid on the padding axis. `width=\"trigger\"` matches the anchor, the shape every select-like control wants.",
      "DO set `<PopoverContent flush>` when the panel holds a Command list, a menu or a table — the child owns its own inset, so its rows and separators reach the panel edges. Never zero the padding with a utility on className: that is a per-call-site constant no service theme can retune, while `flush` keeps the inset on --popover-space-inset.",
      "DO use asChild on PopoverTrigger when the trigger is already a Button or link — this avoids a nested <button><button> violation and extra DOM nesting.",
      "DO use controlled mode (open + onOpenChange) when external code must open/close the popover programmatically (e.g., form validation reveal, keyboard shortcut). For toggle-only interactions, uncontrolled (defaultOpen) is simpler.",
      "DO structure panel content with PopoverHeader > PopoverTitle + PopoverDescription for labelled panels. This is purely presentational but establishes the correct font-weight and muted-foreground on the description.",
      "DON'T hand-roll a floating div or use a CSS show/hide toggle — Popover provides portal rendering, focus trap, Escape-to-close, and ARIA automatically.",
      "DON'T place a Popover inside a Dialog without setting modal={false} on the Popover — nested modals conflict in focus management and produce stuck focus."
    ],
    "useCases": [
      "User/profile preview on an @mention, or an entity (customer/account) preview on a table cell — `openOn=\"hover\"`.",
      "Advanced filter panel: a Filters Button triggers a Popover containing filter inputs (date range, status selects); panel measured with `<PopoverContent width='auto'>` so the filters set the width, or `width='trigger'` to match the button.",
      "Row action menu overflow: when a DataTable row has too many actions for inline display, a Popover holds the secondary actions (Edit, Archive, Delete) without navigating away.",
      "Contextual help / tooltip-rich: a small '?' icon button opens a Popover with PopoverTitle + PopoverDescription explaining a form field — richer than a Tooltip but less intrusive than a Dialog.",
      "Inline record preview: clicking a reference number in an invoice list opens a Popover showing a summary card of the linked document before the user decides to navigate.",
      "Column visibility picker: a 'Columns' button above a DataTable opens a Popover containing checkboxes to show/hide columns, with controlled state managed in parent.",
      "Quick-edit cell: for an admin table, clicking a status badge opens a Popover with a RadioGroup to change status in-place without a full Dialog."
    ]
  },
  {
    "example": "import { Card, CardContent, ScrollArea } from \"@godxjp/ui/data-display\";\nimport { Text } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n// Vertical-only (default). The FRAME is a Card, not a rounded-md border — a hand-rolled surface\n// freezes the radius and the border colour at the call site, where no theme can reach them. The\n// bounded height stays a class because it is a measurement of this SCREEN, not a shape of the\n// component.\n<Card>\n  <CardContent flush>\n    <ScrollArea className=\"h-64\">\n      <Flex direction=\"col\" gap=\"sm\" pad={4}>\n        {entries.map((entry) => (\n          <Text key={entry.id} size=\"sm\">{entry.label}</Text>\n        ))}\n      </Flex>\n    </ScrollArea>\n  </CardContent>\n</Card>\n\n// Horizontal + vertical (e.g. wide Cascader columns). Use border-e, never border-r: a physical\n// edge puts the rule on the wrong side under RTL.\n<ScrollArea className=\"w-full\" orientation=\"both\">\n  <Flex className=\"max-h-[min(280px,50vh)]\">\n    {columns.map((col, i) => (\n      <ul key={i} className=\"min-w-36 border-e last:border-e-0\">\n        {col.map((item) => <li key={item.value}>{item.label}</li>)}\n      </ul>\n    ))}\n  </Flex>\n</ScrollArea>\n\n// Live stream — pinned to the newest post, never yanked while reading history\nconst viewport = React.useRef<HTMLDivElement>(null);\nconst [atNewest, setAtNewest] = React.useState(true);\n\n<Card>\n  <CardContent flush>\n    <ScrollArea\n      anchor=\"bottom\"\n      viewportRef={viewport}\n      onAnchoredChange={setAtNewest}\n      className=\"h-64\"\n    >\n      <Flex direction=\"col\" pad={{ inline: 3 }}>\n        {posts.map((post) => <PostRow key={post.id} post={post} />)}\n      </Flex>\n    </ScrollArea>\n  </CardContent>\n</Card>\n<Button\n  type=\"button\"\n  disabled={atNewest}\n  onClick={() => {\n    const node = viewport.current;\n    if (node) node.scrollTop = node.scrollHeight;\n  }}\n>\n  {t(\"chat.jumpToNewest\")}\n</Button>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "ScrollArea",
    "props": [
      {
        "description": "Extra classes applied to the root element. Must include a height constraint (h-*, max-h-*) — without one, the viewport expands to fit its content and no scrollbar is rendered.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "The content to make scrollable. Wrap it in a single element so the Viewport can measure its full size correctly.",
        "name": "children",
        "required": true,
        "type": "React.ReactNode"
      },
      {
        "description": "Rides through to the element as a plain attribute. Rarely needed: the element inherits the page's direction, and the component stamps NO direction of its own (the Radix root used to stamp `ltr`, which reset the inline axis for everything inside it on an RTL page).",
        "name": "dir",
        "type": "\"ltr\" | \"rtl\""
      },
      {
        "defaultValue": "\"vertical\"",
        "description": "Axes that scroll — this IS the element's `overflow`, so an axis you do not ask for is `hidden` and its content is CLIPPED, not merely missing a bar. Use 'horizontal' for a strip of non-shrinking columns (a board, a lane of cards) — the element keeps its tab stop so the strip stays keyboard-scrollable, and the consumer writes no overflow class of its own. Pair it with a width constraint, exactly as the vertical case needs a height one. 'both' is the replacement for the pre-v23 shape of a vertical area that also mounted <ScrollBar orientation=\"horizontal\" />.",
        "name": "orientation",
        "type": "\"vertical\" | \"horizontal\" | \"both\""
      },
      {
        "description": "Ref to the element that actually SCROLLS. Since v23 that is the component's own element, so `ref` and `viewportRef` hand back the SAME node (before v23 `ref` pointed at an overflow:hidden root that never scrolled). Use it to read scrollTop/scrollHeight, call scrollTo(), restore a saved position or drive a 'jump to newest' button. `[data-radix-scroll-area-viewport]` no longer exists anywhere — query `[data-slot=\"scroll-area-viewport\"]` only if you truly cannot hold a ref.",
        "name": "viewportRef",
        "type": "React.Ref<HTMLDivElement>"
      },
      {
        "defaultValue": "\"auto\"",
        "description": "Whether the bar is ALWAYS drawn or left to the platform. `auto` styles whatever bar the platform decides to draw — and on macOS/iPadOS with the system default \"Show scroll bars: when scrolling\" that is an OVERLAY bar, present only while the reader is already scrolling, so a wide area does not look scrollable at rest (measured: clientWidth 727 of scrollWidth 3476, nothing occupying layout). `always` forces a classic bar that occupies layout, from the same `--scroll-area-*` tokens. Reach for it where the affordance IS the information (a wide table, a roster, a board); leave it `auto` for a chat stream.",
        "name": "scrollbar",
        "type": "\"auto\" | \"always\""
      },
      {
        "defaultValue": "\"none\"",
        "description": "Edge the viewport sticks to as its content grows. 'none' leaves the scroll offset entirely alone (today's behaviour).",
        "name": "anchor",
        "type": "\"none\" | \"bottom\""
      },
      {
        "defaultValue": "--scroll-area-anchor-offset (3rem)",
        "description": "Distance in px from the bottom edge inside which the reader still counts as 'following' for anchor=\"bottom\". Read from the --scroll-area-anchor-offset token at mount, so a theme moves it globally; this prop overrides it per instance. Size it to the row height the service renders (a dense log line vs a chat bubble with an avatar).",
        "name": "anchorOffset",
        "type": "number"
      },
      {
        "description": "Fires when the pinned state flips — false when the reader scrolls away from the bottom, true when they return inside anchorOffset. Render a real focusable 'jump to newest' Button from it: anchoring must never be the only route back to new content.",
        "name": "onAnchoredChange",
        "type": "(anchored: boolean) => void"
      },
      {
        "defaultValue": "the localized \"Scrollable region\"",
        "description": "Accessible name for the scroll REGION — the tabindex=\"0\" element a keyboard user lands on to scroll content bigger than the box. Optional: left out it takes the localized dataDisplay.scrollArea.region default, so no consumer has to invent a name merely to stop shipping an anonymous focus stop (gh#821). Pass a plain string when the screen can say WHICH region ('Activity log'); a non-string node cannot be an aria-label and falls back to the default. The role, the name and the stop are all emitted only while there IS overflow on the axes `orientation` opens.",
        "name": "label",
        "type": "React.ReactNode"
      }
    ],
    "related": [
      "DataTable — use DataTable (not ScrollArea) when data is tabular and needs sorting/selection; DataTable manages its own overflow internally.",
      "Collapsible — use Collapsible to show/hide a section; pair with ScrollArea when the revealed content can itself overflow.",
      "Card/CardContent — when the card body should scroll, put ScrollArea inside CardContent rather than applying overflow directly to CardContent."
    ],
    "rules": [
      2,
      3,
      24,
      31
    ],
    "storyPath": "data-display/scroll-area.tsx",
    "subParts": [
      "ScrollBar"
    ],
    "tagline": "Keyboard focus on the scroll viewport and its descendants uses the shared inset brand ring (--focus-ring-inset-offset), without padding changes. A native scrolling box (no scroll library since v23): one `overflow: auto` element whose scrollbar is styled from --scroll-area-* tokens. Always set an explicit height/max-height, or nothing overflows and no scrollbar appears. Owns the scrolling element, so it also owns reaching it (viewportRef) and bottom anchoring for a live stream (anchor).",
    "usage": [
      "DO set an explicit height or max-height on ScrollArea via className (e.g. `className=\"h-64\"` or `className=\"max-h-[min(300px,50vh)]\"`). Without a height constraint the viewport grows to fit content and the scrollbar is never rendered.",
      "DO wrap content in a single child element inside ScrollArea — the Viewport observes its single child's size to decide whether overflow exists.",
      "DO open a second axis with `orientation=\"both\"`. `ScrollBar` still exports but RENDERS NOTHING — before v23 mounting it was what enabled an axis; now `orientation` is the only thing that does, so a leftover `<ScrollBar orientation=\"horizontal\" />` silently leaves that axis `hidden`.",
      "DO reach for ScrollArea rather than your own `overflow-auto` div: it is the same native scrolling, plus the token-styled scrollbar, the keyboard tab stop the axe rule wants, and bottom anchoring. A bare overflow div gets none of that and each call site re-decides the scrollbar's look.",
      "DON'T put ScrollArea inside a flex parent without giving it a `flex-1` or fixed size — it will collapse to zero height and appear broken.",
      "For horizontal-only scrolling use `orientation=\"horizontal\"` with a width constraint; the content is allowed to grow past the box on that axis, so a row of non-shrinking columns overflows instead of squashing.",
      "DON'T use a horizontal ScrollArea for a strip of filters, search fields, form controls or filter chips — a filter strip WRAPS (cardinal rule #51; ui-audit `filter-strip-no-scroll`). Use FilterBar, and past 2–4 filters the `list-filters` pattern's 'More filters' Sheet.",
      "DO use `viewportRef` (or plain `ref`) when you need the scrolling element, never a `querySelector` for an internal attribute — `[data-radix-scroll-area-viewport]` is gone with Radix, and any selector matches the wrong node the moment two ScrollAreas nest.",
      "DO build a live stream (chat, log tail, streaming response, activity feed) with `anchor=\"bottom\"` instead of writing `viewport.scrollTop = viewport.scrollHeight` in an effect. That naive version is the BUG, not the feature: it yanks a reader who has scrolled up back to the bottom on every arriving message (WCAG 3.2.5).",
      "DO pair `anchor=\"bottom\"` with `onAnchoredChange` and a real Button (\"jump to newest\"). A keyboard user who has scrolled up needs a focusable route back to new content — the anchor alone is a pointer affordance.",
      "DON'T remove the viewport's `tabIndex={0}`, and don't wrap the scrolling content in something that swallows arrow keys: the region must stay keyboard-scrollable (WCAG 2.1.1 / axe scrollable-region-focusable). The component measures its own overflow and emits the stop only while there is something to scroll, so you never have to withhold it yourself.",
      "DO pass `label` on a scroll area the screen can NAME ('Activity log', 'Board'). The stop the keyboard user lands on carries role=\"group\" plus that name; without one it still announces the localized 'Scrollable region', never the anonymous stop it used to be (gh#821). `group`, not a landmark `region` — several named regions on one page collide under axe landmark-unique.",
      "DON'T add an `aria-live` region to the ScrollArea to announce arriving items — a live region on a scroll container re-announces on reflow. Announce from your own small status region next to it.",
      "Retune the stickiness once per service with the `--scroll-area-anchor-offset` token rather than passing `anchorOffset` on every instance."
    ],
    "useCases": [
      "Long dropdown lists inside Popovers or Selects where the list height must be capped (e.g. TreeSelect, Cascader columns, Combobox options).",
      "Sidebar navigation panels or filter drawers whose content can exceed viewport height.",
      "Transfer-list panels with a fixed height that must scroll through a large item list.",
      "Cascader multi-column layouts where both axes may overflow (orientation=\"both\").",
      "Detail panels or audit-log timelines inside a fixed-height Card that should not stretch the page.",
      "Code or JSON viewers with a fixed max-height needing both axes scrollable.",
      "Live message streams and log tails: `anchor=\"bottom\"` pins to the newest item while the reader is at the bottom, freezes when they scroll up to read history, and preserves their position when a page of older history is prepended."
    ]
  },
  {
    "example": "{`import { useState } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\n// Money goes through Intl — the symbol, the grouping and the minor units are all locale data.\nconst money = new Intl.NumberFormat(\"ja-JP\", { style: \"currency\", currency: \"JPY\" });\nimport {\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n} from \"@godxjp/ui/data-display\";\nimport { Card, CardContent } from \"@godxjp/ui/data-display\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n// --- Uncontrolled (simplest) ---\n// The icon needs no margin of its own: Button spaces its own icon slot. The gap between the\n// trigger and the revealed body belongs to the Flex around them, and the body is a Card — a\n// rounded-md border div is a hand-rolled surface with the radius frozen at the call site.\nexport function InvoiceLineDetail() {\n  return (\n    <Collapsible>\n      <Flex direction=\"col\" align=\"start\" gap=\"sm\">\n        <CollapsibleTrigger asChild>\n          <Button variant=\"ghost\" size=\"sm\">\n            <ChevronDown aria-hidden=\"true\" />\n            Show tax breakdown\n          </Button>\n        </CollapsibleTrigger>\n        <CollapsibleContent>\n          <Card>\n            <CardContent>\n              <Flex direction=\"col\" gap=\"xs\">\n                <Text size=\"sm\">Consumption tax (10%): {money.format(1234)}</Text>\n                <Text size=\"sm\">Withholding tax: {money.format(0)}</Text>\n              </Flex>\n            </CardContent>\n          </Card>\n        </CollapsibleContent>\n      </Flex>\n    </Collapsible>\n  );\n}\n\n// --- Controlled (open driven externally) ---\nexport function FilterSection() {\n  const [open, setOpen] = useState(false);\n  return (\n    <Collapsible open={open} onOpenChange={setOpen}>\n      <Flex direction=\"col\" align=\"start\" gap=\"sm\">\n        <CollapsibleTrigger asChild>\n          <Button variant=\"outline\" size=\"sm\">\n            Advanced filters\n            <ChevronDown aria-hidden=\"true\" />\n          </Button>\n        </CollapsibleTrigger>\n        <CollapsibleContent>\n          {/* place filter controls here */}\n          <Text size=\"sm\" tone=\"muted\">Date range, entity, status…</Text>\n        </CollapsibleContent>\n      </Flex>\n    </Collapsible>\n  );\n}`}",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Collapsible",
    "props": [
      {
        "description": "Controlled open state. When provided, pair with onOpenChange to keep in sync. Omit for uncontrolled behaviour.",
        "name": "open",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Initial open state when used uncontrolled. Useful for auto-expanding when a child is active (e.g. sidebar nav group).",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Callback fired when the open state changes. Required in controlled mode.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "defaultValue": "false",
        "description": "Prevents the trigger from toggling the content. The trigger is still rendered but inert.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Applied to the root Collapsible element. Use for layout (e.g. sb-nav-group) — the root renders a div.",
        "name": "className",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "On CollapsibleTrigger only — merges the trigger behaviour onto the single child element (e.g. a godx-ui Button) instead of rendering a default button. The child must accept onClick and aria-* props.",
        "name": "asChild (CollapsibleTrigger)",
        "type": "boolean"
      }
    ],
    "related": [
      "Accordion (@godxjp/ui/data-display) — use Accordion when only ONE section can be open at a time across a group; use Collapsible when each section is independent and can be open simultaneously.",
      "Popover — use Popover when the revealed content should float above the layout in a portal overlay; use Collapsible when the content should push surrounding content down inline.",
      "Dialog/Sheet — use Dialog or Sheet for modal or slide-over panels that demand full user attention; Collapsible stays in-flow and non-modal.",
      "Tree (@godxjp/ui/data-display) — use Tree for hierarchical data that expands, collapses and is navigated by keyboard; use Collapsible for ad-hoc single-level toggle regions."
    ],
    "rules": [
      3,
      6,
      23
    ],
    "storyPath": "data-display/collapsible.tsx",
    "subParts": [
      "CollapsibleContent",
      "CollapsibleTrigger"
    ],
    "tagline": "Three-part compound (Collapsible + CollapsibleTrigger + CollapsibleContent) that toggles a region open/closed — never use just one part alone.",
    "usage": [
      "DO compose all three parts together: <Collapsible> wraps both <CollapsibleTrigger> and <CollapsibleContent>. Never render CollapsibleContent without a parent Collapsible — the open state lives on the root.",
      "DO use asChild on CollapsibleTrigger when you want a godx-ui Button (or any styled element) to act as the trigger: <CollapsibleTrigger asChild><Button>Toggle</Button></CollapsibleTrigger>. Without asChild the trigger renders its own plain button.",
      "DO pass defaultOpen={true} (uncontrolled) when the section should auto-expand on mount — for example, a sidebar nav group whose active child matches the current route.",
      "DO use controlled mode (open + onOpenChange) when external UI — a separate button, route change, or search filter — needs to drive the open state independently of the trigger.",
      "DON'T add hidden native <details>/<summary> as a fallback — the primitive is already accessible (aria-expanded, aria-controls) out of the box.",
      "DON'T put interactive controls (Buttons, links, inputs) inside CollapsibleTrigger itself unless using asChild — nested focusable elements break keyboard navigation. Put them inside CollapsibleContent instead."
    ],
    "useCases": [
      "Sidebar navigation group: a top-level nav item with children collapses/expands its sub-items in the rail (used directly in the godx-ui Sidebar component with defaultOpen={active}).",
      "Invoice line-item detail: an invoice row that expands inline to show tax breakdown, allocation notes, or audit trail without navigating away.",
      "Filter panel section: a labelled group of filter controls (date range, entity, status) that can be collapsed to save vertical space in a dense accounting dashboard.",
      "Read-more / long description: a truncated journal entry or payment memo that expands to full text on demand.",
      "Settings sub-section: an optional advanced settings block that is hidden by default and revealed only when the user opts in.",
      "Audit log detail: a compact log entry row that expands to show full diff, user, timestamp, and before/after values."
    ]
  },
  {
    "docPath": "docs/data-display/tree.tsx",
    "example": "import * as React from \"react\";\nimport { Tree } from \"@godxjp/ui/data-display\";\n\nconst permissions = [\n  {\n    value: \"billing\",\n    label: \"Billing\",\n    children: [\n      { value: \"billing.invoice.read\", label: \"Read invoices\" },\n      { value: \"billing.invoice.write\", label: \"Issue invoices\" },\n    ],\n  },\n  {\n    value: \"people\",\n    label: \"People\",\n    children: [\n      { value: \"people.read\", label: \"Read profiles\" },\n      { value: \"people.write\", label: \"Edit profiles\", disabled: true },\n    ],\n  },\n];\n\nexport function PermissionTree() {\n  // The name is YOUR copy — run it through your app's own i18n, not the library's.\n  const [granted, setGranted] = React.useState<string[]>([\"billing.invoice.read\"]);\n\n  return (\n    <Tree\n      aria-label=\"Permissions\"\n      treeData={permissions}\n      checkable\n      defaultExpandAll\n      checkedValues={granted}\n      onCheckedValuesChange={setGranted}\n    />\n  );\n}",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Tree",
    "props": [
      {
        "description": "The hierarchy. Each node: `{ value: string; label: ReactNode; disabled?: boolean; disableCheckbox?: boolean; isLeaf?: boolean; icon?: ReactNode; children?: TreeNodeProp[] }`. Same node shape as TreeSelect/Cascader; use `fieldNames` to remap an API response instead of transforming it.",
        "name": "treeData",
        "required": true,
        "type": "TreeNodeProp[]"
      },
      {
        "description": "Remap the data's own key names, e.g. `{ label: 'name', value: 'id', children: 'items' }`.",
        "name": "fieldNames",
        "type": "{ label?: string; value?: string; children?: string }"
      },
      {
        "description": "Controlled SELECTION (antd `selectedKeys`). `string` while single, `string[]` once `multiple` is on. Selection and checks are separate axes — do not use this for checkboxes.",
        "name": "value",
        "type": "string | string[]"
      },
      {
        "description": "Initial selection for an uncontrolled tree. Ignored once `value` is passed.",
        "name": "defaultValue",
        "type": "string | string[]"
      },
      {
        "description": "Selection change (antd `onSelect`). Emits `string | undefined` while single, `string[]` once `multiple` is on.",
        "name": "onValueChange",
        "type": "(value: string | string[] | undefined) => void"
      },
      {
        "defaultValue": "false",
        "description": "Allow more than one selected node. Also sets `aria-multiselectable` on the tree.",
        "name": "multiple",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw a checkbox on every node (antd `checkable`). Checking cascades to descendants and a parent shows `indeterminate` while only some are checked. With `checkable`, Enter/Space toggles the checkbox rather than selecting.",
        "name": "checkable",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Parent and child checks are independent (antd `checkStrictly`) — no cascade and no indeterminate state.",
        "name": "checkStrictly",
        "type": "boolean"
      },
      {
        "description": "Controlled checked nodes (antd `checkedKeys`).",
        "name": "checkedValues",
        "type": "string[]"
      },
      {
        "description": "Initial checked nodes for an uncontrolled tree (antd `defaultCheckedKeys`).",
        "name": "defaultCheckedValues",
        "type": "string[]"
      },
      {
        "description": "Checked-set change (antd `onCheck`). The array is normalised: a branch appears only when every tickable child is checked, so it can never claim a partial selection is complete.",
        "name": "onCheckedValuesChange",
        "type": "(values: string[]) => void"
      },
      {
        "description": "Controlled expanded branches (antd `expandedKeys`).",
        "name": "expandedValues",
        "type": "string[]"
      },
      {
        "description": "Initial expanded branches (antd `defaultExpandedKeys`).",
        "name": "defaultExpandedValues",
        "type": "string[]"
      },
      {
        "description": "Expansion change (antd `onExpand`). Fires for controlled and uncontrolled trees.",
        "name": "onExpandedValuesChange",
        "type": "(values: string[]) => void"
      },
      {
        "defaultValue": "false",
        "description": "Start with every branch open. Seeded once on mount, never re-applied.",
        "name": "defaultExpandAll",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Expand a parent by clicking its ROW, not only its switcher (antd DirectoryTree): \"click\" selects AND toggles — a page tree where choosing a folder opens it; \"doubleClick\" toggles on a double click (gh#1162). Keyboard keeps the APG map.",
        "name": "expandAction",
        "type": "false | \"click\" | \"doubleClick\""
      },
      {
        "description": "Lazy children (antd `loadData`). Called when a branch with no `children` and `isLeaf !== true` is expanded, and never again once its promise RESOLVES; a Skeleton row and `aria-busy` cover the wait. Push the fetched children into `treeData`. A REJECTED promise is not a load (rc-tree, gh#1041): the branch folds back shut (uncontrolled expansion) and the next expand asks again, up to 10 attempts.",
        "name": "loadData",
        "type": "(node: TreeNodeProp) => void | Promise<void>"
      },
      {
        "defaultValue": "false",
        "description": "Move nodes by dragging (antd `draggable`, gh#1093). Dropping on a row's upper / lower quarter lands in the gap before / after it, the middle files it inside. The tree never edits `treeData` — it reports the move via `onDrop` and you persist it. Keyboard equivalent (WCAG 2.5.7): Alt+↑ / Alt+↓ move past the previous / next sibling, Alt+← outdents (after the parent), Alt+→ indents (into the previous sibling, as its last child); every move is announced in a live region and described on the tree. A node never drops into itself or its own subtree.",
        "name": "draggable",
        "type": "boolean"
      },
      {
        "description": "Per-node drag gate (antd `draggable.nodeDraggable`). Disabled nodes never drag.",
        "name": "allowDrag",
        "type": "(node: TreeNodeProp) => boolean"
      },
      {
        "description": "Per-target drop gate (antd `allowDrop`), asked for pointer AND keyboard moves. Use it for your own rules (max depth, no leaf parents); the self/subtree cycle is already refused.",
        "name": "allowDrop",
        "type": "(info: { dragNode: TreeNodeProp; dropNode: TreeNodeProp; dropPosition: -1 | 0 | 1 }) => boolean"
      },
      {
        "description": "A committed move (antd `onDrop`). `dropPosition` is RELATIVE to `node` — -1 before, 0 inside, 1 after (antd reports an absolute index that every antd demo converts back to this). `dropToGap` is true for before/after (sibling of `node`), false for inside (child of `node`). Update `treeData` here.",
        "name": "onDrop",
        "type": "(info: { dragNode: TreeNodeProp; node: TreeNodeProp; dropPosition: -1 | 0 | 1; dropToGap: boolean }) => void"
      },
      {
        "description": "Render a node's title yourself (antd `titleRender`).",
        "name": "titleRender",
        "type": "(node: TreeNodeProp) => ReactNode"
      },
      {
        "description": "Highlight the nodes this returns true for (antd `filterTreeNode`, gh#1043) — the match of a search box above the tree. Matching rows are MARKED, not hidden: `data-filter-node=\"true\"`, the label in `--tree-node-filter-foreground` (default `hsl(var(--primary))`) at the medium weight, plus sr-only \"matches the filter\" text. Outline, keyboard and ARIA are unchanged. To also open the branches that hold a match, drive `expandedValues`.",
        "name": "filterTreeNode",
        "type": "(node: TreeNodeProp) => boolean"
      },
      {
        "description": "Viewport height in px (antd `height`, gh#1042). The tree scrolls inside it and, unless `virtual={false}`, renders only the rows in view plus a small overscan — the windowed rows stay a flat run of treeitems whose `aria-level`/`aria-setsize`/`aria-posinset` still describe the whole outline. Rows must share one height (the `size` tier).",
        "name": "height",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Set `false` to keep the `height` viewport but render every row (antd `virtual`).",
        "name": "virtual",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw the connector rails between a parent and its children (antd `showLine`). Off by default — a rail is chrome, and chrome defaults quiet.",
        "name": "showLine",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Rule between the rows: a hairline on every node's block-start except the outline's first, so a tree used as navigation inside a Card reads as a list instead of one block (gh#732). The rule runs the FULL width of the row at every depth — never indented per level, which would make the children read as a nested table. Colour is `--tree-divider-color` (default `hsl(var(--border))`). Off by default, like `showLine` — chrome defaults quiet. Not antd's name: antd's Tree has no such capability, and `bordered` means a grid's frame plus column rules elsewhere in this library.",
        "name": "divided",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw each node's `icon` (antd `showIcon`). `variant=\"directory\"` supplies folder/file glyphs when a node has no `icon` of its own.",
        "name": "showIcon",
        "type": "boolean"
      },
      {
        "defaultValue": "\"default\"",
        "description": "`directory` is antd's `<DirectoryTree>`: folder/file glyphs and a full-row selected band.",
        "name": "variant",
        "type": "\"default\" | \"directory\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Row height tier — the shared `--control-height` ladder, so rows stay in step with the controls beside them under every density.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "false",
        "description": "Disable the whole tree: nothing selects, checks or expands, and the nodes stay readable.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Classes applied to the `role=\"tree\"` container.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "HTML id on the `role=\"tree\"` container.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Accessible name of the tree. Required unless `aria-labelledby` names it from a visible heading.",
        "name": "aria-label",
        "type": "string"
      }
    ],
    "related": [
      "TreeList — REMOVED in 21.0.0, replaced by Tree. It was a flat `<ul>` whose `depth` only drove `margin-inline-start`: it LOOKED like a tree and had no expand/collapse, no `role=\"tree\"`, no keyboard and no selection contract. Migration: nest the flat `items` into `treeData` (`id`→`value`, `title`→`label`, `depth`→nesting) and pass `aria-label`. A genuinely static indented outline that never opens is a `Descriptions` or a `ListRow` stack, not a tree.",
      "TreeSelect — the same hierarchy INSIDE a Popover, as a form field. Use TreeSelect when the answer is a value in a form; use Tree when the hierarchy itself is the page.",
      "Cascader — a path picker across columns. Use it when the user walks one path to a leaf; use Tree when several branches are open at once.",
      "Accordion — single-level disclosure with rich panel content. It is not a hierarchy and has no tree keyboard model.",
      "ScrollArea — not needed around Tree: pass `height`, and the tree is its own (windowed) scroll viewport."
    ],
    "rules": [
      2,
      6,
      23,
      31,
      44,
      45
    ],
    "storyPath": "data-display/tree.tsx",
    "tagline": "The standalone WAI-ARIA tree view on a page (antd `Tree` / `DirectoryTree`) — nested `treeData`, a disclosure triangle per branch, roving-tabindex arrow navigation, optional tri-state checkboxes, async `loadData` and `draggable` reorder/reparent (antd `onDrop`). Reach for it whenever nodes expand, collapse or are navigated by keyboard; TreeSelect is the same hierarchy inside a Popover.",
    "usage": [
      "DO name the tree — pass `aria-label` (through your own `t()`) or `aria-labelledby`. A tree with no name is an unlabelled landmark for every screen reader user.",
      "DO rely on Tree for a cut label (gh#1308): a label that is actually truncated gets its full text as a `title` and unfolds while its row holds keyboard focus (not in a windowed tree, whose rows share one height — the `title` stays). It is re-measured when a label's box, the tree's width or the loaded fonts change — text spacing (WCAG 1.4.12) or a late web font included (gh#1318). No `titleRender` wrapper with a hand-set `title` is needed; one you set is kept.",
      "DO keep SELECTION and CHECKS apart: `value`/`onValueChange` is which node is open in the detail pane; `checkedValues`/`onCheckedValuesChange` is which nodes are ticked. They are two axes, exactly as in antd — never drive one from the other.",
      "DO keep SELECTION and EXPANSION apart too — this is the THIRD axis and the one gh#910 tripped on. `onValueChange` says which node a detail pane should show; `onExpandedValuesChange` says which branches are unfolded. A sibling pane listens to the first and ignores the second, so unfolding a branch cannot reload it. If your pane reloads when a branch opens, it is being driven from the wrong event — not from a missing feature.",
      "DO let the keyboard work: the tree ships the full APG contract (Up/Down through visible nodes, Right expands then descends, Left collapses then climbs, Home/End, Enter/Space, `*` to expand the current level, type-ahead). Do not add your own key handling on top.",
      "DON'T nest a Button, Checkbox, Link or any focusable control inside a node label. A tree item owns exactly ONE tab stop; the disclosure triangle and the tick box are decorative glyphs for that reason. Put row actions in a sibling column outside the tree, or open a detail pane on selection.",
      "DON'T hand-roll an indented `<ul>` (or a NavList / ListRow stack with a per-depth margin) for a hierarchy. A flat indented list only LOOKS like a tree: no expand/collapse, no `role=\"tree\"`, no keyboard model, no selection contract. `TreeList` was exactly that list and was REMOVED in 21.0.0 — Tree is what replaced it, and it is the one to reach for whenever nodes expand, collapse or are keyboard-navigated.",
      "DO pass `divided` when the tree IS the navigation of a page — a wiki/document outline or a section index sitting in a `Card` (`Card` > `CardContent flush` > `Tree divided`). Without a rule the rows run together and the outline reads as one block; with it, it reads as the ruled list ListRow and Table already give a flat list. Retint it per theme with `--tree-divider-color`, never with a per-page utility class.",
      "DO pass `height` for a long tree (antd `height` + `virtual`): the tree scrolls inside that height and keeps only the rows in view in the DOM, so a 421-child level renders a few dozen rows. Do not wrap it in `ScrollArea` as well — the tree is its own viewport.",
      "DO push fetched children into `treeData` from `loadData`; the tree calls it once per node that loads and shows a Skeleton row until the data lands. Reject the promise on failure — the branch folds shut and the next expand retries.",
      "DON'T wait for a paged \"load more\" node — antd's Tree has none, and `height` covers a long list. If the API itself is paged, append a leaf node such as `{ value: `${parent}::more`, label: t(\"…load more\"), isLeaf: true }` to the loaded children and fetch the next page from `onValueChange` when it is selected: it is a real treeitem, so it is keyboard-reachable and announced."
    ],
    "useCases": [
      "A permission tree: modules → resources → actions with tri-state checkboxes, where ticking a module ticks everything under it and a partly-granted module shows the dash.",
      "A category browser beside a detail pane — selecting a category loads its products, and the whole hierarchy stays navigable by keyboard.",
      "An organisation chart / department picker on a settings page, where a branch's children are fetched on demand with `loadData`.",
      "A file explorer (`variant=\"directory\"`, `showIcon`, `showLine`) where folders and files read differently and the selected row spans the width.",
      "A chart-of-accounts outline that must expand and collapse — the case a flat indented list only ever looked like it handled.",
      "A wiki / document page index used as the navigation of a screen, inside a Card: `divided` rules the rows so the outline reads as a list, and selection drives the reading pane."
    ]
  },
  {
    "example": "import {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Trash2Icon } from \"lucide-react\";\n\n// Icon-only button with tooltip label\nexport function DeleteAction() {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\" aria-label=\"Delete invoice\">\n          <Trash2Icon className=\"size-4\" />\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent side=\"top\">Delete invoice</TooltipContent>\n    </Tooltip>\n  );\n}\n\n// Controlled tooltip (e.g. force-open for a tutorial)\nexport function ControlledExample() {\n  const [open, setOpen] = React.useState(false);\n  return (\n    <Tooltip open={open} onOpenChange={setOpen}>\n      <TooltipTrigger asChild>\n        <Button variant=\"outline\" onMouseEnter={() => setOpen(true)}>\n          Hover me\n        </Button>\n      </TooltipTrigger>\n      <TooltipContent side=\"bottom\" sideOffset={8}>\n        This is a controlled tooltip\n      </TooltipContent>\n    </Tooltip>\n  );\n}",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "Tooltip",
    "props": [
      {
        "defaultValue": "200",
        "description": "Milliseconds of hover before the tooltip opens. Accepted on both Tooltip (per-instance) and TooltipProvider (subtree-wide override).",
        "name": "delayDuration",
        "type": "number"
      },
      {
        "description": "Controlled open state. Pair with onOpenChange to drive the tooltip programmatically.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Callback fired when the open state changes. Required when using controlled 'open'.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "defaultValue": "false",
        "description": "Uncontrolled initial open state.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "No effect on the React Aria base (kept for API compatibility): React Aria tooltips are never hoverable content, so they already close when the pointer leaves the trigger.",
        "name": "disableHoverableContent",
        "type": "boolean"
      },
      {
        "defaultValue": "\"top\"",
        "description": "On TooltipContent — preferred side for the tooltip panel. It flips automatically when there is not enough space.",
        "name": "side",
        "type": "'top' | 'right' | 'bottom' | 'left'"
      },
      {
        "defaultValue": "6",
        "description": "On TooltipContent — pixel gap between the trigger edge and the tooltip panel.",
        "name": "sideOffset",
        "type": "number"
      },
      {
        "defaultValue": "\"center\"",
        "description": "On TooltipContent — alignment relative to the trigger along the cross-axis.",
        "name": "align",
        "type": "'start' | 'center' | 'end'"
      },
      {
        "defaultValue": "0",
        "description": "On TooltipContent — pixel offset applied to the align position.",
        "name": "alignOffset",
        "type": "number"
      },
      {
        "description": "On TooltipContent — extra Tailwind classes merged with the built-in panel styles (z-50, max-w-xs, rounded-md, shadow-md, text-xs).",
        "name": "className",
        "type": "string"
      },
      {
        "description": "On TooltipContent — the text or JSX rendered inside the floating panel.",
        "name": "children",
        "required": true,
        "type": "React.ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "On TooltipTrigger — merges props onto the immediate child instead of wrapping with a <button>. Use when the trigger is already an interactive element.",
        "name": "asChild",
        "type": "boolean"
      }
    ],
    "related": [
      "Popover — use Popover (also @godxjp/ui/feedback) when the floating panel needs interactive content (forms, links, action menus) rather than read-only text. Tooltip is read-only; Popover is interactive.",
      "Popover — `openOn=\"hover\"` for rich preview cards (user profiles, link previews) that appear on hover with more complex layout. Tooltip is for short text hints only.",
      "Badge — for persistent, always-visible short labels inline with text; not hover-triggered. Use Tooltip when the hint should be hidden until hovered."
    ],
    "rules": [
      3,
      6,
      23,
      39
    ],
    "storyPath": "feedback/tooltip.tsx",
    "subParts": [
      "TooltipContent",
      "TooltipProvider",
      "TooltipTrigger"
    ],
    "tagline": "React Aria hover/focus tooltip — self-providing, no app-level TooltipProvider required; compose Tooltip > TooltipTrigger > TooltipContent every time.",
    "usage": [
      "DO compose the full three-part structure every time: <Tooltip> wraps <TooltipTrigger> (the element that triggers the tip) and <TooltipContent> (the floating panel). Omitting any part silently produces nothing.",
      "DO NOT add an app-level <TooltipProvider> — every <Tooltip> self-provides its own provider. Only add <TooltipProvider> at a subtree root when you need a shared delayDuration different from the default 200ms across many tooltips.",
      "DO use asChild on TooltipTrigger when the trigger is already a Button, IconButton, or other interactive element — this avoids nesting a <button> inside a <button>, which is invalid HTML and breaks keyboard focus.",
      "DON'T put non-interactive elements (plain <div>, <span>) as the direct TooltipTrigger child without asChild — the trigger needs a focusable element for keyboard accessibility. Wrap the target in a <span tabIndex={0}> or use a Button.",
      "For controlled usage (e.g. programmatic show/hide or testing), pass open + onOpenChange to <Tooltip>. For typical hover/focus behaviour, leave both unset (uncontrolled).",
      "TooltipContent renders in a portal appended to document.body — z-index and overflow:hidden on ancestors do NOT clip it. Use className to extend max width beyond the built-in max-w-xs if long text is expected."
    ],
    "useCases": [
      "Explaining an icon-only Button action (e.g. a trash icon, a copy-to-clipboard icon) in a DataTable action column — show the label on hover without cluttering the row.",
      "Annotating a truncated cell value in a DataTable (e.g. a long account name clipped with text-ellipsis) — reveal the full text on hover without a modal.",
      "Providing contextual help for a form field label or an info icon next to an accounting term (e.g. 'AR Balance' with a short definition).",
      "Surfacing keyboard shortcut hints next to toolbar buttons (e.g. '⌘K — open search') without adding visible text to the UI.",
      "NOT for a disabled control's reason. A disabled Button gets `pointer-events: none` (control.css) and leaves the tab order, so a tooltip on one can never open — measured in Chromium: the same trigger showed 1 tooltip on hover while enabled and 0 while disabled, and `.focus()` on it left activeElement as BODY, so keyboard and touch get nothing either. Render the reason as VISIBLE TEXT before the action instead — that is what ServiceLauncherCard's `disabledReason` does, and it puts the explanation ahead of the control in DOM order (WCAG 1.3.2). See docs/CONSUMER-RULES.md."
    ]
  },
  {
    "example": "import { PrefetchLink } from \"@godxjp/ui/react-router\";\nimport { fetchInvoice } from \"@/api/invoices\";\n\n// Inside a table row or list item:\n<PrefetchLink\n  to={`/invoices/${invoice.id}`}\n  queryKey={[\"invoice\", invoice.id]}\n  queryFn={() => fetchInvoice(invoice.id)}\n  staleTime={60_000}\n  className=\"font-medium hover:underline\"\n>\n  {invoice.number}\n</PrefetchLink>\n\n// Disable prefetch for rows where data is not yet stable:\n<PrefetchLink\n  to={`/invoices/${invoice.id}`}\n  queryKey={[\"invoice\", invoice.id]}\n  queryFn={() => fetchInvoice(invoice.id)}\n  prefetchOn=\"none\"\n>\n  {invoice.number}\n</PrefetchLink>",
    "group": "data-display",
    "importPath": "@godxjp/ui/react-router",
    "name": "PrefetchLink",
    "props": [
      {
        "description": "TanStack Query cache key for the data to prefetch. Must match the key used by the destination page's useQuery call.",
        "name": "queryKey",
        "required": true,
        "type": "QueryKey"
      },
      {
        "description": "Fetch function passed to queryClient.prefetchQuery. Same function (or equivalent) as the one used in the destination page's useQuery.",
        "name": "queryFn",
        "required": true,
        "type": "() => Promise<unknown>"
      },
      {
        "defaultValue": "\"both\"",
        "description": "Which interaction triggers prefetching. 'both' fires on either mouseenter or focus. 'none' disables prefetching entirely (useful for conditional opt-out without unmounting the component).",
        "name": "prefetchOn",
        "type": "\"hover\" | \"focus\" | \"both\" | \"none\""
      },
      {
        "defaultValue": "30000",
        "description": "Milliseconds before the prefetched cache entry is considered stale. Matches queryClient.prefetchQuery staleTime. Defaults to 30 s — set higher for rarely-changing data (e.g. reference tables), lower for live feeds.",
        "name": "staleTime",
        "type": "number"
      },
      {
        "description": "All standard React Router v6 Link props are spread through: to, replace, state, relative, reloadDocument, preventScrollReset, viewTransition, className, children, etc. onMouseEnter and onFocus are merged (your handler still fires).",
        "name": "...linkProps",
        "type": "LinkProps (react-router-dom)"
      }
    ],
    "related": [
      "Link (react-router-dom) — use bare Link when no prefetching is needed or when the destination has no TanStack Query data (e.g. a static page or a form page that fetches nothing on load).",
      "DataState — the companion lifecycle widget for the destination page; ensures PrefetchLink's prefetch is consumed correctly via useQuery.",
      "InfiniteQueryState — use instead of PrefetchLink when the list itself is infinitely paginated and items are loaded lazily rather than navigated to.",
      "ButtonRefetch — for triggering a manual cache refresh on an already-loaded page, not for navigation prefetching."
    ],
    "rules": [
      2,
      3,
      31
    ],
    "storyPath": "react-router/prefetch-link.tsx",
    "tagline": "React Router Link that fires prefetchQuery on hover/focus so detail pages feel instant — requires a TanStack Query QueryClient in context. Imported from `@godxjp/ui/react-router` (not `/query`) because it imports the optional `react-router-dom` peer; in `/query` it broke every router-less app's build (gh#996).",
    "usage": [
      "DO: provide a queryKey that exactly matches the destination page's useQuery key — a mismatch means the prefetch populates a different cache slot and the page still loads cold.",
      "DO: keep queryFn lightweight and side-effect-free; it runs speculatively on hover. Avoid mutations or write operations inside queryFn.",
      "DO: tune staleTime to your data's freshness requirement. The default 30 s is fine for most detail views; set it lower (e.g. 5000) for real-time data or higher (e.g. 300_000) for static reference data.",
      "DON'T: use PrefetchLink when the destination page's data is user-specific per request (e.g. contains a nonce or CSRF token embedded in the payload) — the prefetched response may be stale or unusable.",
      "DON'T: hand-roll a Link + useQueryClient prefetch pattern when PrefetchLink already ships it. Using raw Link loses the hover/focus prefetch behaviour and duplicates logic.",
      "REQUIRES: a TanStack Query QueryClient provider (QueryClientProvider) above this component in the tree. It calls useQueryClient internally — rendering without a provider throws."
    ],
    "useCases": [
      "List-to-detail navigation in an accounting app: hovering an invoice row in a DataTable triggers prefetch of that invoice's detail data so the detail page renders immediately on click.",
      "Sidebar navigation links where the destination is a dashboard or summary page — prefetch on focus covers keyboard-only users tabbing through the nav.",
      "Paginated table rows where each row links to a resource detail page (partner, journal entry, account). prefetchOn='hover' avoids wasted prefetches from keyboard navigation.",
      "Admin list pages with 'Edit' action links: the edit form data is prefetched on hover so the form appears populated without a loading spinner.",
      "Breadcrumb links on deep-nested pages where clicking 'back' should feel instant — prefetch the parent page's query on mount/focus of the breadcrumb link."
    ]
  },
  {
    "example": "import { Avatar, AvatarFallback, AvatarImage } from \"@godxjp/ui/data-display\";\n\n<Avatar>\n  <AvatarImage src=\"/user.png\" alt=\"User\" />\n  <AvatarFallback>UI</AvatarFallback>\n</Avatar>\n\n// Organization entity header mark\n<Avatar shape=\"square\">\n  <AvatarFallback>山</AvatarFallback>\n</Avatar>\n\n// A chat member with realtime presence — dot + localized sr-only text, never colour alone\n<Avatar presence=\"busy\">\n  <AvatarImage src=\"/rei.png\" alt=\"佐藤 玲\" />\n  <AvatarFallback>佐</AvatarFallback>\n</Avatar>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Avatar",
    "props": [
      {
        "defaultValue": "\"circle\"",
        "description": "Identity geometry. `circle` (default, inert) is the PERSON avatar — the round --radius-pill mark on the muted surface. `square` is the ENTITY-HEADER organization/service mark: a compact rounded square on the brand surface, whose radius, box size, fill and glyph colour are all --avatar-square-{radius,size,background,foreground} tokens. Pick the shape by WHAT the mark represents; never hand-roll it with className=\"rounded-md bg-primary\".",
        "name": "shape",
        "type": "\"circle\" | \"square\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Box size on the SHARED CONTROL LADDER — md 32px (--control-height, the inert default), sm 28px, xs 24px, lg 36px, the same --control-height-* tier Button and Input read. STATE IT WHENEVER THE ROW'S HEIGHT IS ALREADY DECIDED: a mark inside a `<Button size=\"icon-sm\">` trigger, a mark in a 24/28px dense table row, a mark beside a `size=\"sm\"` Button. A `size=\"sm\"` avatar measures exactly as tall as a `size=\"sm\"` Button, so the row stays level. Before gh#716 the box was welded to --control-height: a 32px mark inside a 28px icon-sm trigger OVERFLOWED it, and a consumer sweeping control heights across 38 routes had no legal move but to raise the whole row to 32px. The initials' type step and a glyph's box move WITH the box (one step of the type scale, one step of the --icon-size-* scale), and shape=\"square\" rides the identical ladder — so a small mark is small, never clipped. A size BEYOND the control ladder (a 96px profile mark) is still a className size utility; the prop is for the control row.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Fill treatment, ORTHOGONAL to `shape`. `default` (inert) is the identity fill — --muted for a person, the solid brand mark for `shape=\"square\"`. `tinted` is the CAPABILITY MEDALLION: a soft role wash behind a role-coloured glyph, with the glyph sized by the component. `shape=\"square\" appearance=\"tinted\"` is the canonical rounded-square medallion a feature/capability icon sits on.",
        "name": "appearance",
        "type": "\"default\" | \"tinted\""
      },
      {
        "description": "Realtime reachability, drawn as an indicator at the block-end/inline-end corner of the mark. Ships the dot, its geometry tokens and a localized sr-only label TOGETHER, so presence is never colour-only (WCAG 1.4.1): each value also has its own silhouette — online = filled disc (--success), away = half-filled (--warning), busy = filled + a horizontal do-not-disturb bar (--destructive), offline = hollow ring (--muted-foreground). OMIT the prop for an entity with no presence concept (an organization mark, a capability medallion): no node and no attribute are emitted. `presence=\"offline\"` is the DIFFERENT, positive statement \"known to be unreachable\" — the same distinction ListRow draws between an omitted and a `false` `unread`.",
        "name": "presence",
        "type": "\"online\" | \"away\" | \"busy\" | \"offline\""
      },
      {
        "description": "Override the localized presence text (t(\"dataDisplay.avatar.presence.online\") …) when the product has a more precise phrasing (\"In a meeting until 15:00\"). Visually hidden either way — a presence dot never carries visible text; that is Badge status.",
        "name": "presenceLabel",
        "type": "string"
      },
      {
        "description": "Compose AvatarImage and AvatarFallback.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Extra classes on the avatar root.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Badge — use beside Avatar for role/status metadata. Badge `status` is the LIFECYCLE chip (a record's state, with a visible label); Avatar `presence` is the realtime person dot. Different vocabularies on purpose — do not swap them.",
      "ListRow — its `unread` prop is the same bargain one level up (a dot + localized sr-only text + tokens). Use ListRow `unread` for a row's read state, Avatar `presence` for the person on it."
    ],
    "rules": [
      3,
      35
    ],
    "storyPath": "data-display/avatar.tsx",
    "subParts": [
      "AvatarFallback",
      "AvatarImage"
    ],
    "tagline": "Avatar mark with image and fallback slots for users, teams, and entities.",
    "usage": [
      "DO compose Avatar > AvatarImage + AvatarFallback so broken or missing images still show a readable fallback.",
      "DO pass `fit=\"contain\"` on AvatarImage for a logo or app icon that is wide, transparent or off-centre — the default `cover` crops to fill, right for a face (gh#1176). A full-bleed square icon (the GoDX service icon set) keeps the default.",
      "DO use `presence` for who is reachable RIGHT NOW — a chat member list, a DM row, a message-stream author, the topbar account mark. It is the only supported way to put a status dot on an avatar: the dot's inset is a function of the mark's own --avatar-* radius/size and of the root's clip, neither of which a page can read, which is why the hand-rolled `<span className=\"relative\"><Avatar/><span className=\"absolute -end-0.5 -bottom-0.5 size-2.5 rounded-full bg-green-500 ring-2 ring-background\"/></span>` wrapper can never be got right from outside (and fails the DS audit on the raw palette).",
      "DON'T announce a presence CHANGE from the avatar — it carries no aria-live on purpose. A roster of 40 marks resyncing over a socket would flood a screen reader; announce the change in the consumer's own live region if the product wants it.",
      "DON'T reach for `Badge status` for presence, or `presence` for lifecycle. Presence is volatile, per-person and realtime and renders as a dot; a lifecycle status is a record's state and renders as a labelled chip.",
      "DO render a capability / feature icon as `<Avatar shape=\"square\" appearance=\"tinted\"><AvatarFallback><Sparkles aria-hidden /></AvatarFallback></Avatar>` — the medallion is a COMPOSITION (Avatar + a Lucide glyph, per docs/COMPOSITION-VS-COMPONENT.md), and `appearance=\"tinted\"` is the token-owned tint that composition needs. A BARE glyph beside a card title, or a hand-derived `hsl(var(--primary) / 0.1)` plate in page CSS, are both the anti-pattern this replaces. Left-aligned capability card: `<Card><CardHeader><Flex align=\"center\" gap={3}><Avatar shape=\"square\" appearance=\"tinted\">…</Avatar><CardTitle>…</CardTitle></Flex></CardHeader>…`. The row is a `Flex`, NOT `className=\"flex flex-row items-center gap-3\"` on the CardHeader — that spelling shipped here for a while and it is two ui-audit errors in a consumer (`no-utility-layout` + `no-utility-spacing`), i.e. this catalog was prescribing what the audit forbids. `CardTitle` has no `icon` prop and does not need one: the medallion is its SIBLING, not its content, and inside CardTitle it would get neither the gap nor the glyph sizing.",
      "DO use `shape=\"square\"` for an organization / service / tenant mark in an entity header, and keep the default `shape=\"circle\"` for people. The square appearance already carries the brand surface and an AA-contrast glyph colour — a className/colour override on the call site is never needed (and is forbidden by the API-first redesign policy).",
      "DON'T retune the entity mark per call site: set --avatar-square-{radius,size,background,foreground} ONCE in the service theme (e.g. --avatar-square-background: hsl(var(--muted)) for a neutral mark).",
      "DON'T use Avatar for decorative thumbnails; use CardCover or an img when the image is content rather than identity.",
      "DO set `size` from the ROW, not from the mark: a row whose height is already fixed (an icon-sm trigger, a 24/28px dense table row, a toolbar of sm controls) takes the matching avatar step, and the mark then measures exactly what a Button of the same step measures. `<Button size=\"icon-sm\"><Avatar size=\"sm\">…</Avatar></Button>` fits; the default md mark in that same trigger overflows it.",
      "DON'T raise a whole row's control height just to fit a person's mark, and DON'T reach for a className size utility for a step that is ON the ladder (size-6 / size-7 re-derive 24/28px outside the density axis, so they stop tracking --control-height and drift from the controls beside them). className stays the escape hatch for sizes OFF the ladder — a 96px profile mark."
    ],
    "useCases": [
      "User profile chips",
      "Team member lists",
      "Account owner cells in a DataTable",
      "Organization / service entity headers (shape=\"square\" beside the entity name and code)",
      "Chat / collaboration people surfaces (presence=\"online|away|busy|offline\" on DM rows, channel member lists, message-stream authors)"
    ]
  },
  {
    "example": "import { formatDate } from \"@godxjp/ui/datetime\";\nimport { Separator } from \"@godxjp/ui/layout\";\n\n// Plain section rule — decorative, nothing announced.\n<Separator />\n\n// Day divider: the label is the calendar boundary, formatted on the active locale.\n<Separator label={formatDate(\"2026-08-22\", { kind: \"long\" })} labelAlign=\"start\" />\n\n// Unread watermark: content, not decoration — announced once as the separator's name.\n<Separator label={t(\"chat.newMessages\")} tone=\"primary\" />",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "Separator",
    "props": [
      {
        "description": "Hide from viewport breakpoint.",
        "name": "hideFrom",
        "type": "BreakpointProp"
      },
      {
        "description": "Hide below viewport breakpoint.",
        "name": "hideBelow",
        "type": "BreakpointProp"
      },
      {
        "description": "Block spacing on the token scale.",
        "name": "space",
        "type": "GapProp"
      },
      {
        "description": "Label typography step.",
        "name": "labelSize",
        "type": "\"2xs\" | \"xs\" | \"sm\" | \"md\""
      },
      {
        "defaultValue": "\"horizontal\"",
        "description": "Divider direction.",
        "name": "orientation",
        "type": "\"horizontal\" | \"vertical\""
      },
      {
        "description": "Localized text that interrupts the rule — the rule splits into two halves around it. Horizontal only (ignored with a dev warning when vertical). A string, not a node, because it becomes the separator's ACCESSIBLE NAME. Omit for a plain rule.",
        "name": "label",
        "type": "string"
      },
      {
        "defaultValue": "\"center\"",
        "description": "Where the label sits on the rule. `center` is the classic conjunction; `start` is the Slack/Mattermost stream convention. Logical — it flips under dir=\"rtl\".",
        "name": "labelAlign",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Semantic emphasis of the label AND the rule together (never colour-only). `default` is the quiet chrome; use a role for an attention rule such as an unread watermark.",
        "name": "tone",
        "type": "\"default\" | \"muted\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\" | \"inherit\""
      },
      {
        "defaultValue": "true (false when `label` is set)",
        "description": "Whether the separator is decorative for assistive tech. A `label` flips the default to false so the rule becomes a real role=\"separator\" named by the label.",
        "name": "decorative",
        "type": "boolean"
      }
    ],
    "related": [
      "Flex direction='col' — use for vertical spacing without a visible rule.",
      "AuthDivider — the auth-scoped preset over `Separator label` (it only re-points the --separator-* knobs at the --auth-shell-divider-* layer)."
    ],
    "rules": [
      2,
      3,
      44,
      45
    ],
    "storyPath": "layout/separator.tsx",
    "tagline": "Tokenized horizontal or vertical rule, optionally INTERRUPTED by a localized label (day divider, unread watermark, auth conjunction).",
    "usage": [
      "DO use Separator for section dividers instead of raw border divs.",
      "DO set orientation='vertical' only when the parent gives it a stable height.",
      "DO use `label` for a message stream's day divider or a \"new messages\" unread watermark — it is the generic primitive AuthDivider was being misused for. Never hand-roll a <div> grid of two <span> rules.",
      "DO format a day divider's date with the package's Intl/CLDR date subsystem (`formatDate(iso, { kind: \"long\" })` from @godxjp/ui/datetime) on the active locale — never hand-build the string.",
      "DO pass `label` as a t() string. It becomes the separator's accessible name, and the visible node is aria-hidden so it is announced EXACTLY once.",
      "DON'T set `decorative` on a labelled rule unless you mean it: role=\"none\" cannot carry a name, so the rule stops being announced as a milestone.",
      "DON'T reach for AuthDivider outside an auth form — it is the auth-scoped preset over this and drags the --auth-shell-divider-* micro-scale with it.",
      "DON'T bake the rule weight, the label gap/inset or the label type ramp into page CSS — they are --separator-* component tokens (rules #44/#45); retune them from theme.css."
    ],
    "useCases": [
      "Separating toolbar groups",
      "Dividing stacked page sections",
      "Vertical split between metadata groups",
      "A message stream's calendar-boundary day divider",
      "A \"new messages\" unread watermark above the first unread post"
    ]
  },
  {
    "absorbed": [
      "SkeletonAvatar",
      "SkeletonButton",
      "SkeletonNode",
      "SkeletonImage"
    ],
    "example": "import { Skeleton } from \"@godxjp/ui/feedback\";\n\n<Skeleton className=\"h-6 w-48\" />\n\n// The same component is the antd namespace:\n<Skeleton.Input size=\"sm\" />",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "Skeleton",
    "props": [
      {
        "description": "Size and layout classes for the block.",
        "name": "className",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Swap the resting pulse for a travelling sheen (Ant Design's `active`). The block already pulses without it — `active` picks the louder of the two motions, it does not turn motion on. Both stop under prefers-reduced-motion.",
        "name": "active",
        "type": "boolean"
      },
      {
        "description": "Pass `false` to render `children` INSTEAD of the placeholder, so a call site can swap without a ternary. Omitting it keeps the placeholder (Ant Design's `loading || !('loading' in props)`).",
        "name": "loading",
        "type": "boolean"
      }
    ],
    "related": [
      "Activity — the ambient 'working…' mark. Skeleton is a SHAPE standing in for content that has not arrived; Activity is motion beside content that is already there."
    ],
    "rules": [
      3,
      31
    ],
    "storyPath": "feedback/skeleton.tsx",
    "subParts": [
      "SkeletonTable",
      "SkeletonRows",
      "SkeletonDetail",
      "SkeletonStat",
      "SkeletonArticle",
      "SkeletonInput",
      "SkeletonForm"
    ],
    "tagline": "Base pulsing skeleton block, the namespace the presets hang off (Skeleton.Input / .Article), and the ONE entry for every loading shape: SkeletonTable / SkeletonRows / SkeletonDetail / SkeletonStat / SkeletonArticle / SkeletonInput / SkeletonForm.",
    "usage": [
      "THE SHAPES (one family, one entry): SkeletonTable — Loading placeholder matching the DataTable layout (header + N rows). Drop-in while data loads (deferred props).",
      "SkeletonRows — Repeated flat loading lines on a responsive grid — the list-shaped placeholder when the final layout is rows of short bars, not a table header, not avatar + prose.",
      "SkeletonDetail — Fixed-shape placeholder for a RECORD page — title, subtitle, then a bordered box of six label/value pairs. Takes no props: the shape IS the contract, so every detail screen loads identically.",
      "SkeletonStat — Fixed-shape placeholder for ONE KPI tile — label, big value, caption. Takes no props, so a row of tiles loads at a uniform height and nothing reflows when the numbers arrive.",
      "SkeletonArticle — Avatar + heading line + paragraph placeholder — Ant Design's own <Skeleton> shape, for a comment, a profile block or a feed item.",
      "SkeletonInput — Field-shaped placeholder (also Skeleton.Input) — five control-heights wide.",
      "SkeletonForm — The skeleton of a `Form columns={N}` — label + control PAIRS on the form's own ResponsiveGrid, not the flat line list SkeletonRows draws.",
      "antd's Skeleton.Avatar / .Button / .Node / .Image presets are not components here (retired in v32, 0 users): the avatar box lives on as `SkeletonArticle avatar`, the control box as `SkeletonInput`; a media slot is a sized `Skeleton` block.",
      "DO use Skeleton for a custom block when SkeletonRows/Table/Stat/Article do not match the final layout.",
      "DO reach for a shaped preset before sizing a bare block by hand: SkeletonButton, SkeletonInput, SkeletonAvatar, SkeletonNode and SkeletonImage already carry the box of the control they stand in for, from the --control-height tier.",
      "DON'T use a spinner overlay for skeletonable page content.",
      "DON'T reuse Skeleton as an ambient 'something is happening' mark — it hard-codes aria-busy + aria-live because it means CONTENT IS LOADING. Use Activity for ambient motion."
    ],
    "useCases": [
      "Single loading line",
      "Custom card media placeholder",
      "Inline metadata placeholder"
    ]
  },
  {
    "example": "import { Toggle } from \"@godxjp/ui/data-entry\";\n\n<Toggle aria-label=\"Bold\">B</Toggle>\n\n// A counted filter chip — one control, one accessible name (\"Unread, 12 items\").\n// Drive the state with the usual pair: pressed / defaultPressed / onPressedChange.\n// variant=\"soft\" + shape=\"pill\" is the antd Tag.CheckableTag shape: it has a RESTING fill, so\n// the chip is legible before it is pressed.\n<Toggle\n  variant=\"soft\"\n  shape=\"pill\"\n  onPressedChange={setUnreadOnly}\n  count={12}\n  countLabel={t(\"common.items\")}\n>\n  {t(\"inbox.unread\")}\n</Toggle>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Toggle",
    "props": [
      {
        "description": "Controlled pressed state.",
        "name": "pressed",
        "type": "boolean"
      },
      {
        "description": "Pressed-state callback.",
        "name": "onPressedChange",
        "type": "(pressed: boolean) => void"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Visual style. `default` is a 1px TRANSPARENT border (a toolbar affordance — no resting surface at all), `outline` a hairline on --background, and `soft` (gh#734) the REST FILL a chip needs: hsl(var(--secondary)), byte-identical to the fill `Badge variant=\"secondary\"` and `Button variant=\"secondary\"` already paint, so a chip, a badge and a button on one row are one family. `soft` is antd Tag's resting surface and is what makes a FILTER CHIP legible before it is pressed — with `default` or `outline` the chip reads as transparent, which is the reported defect. Hover is the opaque --secondary-hover (not a translucent tint, which would shift between a page and a Card); pressed still wins over both. `quiet` (gh#1257) is the LOW-KEY pressed state for a mark beside content (a comment star, a reaction): the transparent rest of `default`, and pressed as a --secondary fill with --foreground text and the primary hairline, instead of a solid primary block; knobs `--toggle-quiet-on-{background,color,count-background,count-color}`.",
        "name": "variant",
        "type": "\"default\" | \"outline\" | \"soft\" | \"quiet\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control size on the --control-height tier: xs 24px · sm 28px · md 32px · lg 36px · touch ≥56×56 (gh#1304, the glove step, `--control-height-touch`). Pick the step the ROW already has — an xs chip is what lets a 24px-dense row carry a pressed/segmented control instead of someone hand-rolling one out of Buttons (gh#716). Every step clears the WCAG 2.2 SC 2.5.8 24px target floor, xs exactly.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"touch\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Corner shape, the SAME three values and the same two radius tokens `Button` and `Badge` use, so `shape=\"pill\"` means one thing across the library (gh#734). `pill` + `variant=\"soft\"` is the antd-Tag filter chip. On a ToggleGroup it propagates to every item through context, because a chip row is a row of pills, not one pill among squares.",
        "name": "shape",
        "type": "\"default\" | \"pill\" | \"sharp\""
      },
      {
        "description": "Optional numeric count rendered as a borderless counter pill after the label — the SAME vocabulary Button defines (count/overflowCount/showZero), so a counted filter tab and a counted PRESSED chip read identically. This is what makes a faceted filter chip ('Open 42', selected or not) or a reaction chip ONE control: one button[aria-pressed], one tab stop, one focus ring, one accessible name. Formatted with Intl.NumberFormat in the active locale — never String(n), never a hand-rolled separator. NEVER nest a Badge inside a Toggle for this (a Badge is a status chip with its own surface: it double-borders the chip and puts two boxes where there is one control), and never hand-write a <span> counter inside a Toggle (that re-derives the Intl formatting, the cap and the pill's type ramp page-side).",
        "name": "count",
        "type": "number"
      },
      {
        "defaultValue": "99",
        "description": "Cap for `count` — above it the pill shows `{overflowCount}+` (e.g. 99+). The cap itself is locale-formatted too (vi: 1.000+). Same default and semantics as Button.",
        "name": "overflowCount",
        "type": "number"
      },
      {
        "defaultValue": "true (false for variant=\"quiet\")",
        "description": "Render the pill when `count` is 0. Same default as Button — a filter chip saying \"Open 0\" is information. `variant=\"quiet\"` defaults to false (gh#1275): a quiet toggle is a mark beside content, and a \"0\" beside an unstarred star is noise. Set it explicitly to override either way.",
        "name": "showZero",
        "type": "boolean"
      },
      {
        "description": "Localized description of what the count MEANS, folded into the accessible name so the control never announces as a bare number. The name always comes out as '<label>, <count> <unit>' — from contents for a text chip ('Unread, 12 items'), or folded into `aria-label` for an icon/emoji chip ('thumbs up, 3 reactions'). REQUIRED in practice whenever the visible label is an icon or emoji. Pass a t()-resolved string; the library does not own this wording.",
        "name": "countLabel",
        "type": "string"
      }
    ],
    "related": [
      "ToggleGroup",
      "Button",
      "Badge — the REMOVABLE chip (`onRemove` = antd Tag `closable`). Toggle is the SELECTABLE chip. Between them they cover antd's Tag family, which is why no `Tag` component exists here."
    ],
    "rules": [
      3,
      13,
      45
    ],
    "storyPath": "data-entry/toggle.tsx",
    "tagline": "React Aria toggle button with default/outline/soft variants and tokenized sizes.",
    "usage": [
      "DO use `variant=\"quiet\"` for a pressed state that should stay LOW-KEY — a comment star, a like/reaction, a pin beside a row. The solid primary fill is for a selected filter or mode; on a mark beside content it reads as the loudest thing on the screen. No app CSS needed (gh#1257).",
      "DO provide an accessible label when the toggle only contains an icon.",
      "DON'T use Toggle for multi-option selection; use ToggleGroup.",
      "DO reach for `count` for a faceted filter chip, a counted segmented toggle or a reaction chip — Toggle is the primitive that owns BOTH the count and the pressed state.",
      "DON'T write `<Button count={n} aria-pressed>` for this: buttonVariants has no pressed branch, so the state paints nothing and the chip is indistinguishable from an unpressed one.",
      "DON'T nest a Badge (or a hand-written span) inside a Toggle to show a count — use `count`.",
      "DO pass `countLabel` whenever the label is an icon or an emoji, so the chip does not announce as a bare number.",
      "DON'T wrap the chip in a live region to announce count changes — Toggle deliberately does not, because a count driven by other people is not this control's status.",
      "DO build a FILTER CHIP (antd `Tag.CheckableTag`) as `<Toggle variant=\"soft\" shape=\"pill\" count countLabel>` — that IS the chip: a resting --secondary fill, a pill corner, a real pressed state and the counter pill, in one control with one tab stop. There is no separate `Tag` / `CheckableTag` export and there will not be one (gh#734).",
      "DON'T substitute `<Button variant=\"secondary\" shape=\"pill\" aria-pressed>` for it: Button has no pressed branch, so the chip paints identically selected and unselected (WCAG 1.4.1).",
      "DO reach for `Badge onRemove` instead when the chip is REMOVABLE rather than selectable — an applied-filter chip that draws its own × is antd `Tag closable`, and this library put that on Badge, not on Toggle."
    ],
    "useCases": [
      "Bold/italic toolbar buttons",
      "Pinned filter toggles",
      "Compact view mode buttons",
      "Faceted filter chips with facet sizes (Open 42 / Closed 118)",
      "A tag-filter panel — `variant=\"soft\" shape=\"pill\"` chips carrying tag counts (antd Tag.CheckableTag)",
      "Reaction chips (emoji + how many reacted + whether YOU did)"
    ]
  },
  {
    "example": "import { ToggleGroup, ToggleGroupItem } from \"@godxjp/ui/data-entry\";\n\n// size/variant are set ONCE on the group and reach every item.\n<ToggleGroup type=\"single\" size=\"lg\" variant=\"outline\" defaultValue=\"left\" aria-label=\"Alignment\">\n  <ToggleGroupItem value=\"left\">Left</ToggleGroupItem>\n  <ToggleGroupItem value=\"center\">Center</ToggleGroupItem>\n</ToggleGroup>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "ToggleGroup",
    "props": [
      {
        "description": "Selection mode.",
        "name": "type",
        "required": true,
        "type": "\"single\" | \"multiple\""
      },
      {
        "description": "Controlled selected value(s).",
        "name": "value",
        "type": "string | string[]"
      },
      {
        "description": "Uncontrolled initial value(s).",
        "name": "defaultValue",
        "type": "string | string[]"
      },
      {
        "description": "Selection callback.",
        "name": "onValueChange",
        "type": "(value: string | string[]) => void"
      },
      {
        "defaultValue": "\"default\"",
        "description": "Visual style, PROVIDED TO EVERY ITEM via context — set it once on the group, not on each ToggleGroupItem. An explicit `variant` on an item still wins. The default is applied per item by toggleVariants, so an unset group emits no `data-variant` at all. `soft` (gh#734) is the chip fill: use it for a tag-filter panel, where `default`/`outline` leave the unselected chips reading as transparent.",
        "name": "variant",
        "type": "\"default\" | \"outline\" | \"soft\" | \"quiet\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Corner shape, PROVIDED TO EVERY ITEM via context exactly as `variant`/`size` are — a chip row is a row of pills, so the decision belongs to the row (gh#734). An explicit `shape` on an item still wins. Same three values and same radius tokens as Button and Badge.",
        "name": "shape",
        "type": "\"default\" | \"pill\" | \"sharp\""
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control size, PROVIDED TO EVERY ITEM via context — set it once on the group. An explicit `size` on an item still wins. Heights come from the --control-height tier (xs 24px · sm 28px · md 32px · lg 36px). Pick the step the ROW already has: xs is the one that fits a 24px-dense row (gh#716). `touch` (gh#1304) is the glove step: every item at least 56×56 CSS px and the items spaced `--control-touch-gap` (8px) apart, for wet or gloved hands.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"touch\""
      },
      {
        "description": "Disables the whole group; individual items also accept `disabled`.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "May the group end up with NOTHING selected? Omitted it may — pressing the selected item again clears it and reports `\"\"`, which is what a tag-filter row wants. THIS PROP DECIDES THE ARIA ROLE of a type=single group (gh#744), because emptiness is the one thing the two candidate roles disagree about: ARIA has no press-again-to-deselect for a radio, so a radiogroup the user just emptied is a state a screen reader cannot read out. Omitted → `role=\"group\"` + `aria-pressed` per item (no `aria-orientation`, which `group` does not take), arrow keys move FOCUS and Space/Enter presses. Set → `role=\"radiogroup\"` + `role=\"radio\"` / `aria-checked`, the selected item is the single tab stop, arrow keys move the SELECTION as APG requires, and pressing the selected item again keeps it. The name is React Aria's own (`useToggleGroupState`); neither antd nor Radix names the capability, and Radix's own single group emits radio roles while still allowing empty — the divergence is recorded in docs/DESIGN-AUTHORITY.md. On type=multiple it only keeps the last item selected; the roles do not move.",
        "name": "disallowEmptySelection",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Let the row break onto further lines instead of running past its rail (gh#741) — the SAME name, boolean shape and `data-wrap` attribute `Flex` carries. OPT-IN, and the same default for every `variant`, both decided by measurement: a twelve-chip soft/pill xs row in a 320px rail is scrollWidth 644 > clientWidth 320 and 171.5px tall without it (chips squeezed to min-content, labels broken over up to seven lines) and scrollWidth 320 = clientWidth 320, 108px, 4 lines with it — while forcing wrap on all 18 groups of the docs page changed NONE of the 17 that already fit, at a 320px and a 1358px rail. So a default of true could only reflow rows that overflow today, silently, on an upgrade; and `variant=\"soft\"` gets no different default because forcing wrap changed nothing across default/outline/soft either — paint is not the axis that decides, content width against the rail is. The wrapped lines keep the group's single `gap`, so both axes measure the same 4px, and `flex-wrap` reorders nothing: arrow keys still walk the items in DOM order across lines, in LTR and in RTL.",
        "name": "wrap",
        "type": "boolean"
      },
      {
        "description": "The counter-pill vocabulary is available PER ITEM, because variant/size are a group decision but the number is per-item data. Same vocabulary and same rendering as Toggle and Button: Intl.NumberFormat on the active locale, `{overflowCount}+` above the cap, and the count folded into that item's own accessible name. This is the faceted filter chip row (Open 42 / Closed 118) and the reaction row.",
        "name": "ToggleGroupItem count / overflowCount / showZero / countLabel",
        "type": "number | number | boolean | string"
      }
    ],
    "related": [
      "Segmented — the single-select sibling with a shared connected track. ToggleGroup is the generic multi/single toggle set; Segmented is the one-of-N control.",
      "Toggle",
      "RadioGroup"
    ],
    "rules": [
      3,
      13,
      51
    ],
    "storyPath": "data-entry/toggle-group.tsx",
    "subParts": [
      "ToggleGroupItem"
    ],
    "tagline": "Single or multiple toggle selection on react-aria-components. On type=single the ARIA role follows `disallowEmptySelection`: omitted it is a `group` of `aria-pressed` buttons that MAY be all-off, set it is a `radiogroup` of `radio`s (gh#744).",
    "usage": [
      "DO choose type='single' for mutually exclusive toolbar modes.",
      "DO add `disallowEmptySelection` to a type='single' group that is a SETTING — a view density, a sort order, a fiscal period. It is what makes the group a real radiogroup (role, aria-checked, one tab stop, arrow keys that move the selection) and it stops the second press from clearing the value, so you no longer need the `onValueChange={(v) => { if (v) setX(v) }}` guard that used to paper over it (gh#744).",
      "DON'T add `disallowEmptySelection` to a FILTER row. Clearing a chip by pressing it again is what the user expects there, and without the prop the group says so honestly: `role=\"group\"` + `aria-pressed`, a set of buttons that may all be off.",
      "DO choose type='multiple' for independent formatting toggles.",
      "DO set `variant`/`size`/`shape` ONCE on the ToggleGroup — they propagate to every ToggleGroupItem through context. Repeating them on each item is redundant (it still works, and an explicit item prop overrides the group).",
      "DO build a tag-filter panel as `<ToggleGroup type=\"multiple\" variant=\"soft\" shape=\"pill\" size=\"xs\">` with one counted `ToggleGroupItem` per tag — that is the whole antd `Tag.CheckableTag` row, one tab stop per chip, no `Tag` component needed (gh#734).",
      "DO set `size`/`variant` on an individual ToggleGroupItem only when that ONE item must differ from the group.",
      "DO add `wrap` to a TAG FILTER ROW — `<ToggleGroup type=\"multiple\" variant=\"soft\" shape=\"pill\" size=\"xs\" wrap>` is the whole folder-tag panel however many tags the folder has. This is what replaces the hand-built `<ul>` of individual `<Toggle>`s a row wider than its rail used to force (gh#741): that list loses exactly what the group owns — the shared `variant`/`size`/`shape` context, ONE `value`/`onValueChange`, and the group's arrow-key traversal, which keeps walking the chips in DOM order across the wrapped lines.",
      "DON'T put a filter-chip row in a horizontal scroller (`overflow-x-auto`, `ScrollArea orientation=\"horizontal\"`) with `shrink-0 whitespace-nowrap` chips — at every width, phone included, a chip row that filters a list takes `wrap` (cardinal rule #51). A sideways chip row hides chips past the edge with no affordance, and this kit ships no primitive that guarantees the first chip unclipped and a visible scroll cue, so the scroller is a hand-roll.",
      "DON'T set `wrap` on a 3–4 item segmented group. Measured at a 320px rail it is one 32px line either way, so the prop buys nothing and only adds a way for a toolbar to reflow.",
      "DON'T pass size='default' — it is not a member of the `xs | sm | md | lg` union. Omit `size` for the md default.",
      "DO give the group an accessible name (`aria-label`) — it renders a `group` of toggle buttons (single, the default), a `radiogroup` (single + `disallowEmptySelection`) or a `toolbar` of toggle buttons (multiple), and all three need a name."
    ],
    "useCases": [
      "Text alignment selector",
      "Formatting toolbar",
      "View density switcher",
      "Tag filter row (wrap)"
    ]
  },
  {
    "example": "import { AspectRatio } from \"@godxjp/ui/layout\";\n\n<AspectRatio ratio={16 / 9}>...</AspectRatio>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AspectRatio",
    "props": [
      {
        "defaultValue": "16 / 9",
        "description": "Width divided by height.",
        "name": "ratio",
        "type": "number"
      },
      {
        "description": "Content constrained to the ratio.",
        "name": "children",
        "type": "ReactNode"
      }
    ],
    "related": [
      "CardCover",
      "Skeleton"
    ],
    "rules": [
      2,
      3
    ],
    "storyPath": "layout/aspect-ratio.tsx",
    "tagline": "Fixed-ratio box (CSS aspect-ratio) for stable media and preview frames.",
    "usage": [
      "DO use AspectRatio for media, maps, charts, or previews that must not jump during load.",
      "DON'T use it for unconstrained text content."
    ],
    "useCases": [
      "Video embed frame",
      "Image preview slot",
      "Dashboard chart placeholder"
    ]
  },
  {
    "example": "import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"@godxjp/ui/data-display\";\n\n<Accordion type=\"single\" collapsible>\n  <AccordionItem value=\"ship\">\n    <AccordionTrigger>配送について</AccordionTrigger>\n    <AccordionContent>3〜5営業日でお届けします。</AccordionContent>\n  </AccordionItem>\n</Accordion>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Accordion",
    "props": [
      {
        "description": "single = one open at a time; multiple = independent.",
        "name": "type",
        "required": true,
        "type": "\"single\" | \"multiple\""
      },
      {
        "description": "When type=single, allow closing the open item.",
        "name": "collapsible",
        "type": "boolean"
      },
      {
        "description": "Controlled open item(s).",
        "name": "value",
        "type": "string | string[]"
      },
      {
        "description": "Uncontrolled initial open item(s).",
        "name": "defaultValue",
        "type": "string | string[]"
      },
      {
        "description": "Open-state callback.",
        "name": "onValueChange",
        "type": "(value: string | string[]) => void"
      }
    ],
    "related": [
      "Collapsible (single open/close region, no item set)",
      "Tabs (mutually-exclusive views, always one visible)"
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "data-display/accordion.tsx",
    "subParts": [
      "AccordionContent",
      "AccordionItem",
      "AccordionTrigger"
    ],
    "tagline": "React Aria disclosure group — vertically stacked, collapsible sections. Compose Accordion > AccordionItem > AccordionTrigger + AccordionContent.",
    "usage": [
      "DO compose the full set: <Accordion type=\"single\" collapsible><AccordionItem value=\"a\"><AccordionTrigger/><AccordionContent/></AccordionItem></Accordion>.",
      "DO give each AccordionItem a unique `value`.",
      "DON'T use it for primary navigation — that's Sidebar/Tabs. Accordion is for collapsible content/FAQ."
    ],
    "useCases": [
      "FAQ lists",
      "Grouped settings sections",
      "Collapsible detail panels on a record page",
      "Filter facet groups in a sidebar"
    ]
  },
  {
    "example": "import { PasswordInput } from \"@godxjp/ui/data-entry\";\n\n<PasswordInput name=\"password\" autoComplete=\"current-password\" placeholder=\"パスワード\" />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "PasswordInput",
    "props": [
      {
        "description": "Control password visibility; false hides the reveal action.",
        "name": "visibilityToggle",
        "type": "boolean | { visible?: boolean; onVisibleChange?: (visible: boolean) => void }"
      },
      {
        "description": "Customize the reveal glyph. The reveal action owns the single trailing slot.",
        "name": "iconRender",
        "type": "(visible: boolean) => ReactNode"
      },
      {
        "description": "Validation state the field paints — Ant Design `status`. `error` also reports `aria-invalid`, so the red boundary and what a screen reader hears are one fact; `warning` paints only, because a warning is not a validity failure. antd's `success`/`validating` are not implemented: antd only draws them together with its `hasFeedback` icon slot, which FormField owns here.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "defaultValue": "\"outlined\"",
        "description": "Chrome level — Ant Design `variant`. `outlined` is the historical field; `filled` swaps the boundary for a tinted surface (dense forms); `borderless` drops both, for a field inside a box that already draws one. antd's fourth member `underlined` is deliberately absent — a single bottom rule is a Material convention and SmartHR, the JP authority here, draws every field as a full box.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\""
      },
      {
        "description": "Character counter — inherited from Input.",
        "name": "count",
        "type": "ControlCountProp"
      },
      {
        "description": "Controlled value (or use defaultValue/uncontrolled).",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Form field name for native submission.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Placeholder text.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "description": "Disables the field + toggle.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Inherited from Input — a leading glyph (e.g. a Lock icon) pinned inside the start of the field. The built-in show/hide eye stays on the trailing edge, so leading + trailing coexist here.",
        "name": "leadingIcon",
        "type": "React.ReactNode"
      }
    ],
    "related": [
      "Input (the base text field this wraps)",
      "FormField (label + error wrapper around it)"
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "data-entry/password-input.tsx",
    "tagline": "Input for passwords with a built-in show/hide eye toggle. Accepts all Input props except `type`.",
    "usage": [
      "Coarse pointers: affix actions use --control-affix-touch-target-size (44px by default); text space is reserved separately. Fine-pointer geometry is unchanged.",
      "DO use for any password / secret field instead of `<Input type=\"password\">` so users get the show/hide affordance.",
      "DO pass `name` + `autoComplete=\"current-password\"|\"new-password\"` for correct form/password-manager behavior.",
      "DON'T add your own eye button — it's built in (and excluded from the tab order)."
    ],
    "useCases": [
      "Login password field",
      "Sign-up / change-password forms (new-password)",
      "API key / secret entry in settings"
    ]
  },
  {
    "example": "import { PasswordInput, PasswordStrength } from \"@godxjp/ui/data-entry\";\n\nexport default function PasswordBlock() {\n  const [value, setValue] = useState(\"\");\n  return (\n    <div className=\"ui-stack\">\n      <PasswordInput value={value} onChange={(event) => setValue(event.target.value)} />\n      <PasswordStrength value={value} rules={[\"length\", \"upper\", \"lower\", \"number\", \"symbol\"]} />\n    </div>\n  );\n}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "PasswordStrength",
    "props": [
      {
        "description": "The current password value to evaluate. While it is empty (or whitespace only) the meter is NEUTRAL (gh#1247): no segment lit, no tone, the label reads `labels.empty` (\"Enter a password\" / 「パスワードを入力」), every checklist item is `data-state=\"pending\"`, and the live region stays silent — no \"Weak\" verdict before anything is typed.",
        "name": "value",
        "required": true,
        "type": "string"
      },
      {
        "description": "`length` | `upper` | `lower` | `number` | `symbol`. Omit to use defaults.",
        "name": "rules",
        "type": "PasswordRule[]"
      },
      {
        "defaultValue": "true",
        "description": "Render an optional checklist of rule checks below the bar.",
        "name": "showChecklist",
        "type": "boolean"
      },
      {
        "defaultValue": "8",
        "description": "The length rule's threshold (gh#1193): drives the check, the score and the localized rule text (\"{n} 文字以上\" / \"{n}+ characters\"). Match the server's policy, e.g. 12 for Laravel Password::min(12), so the checklist never shows a password the server rejects as met.",
        "name": "minLength",
        "type": "number"
      },
      {
        "description": "Every visible and screen-reader string, each optional (gh#1191). Unset entries come from the kit's own ja / en / vi catalogue for the active locale, so most apps pass nothing: the score words, the neutral empty-field line (`empty`, gh#1247), the checklist line per rule (length / upper / lower / number / symbol), the sr-only passed / failed prefixes, and the polite live-region sentence (`announcement(strength)`). Pass only the ones your product words differently.",
        "name": "labels",
        "type": "{ weak?: string; fair?: string; strong?: string; empty?: string; rules?: Partial<Record<PasswordRule, string>>; passed?: string; failed?: string; announcement?: (strength: string) => string }"
      }
    ],
    "related": [
      "PasswordInput",
      "FormField",
      "Input"
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "data-entry/password-strength.tsx",
    "tagline": "Evaluates password quality with a 0-4 score and optional rule checklist. Use with PasswordInput in secure forms.",
    "usage": [
      "DO show PasswordStrength immediately below PasswordInput so users understand password quality before submitting.",
      "DO keep `rules` stable (default list is recommended for broad UI compatibility).",
      "DON'T treat the score as cryptographic strength; use it as a UI hint only."
    ],
    "useCases": [
      "Account signup password field",
      "Password reset workflow",
      "Admin user invite form"
    ]
  },
  {
    "example": "import { InputOTP, InputOTPGroup, InputOTPSlot } from \"@godxjp/ui/data-entry\";\n\n<InputOTP maxLength={6}>\n  <InputOTPGroup>\n    <InputOTPSlot index={0} /><InputOTPSlot index={1} /><InputOTPSlot index={2} />\n    <InputOTPSlot index={3} /><InputOTPSlot index={4} /><InputOTPSlot index={5} />\n  </InputOTPGroup>\n</InputOTP>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "InputOTP",
    "props": [
      {
        "description": "Initial uncontrolled code.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Canonical value callback, compatible with FormFieldControl.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Mask filled visual slots without changing the submitted code.",
        "name": "mask",
        "type": "boolean | string"
      },
      {
        "description": "Normalize typed and pasted codes.",
        "name": "formatter",
        "type": "(value: string) => string"
      },
      {
        "description": "Shared control height tier.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Validation appearance.",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "Shared field chrome.",
        "name": "variant",
        "type": "\"outlined\" | \"filled\" | \"borderless\" | \"underlined\""
      },
      {
        "description": "Refuse edits including paste.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Number of slots (e.g. 6).",
        "name": "maxLength",
        "required": true,
        "type": "number"
      },
      {
        "description": "Controlled value.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Value callback (this is a true text input — onChange is the DOM-style value handler here).",
        "name": "onChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Allowed-char regex (e.g. digits only).",
        "name": "pattern",
        "type": "string"
      },
      {
        "defaultValue": "\"start\"",
        "description": "Main-axis alignment of the whole code row (groups + separators) inside the field. `center` is the canonical auth challenge. Before this existed, every consumer wrapped the OTP in their own flex-centring div — do not. A service that wants all code fields centred sets `--otp-container-align` once instead.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "description": "Fires ONCE the last slot fills, whether the user typed it or pasted the whole code. This is the auto-submit hook: a 2FA challenge with a visible submit button is a step nobody wants, and the alternative — watching `value.length === maxLength` in an effect — re-fires on every re-render. Keep the submit button anyway for the paste-then-correct case.",
        "name": "onComplete",
        "type": "(value: string) => void"
      },
      {
        "description": "Rewrites CLIPBOARD text before it reaches the field. Distinct from `formatter`, which normalises every value: this one only sees a paste, which is where the junk arrives — `\"123 456\"`, `\"code: 123456\"`, a copied SMS line. Note the order the field applies them: `pattern` is matched against the RAW keystroke first, so a pattern must accept what a user actually types, not only what these two produce.",
        "name": "pasteTransformer",
        "type": "(pasted: string) => string"
      },
      {
        "description": "Class on the ROW container `input-otp` renders (the slots' flex parent), which `className` cannot reach — `className` lands on the hidden input, because that is the real field. Prefer `align` and the `--otp-*` tokens; this is the vendored escape hatch underneath them.",
        "name": "containerClassName",
        "type": "string"
      },
      {
        "description": "`input-otp`'s answer to the 1Password / LastPass badge that browsers float over a code field and that covers the last slot. `increase-width` (its default) reserves room so the badge sits beside the row; `none` turns the accommodation off, which is what a row already centred by `align` usually wants. Pure layout — it changes no value and no keyboard behaviour.",
        "name": "pushPasswordManagerStrategy",
        "type": "\"increase-width\" | \"none\""
      },
      {
        "description": "The `<noscript>` stylesheet `input-otp` injects so the slots are still visible with JS disabled. Pass `null` to suppress it — the one real reason being a CSP that forbids inline styles and that `nonce` cannot satisfy. Leave it alone otherwise.",
        "name": "noScriptCSSFallback",
        "type": "string | null"
      },
      {
        "description": "CSP nonce stamped on the stylesheet `input-otp` injects. Required only under a `style-src 'nonce-…'` policy, where the field otherwise renders unstyled and the console reports a blocked inline style. Pass the same nonce the document was served with.",
        "name": "nonce",
        "type": "string"
      },
      {
        "description": "`input-otp`'s headless mode: you draw the entire row from the slot state instead of composing InputOTPGroup / InputOTPSlot. It is mutually exclusive with `children` — the vendor types the two as a union and this component keeps that union. Reaching for it means giving up the slot styling, the group outline and the separator this package owns, so it is the last resort, not a customisation point.",
        "name": "render",
        "type": "(props: InputOTPRenderProps) => React.ReactNode"
      }
    ],
    "related": [
      "Input (a normal single text field)",
      "PasswordInput (masked secret field)"
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "data-entry/input-otp.tsx",
    "subParts": [
      "InputOTPGroup",
      "InputOTPSeparator",
      "InputOTPSlot"
    ],
    "tagline": "One-time-code / 2FA input (input-otp) — N single-character slots that behave as one field. Compose InputOTP > InputOTPGroup > InputOTPSlot.",
    "usage": [
      "DO set `maxLength` to the code length and render that many InputOTPSlot with sequential `index`.",
      "DO centre a challenge with `align=\"center\"` on InputOTP — never with a wrapper `<div className=\"flex justify-center\">`. The container is owned by input-otp, so a wrapper is the only thing a consumer CAN reach, which is exactly why the prop exists.",
      "DO wrap slots in InputOTPGroup; use InputOTPSeparator between groups (e.g. 3 + 3).",
      "For device codes, set `appearance='grouped'` on each InputOTPGroup to render one outline per group while preserving the single hidden input, paste, caret, keyboard and screen-reader behavior.",
      "DON'T build N separate Inputs — this is ONE field with paste, arrow-key, and caret handling built in.",
      "DO widen the slots with `--otp-slot-size` when a challenge row must fill a wide auth panel — it defaults to the live `--control-height` tier, so re-scoping `--control-height` on the card instead would also resize the submit button and every other input in it. Set a NAMED tier (`var(--control-height-lg)`), never an ad-hoc calc offset.",
      "DO use `--otp-slot-inline-size` / `--otp-slot-block-size` when the code field is NOT square — a device-grant slot is taller than it is wide. They win over the `--otp-slot-size` shorthand and fall back to it, so setting neither keeps the square control tier. You rarely set them by hand inside `the auth CenteredShell preset=\"device-authorization\"`: that preset already owns its code-field measure.",
      "DO drive the sign-in MFA challenge from FormField: `error` wires aria-invalid + aria-errormessage + a role=alert message onto the single field, and the slot borders turn destructive. State is never colour-only."
    ],
    "useCases": [
      "2FA / OTP verification code",
      "Email / SMS confirmation code",
      "PIN entry",
      "Invite / redemption code"
    ]
  },
  {
    "example": "import { Rating } from \"@godxjp/ui/data-entry\";\n\n<Rating name=\"score\" defaultValue={4} onValueChange={(v) => console.log(v)} />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Rating",
    "props": [
      {
        "defaultValue": "5",
        "description": "antd `count` — number of symbols. This library's older `max` still works; `count` wins when both are given.",
        "name": "count",
        "type": "number"
      },
      {
        "defaultValue": "false",
        "description": "antd `allowHalf` — a symbol can be half-filled, so the scale steps by 0.5 (keyboard included). It adds a hit area per symbol WITHOUT doubling the radios: a half is a position inside a step, and ten radios announced for a five-star scale would misstate the scale.",
        "name": "allowHalf",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd `allowClear` — choosing the chosen value clears it to 0. antd defaults this ON; it is OFF here, because a rating in a business form is usually required and a silent reset on a second click reads as a lost answer.",
        "name": "allowClear",
        "type": "boolean"
      },
      {
        "description": "antd `character` — what a symbol IS. A node for every symbol, or a function of the 1-based index for a scale whose symbols differ (A/B/C, 松竹梅).",
        "name": "character",
        "type": "React.ReactNode | ((index: number) => React.ReactNode)"
      },
      {
        "description": "antd `tooltips` — a label per step, in order. Folded into each symbol's ACCESSIBLE NAME rather than shown only on hover: a `title` is invisible to a keyboard and to touch, and saying what '3 of 5' means is the whole point of the prop.",
        "name": "tooltips",
        "type": "readonly string[]"
      },
      {
        "description": "Controlled rating (1..max).",
        "name": "value",
        "type": "number"
      },
      {
        "defaultValue": "0",
        "description": "Uncontrolled initial rating.",
        "name": "defaultValue",
        "type": "number"
      },
      {
        "description": "Rating callback.",
        "name": "onValueChange",
        "type": "(value: number) => void"
      },
      {
        "defaultValue": "5",
        "description": "Number of stars.",
        "name": "max",
        "type": "number"
      },
      {
        "description": "Display-only (e.g. an average score).",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Hidden input name for native form submission.",
        "name": "name",
        "type": "string"
      }
    ],
    "related": [
      "RadioGroup (non-star single choice)",
      "Slider (continuous 0-100 value)"
    ],
    "rules": [
      3,
      6,
      23
    ],
    "storyPath": "data-entry/rating.tsx",
    "tagline": "Star-rating input (radiogroup) — controlled via value/onValueChange, form-submittable via name, supports readOnly display.",
    "usage": [
      "DO theme the stars with `--rating-star-filled-color` (default `var(--warning)`) and `--rating-star-empty-color` (default `var(--muted-foreground)`, at `--rating-star-empty-alpha` 0.45) — HSL components, set on :root or a scoped [data-tenant] (gh#694). NEVER override `.ui-rating-star-filled`: it is an internal class. Unset, the stars paint exactly as before.",
      "DO use readOnly to DISPLAY a score (e.g. product average); interactive (default) for collecting a rating.",
      "DO pass `name` to submit the value in a plain form.",
      "DON'T render raw star icons for input — this handles keyboard (radiogroup), hover preview, and a11y.",
      "NOTE a long scale WRAPS rather than overflowing. At `max={10}` the row needs ~276px of hit area and a 320px viewport offers ~212 inside a card, so the stars fall onto a second line instead of being painted where no one can reach them. Don't add `flex-nowrap` or a fixed width to force one line; use a smaller `max` if a single row matters."
    ],
    "useCases": [
      "Product / vendor review input",
      "Display an average score (readOnly)",
      "Feedback / CSAT survey",
      "Priority or quality scoring in admin"
    ]
  },
  {
    "example": "import { Segmented } from \"@godxjp/ui/data-entry\";\n\n<Segmented\n  aria-label=\"Theme\"\n  value={theme}\n  onValueChange={(next) => setTheme(next as AppTheme)}\n  options={[\n    { value: \"light\", label: \"Light\" },\n    { value: \"dark\", label: \"Dark\" },\n    { value: \"system\", label: \"System\" },\n  ]}\n/>\n\n<Segmented\n  aria-label=\"Status filter\"\n  value={status}\n  onValueChange={setStatus}\n  options={[\n    { value: \"all\", label: \"All\", count: 128 },\n    { value: \"active\", label: \"Active\", count: 96 },\n  ]}\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Segmented",
    "props": [
      {
        "defaultValue": "false",
        "description": "antd `block` — stretch the bar to its container and share the width EQUALLY between the choices, so the selected pill does not resize as the label under it changes length.",
        "name": "block",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "antd `vertical` — stack the choices in a column. It changes the ARROW KEYS as well as the layout: the primitive reads `orientation` to decide which arrows move the roving focus.",
        "name": "vertical",
        "type": "boolean"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Control height tier — reads the shared `--control-height` ladder: xs 24px · sm 28px · md 32px · lg 36px. `touch` (gh#1304) is the glove step and the one exception to the track rule below: every SEGMENT is at least 56×56 CSS px (`--control-height-touch`), segments stand `--control-touch-gap` (8px) apart, and the track grows around them — pair with `block` for full-width pass/fail bars. The TRACK measures exactly one control height, so the bar sits level with an Input, a Button or a ToggleGroup of the same step on the same row. Pick the step the ROW already has: xs (gh#719) is the one that fits a 24px-dense toolbar or audit-log row — before it, that row could only get a hand-rolled set of Buttons, which loses the radiogroup semantics and the arrow keys. xs is the DENSE step, not a smaller default: the track spends its 2px inset at every step, so the individual SEGMENT measures 20px and clears WCAG 2.2 SC 2.5.8 through the Spacing exception (adjacent segments 26.47px apart for a one-glyph label) rather than the 24px minimum. Keep labels at a glyph or more, and stay on sm or md wherever the row height is yours to choose.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"touch\""
      },
      {
        "description": "The closed set of choices, in reading order. `label` is the visible content AND the item's accessible name. Use `count` for a filter total — do not nest `Badge` in `label` (gh#602).",
        "name": "options",
        "type": "{ value: string; label: ReactNode; icon?: ReactNode; disabled?: boolean; count?: number | string; overflowCount?: number; showZero?: boolean; countLabel?: string }[]"
      },
      {
        "description": "Controlled selection.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial selection.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Selection callback.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Disable the whole group.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Form field name — submits the selected value with a native form.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "Root id, for a label that points at it.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "ToggleGroup (independently pressed buttons, or a multi-select toolbar)",
      "RadioGroup (the same semantics with a vertical, described list)",
      "Select (the same choice when the set is long or hidden by default)",
      "Tabs (switches PANELS, not a value)"
    ],
    "rules": [
      3,
      6,
      23
    ],
    "storyPath": "data-entry/segmented.tsx",
    "tagline": "One-of-N from a small, closed, always-visible set — the enterprise Segmented / filter bar drawn on react-aria-components' RadioGroup. A track with the chosen item as a lifted slab. Reach for it INSTEAD OF a Select when there are 2-4 options and all of them fit on screen, and instead of ToggleGroup when exactly one must always be chosen.",
    "usage": [
      "DO use it for a closed set of 2-4 peer choices that are cheap to show — theme, view mode, a date range preset.",
      "DO use it for a list status view. A list status view (All / Active / Closed, each with its count) that shows the SAME table — same columns, same row actions, a different query — is a filter, so it is `Segmented` with `count`, placed in the list's `Toolbar`; past 4 statuses it is a `Select`. `Tabs` only when each status is a genuinely different panel (its own columns, actions or empty state), declared as `TabsContent` or pointed at with `controls` (WAI-ARIA APG: a tab selects a panel, a radiogroup selects a value) (gh#1324).",
      "DO give it an aria-label (or aria-labelledby) — the group needs a name, and each item takes its own from its label.",
      "DON'T use ToggleGroup for a one-of-N choice unless you also pass `disallowEmptySelection`: without it, type=single is a row of aria-pressed toggle buttons that can end up with nothing selected, which a setting can never be (gh#744). Segmented is the one-of-N control and needs no such switch.",
      "DON'T use it past ~4 options — that is a Select. Up to four, a horizontal track WRAPS to a second row when its options cannot share one (a phone-width status filter with counts), so no label or count is truncated while its item fits on a row. `block` is the exception: it promises EQUAL widths, so it still truncates — don't use `block` for four labelled options at phone width.",
      "DO show a short mark and speak a long name by putting BOTH in `label`: an aria-hidden span for the glyph and a VisuallyHidden for the words. `label` is a ReactNode, the item takes its accessible name from its content, and the glyph drops out of that name once it is aria-hidden — so a bar of circle/triangle/cross marks still announces the state in words. There is no separate accessible-name prop and there does not need to be.",
      "DO pass per-option totals via `count` — the DS paints an opaque pill that reads on both the recessed track and the selected slab. Do not put `Badge` in `label` for counts: `secondary` is `--muted`, which is the track fill (1.00:1, gh#602).",
      "DO stack with `vertical` when the labels are too long to sit side by side: a stacked row is a WHOLE `--control-height` tall, where a horizontal bar spends part of that height on the track padding so the bar as a whole lines up with an Input beside it. Inside a MobileShell, which scopes the control tier to the touch step, that is what makes each row a 44px target.",
      "DO remember that `size` and any scoped `--control-height` both reach the track: the item height is composed on the Segmented root, not frozen at :root.",
      "DO set `size=\"xs\"` for a 24px-dense row — an audit-log toolbar, a table header strip, a row that already carries `<Button size=\"xs\">` or an `xs` ToggleGroup. All three measure 24px off the same `--control-height-xs` step, so the row stays level, and the label type and the item inline padding step down with the band (gh#719). DON'T hand-roll that row out of Buttons to get the height: a segmented control is a radiogroup, and a row of buttons loses the arrow keys and the \"1 of 3, selected\" announcement."
    ],
    "useCases": [
      "Theme switch (light / dark / system)",
      "List vs board vs calendar view mode",
      "Chart range: day / week / month",
      "Status filter above a list, each option carrying its count (All 128 / Active 96 / Pending 12 / Archived 0)"
    ]
  },
  {
    "example": "import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"@godxjp/ui/layout\";\n\n<ResizablePanelGroup orientation=\"horizontal\">\n  <ResizablePanel id=\"list\" defaultSize=\"35%\" minSize=\"20%\">Panel A</ResizablePanel>\n  <ResizableHandle />\n  <ResizablePanel id=\"detail\" defaultSize=\"65%\" minSize=\"40%\">Panel B</ResizablePanel>\n</ResizablePanelGroup>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "ResizablePanel",
    "props": [
      {
        "defaultValue": "\"horizontal\"",
        "description": "ResizablePanelGroup prop — axis the panels are laid out / resized along (horizontal = side-by-side).",
        "name": "orientation",
        "type": "\"horizontal\" | \"vertical\""
      },
      {
        "description": "ResizablePanel identifier — required for collapse/expand control and for layout persistence.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "For a percentage of the group pass a string like \"35%\" — `defaultSize={35}` means 35px (a sliver), not 35%.",
        "name": "defaultSize",
        "type": "string | number"
      },
      {
        "description": "ResizablePanel minimum size — drag can't shrink below this. Same unit rule: \"20%\" for percent, bare number = px.",
        "name": "minSize",
        "type": "string | number"
      },
      {
        "description": "ResizablePanel maximum size. \"60%\" for percent, bare number = px.",
        "name": "maxSize",
        "type": "string | number"
      },
      {
        "defaultValue": "false",
        "description": "ResizablePanel — allow the panel to collapse to collapsedSize when dragged below minSize. Pair with onResize to react to collapse.",
        "name": "collapsible",
        "type": "boolean"
      },
      {
        "description": "ResizablePanel — fires while/after the panel is resized (e.g. to persist layout).",
        "name": "onResize",
        "type": "(size: PanelSize, id, prevSize) => void"
      }
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "layout/resizable-panel.tsx",
    "subParts": [
      "ResizableHandle",
      "ResizablePanelGroup"
    ],
    "tagline": "Resizable panel group/child/handle primitives from react-resizable-panels. --resizable-handle-size sets separator thickness; ARIA orientation controls its axis. Handles use the shared inset focus ring, including empty handles.",
    "usage": [
      "DO put the layout on `<ResizablePanelGroup orientation=\"horizontal|vertical\">`, the resizable regions in `<ResizablePanel>`, and a `<ResizableHandle>` BETWEEN every adjacent pair — a group of N panels needs N-1 handles or there is nothing to drag.",
      "DO express `defaultSize`/`minSize`/`maxSize` as PERCENTAGE STRINGS like `defaultSize=\"35%\"` (react-resizable-panels v4: a bare number is PIXELS, so `defaultSize={35}` renders a 35px sliver, NOT 35% — pass the string). Don't fight sizing with a fixed `w-[280px]` className on the panel.",
      "DON'T reach for ResizablePanel when the split is fixed and never user-adjustable — use a plain `Flex`/`ResponsiveGrid`, or `SplitPane` for a simple two-pane layout. Resizable is for *user-draggable* boundaries only.",
      "DO give each panel a stable `id` and use `collapsible` + `collapsedSize` for a side panel the user can fully tuck away (e.g. a filters rail), reacting via `onResize`.",
      "DON'T hand-roll a draggable divider with mouse-move listeners — the primitive handles pointer + keyboard resizing, ARIA separator semantics, and min/max clamping. Always render `<ResizableHandle>`, never a bare styled `<div>`.",
      "DON'T add your own `overflow: auto` wrapper inside a panel: the panel IS the scroll container. Let it own the scrolling and it also manages keyboard reachability — when a panel scrolls but holds nothing focusable, ResizablePanel gives it `tabindex=0` so keyboard users can still reach the clipped content (WCAG 2.1.1); a panel whose content is focusable is left alone, so no redundant tab stop appears. Your own nested scroller gets neither."
    ],
    "useCases": [
      "Master–detail admin layout: a draggable list pane on the left and a detail/preview pane on the right (e.g. 仕訳一覧 | 仕訳詳細).",
      "Collapsible filters or navigation rail beside a data table that operators can widen for long labels or tuck away to maximize the table.",
      "Stacked vertical split (orientation='vertical') such as a results table over a live JSON/log preview in a data-import tool.",
      "Three-pane workbench (nav | content | inspector) where each boundary is independently draggable and layout is persisted via id + onResize."
    ]
  },
  {
    "example": "import { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, CarouselDots } from \"@godxjp/ui/lab\";\n\n// CarouselDots reads the Embla api from context — no setApi wiring needed.\n<Carousel opts={{ loop: true }}>\n  <CarouselContent>\n    <CarouselItem>1</CarouselItem>\n    <CarouselItem>2</CarouselItem>\n  </CarouselContent>\n  <CarouselPrevious />\n  <CarouselNext />\n  <CarouselDots />\n</Carousel>",
    "group": "data-display",
    "importPath": "@godxjp/ui/lab",
    "name": "Carousel",
    "props": [
      {
        "description": "Embla options.",
        "name": "opts",
        "type": "Parameters<typeof useEmblaCarousel>[0]"
      },
      {
        "description": "Embla plugins.",
        "name": "plugins",
        "type": "Parameters<typeof useEmblaCarousel>[1]"
      },
      {
        "description": "Receive the Embla api for custom logic (autoplay, external prev/next). NOT needed for dots — CarouselDots reads the api from context itself.",
        "name": "setApi",
        "type": "(api: CarouselApi) => void"
      }
    ],
    "rules": [
      3,
      6
    ],
    "storyPath": "data-display/carousel.tsx",
    "subParts": [
      "CarouselContent",
      "CarouselDots",
      "CarouselItem",
      "CarouselNext",
      "CarouselPrevious"
    ],
    "tagline": "Embla-backed carousel primitives: previous/next controls, CarouselDots indicators, and a context API.",
    "tier": "lab",
    "usage": [
      "DO compose the full set: `<Carousel>` › `<CarouselContent>` › many `<CarouselItem>`, with `<CarouselPrevious>`/`<CarouselNext>` for arrows and `<CarouselDots>` for the indicator row. Don't render items outside `<CarouselContent>` — the track is the scroll container.",
      "DO use `<CarouselDots>` for the active-slide indicator instead of wiring `setApi` by hand — it reads `selectedIndex`/`scrollSnaps` from the Carousel context, renders one `aria-current` dot per snap, and auto-hides when there is ≤1 slide.",
      "DON'T use a Carousel where ALL items must be seen/compared at once or be keyboard-reachable in reading order (e.g. a list of selectable options, a data table, primary navigation) — hiding content behind a swipe is an anti-pattern there; use a Grid/`ResponsiveGrid`, `ScrollArea`, or `Tabs`.",
      "DON'T autoplay without a pause-on-hover/focus control and reduced-motion respect — pass the Embla autoplay plugin via `plugins` only for non-essential decorative content, never for content the user must read.",
      "DO set `opts={{ loop: true }}` for galleries that wrap, and rely on the built-in disabling: `CarouselPrevious`/`CarouselNext` auto-disable at the ends (via `canScrollPrev`/`canScrollNext`) — don't hide them, let them grey out.",
      "DO give each `<CarouselItem>` real, meaningful content; the component already injects an 'N of M' slide label for screen readers, so don't add a redundant one (a consumer `aria-label` on the item overrides the default).",
      "DO size a MULTI-ITEM rail with `basis-*` on the item: `<CarouselItem className=\"basis-full sm:basis-1/2 lg:basis-1/3\">` is 1/2/3 per view, and `basis-4/5` leaves the \"peek\" that tells a phone user the rail scrolls. Use the FRACTIONAL ladder, never an arbitrary `basis-[82%]` (the audit rejects it). Fixed in gh#930 — before that the item carried `min-width: 100%`, which silently beat every `basis-*`: the documented 3-across composition computed `flex-basis: 33.3333%` and still painted one full-width slide 32px WIDER than its own rail. If a rail renders 1-up on an older version, that is the bug, not your className.",
      "DON'T wrap each `<CarouselItem>` in `<Reveal on=\"view\">`. The rail clips its slides, so the observer on a slide outside the window has an empty intersection rect and the slide never becomes visible — measured at 3 of 4 slides stuck at `opacity: 0`. Put ONE `<Reveal on=\"view\">` around the whole `<Carousel>`.",
      "DO expect the prev/next arrows to OVERLAY the rail edges — they are `position: absolute` inside the Carousel, so placing them in a caption row is not something a prop offers. Two knobs retune them: `--carousel-arrow-inset-block-start` for the vertical placement, and `--carousel-arrow-size` for the BUTTON box (default 2rem/32px, which clears WCAG 2.2 SC 2.5.8's 24px floor). A brand whose own standard is a 44px target sets `--carousel-arrow-size: 2.75rem` in its theme — the box and the glyph are separate decisions, so the mark stays on the icon scale via `--carousel-arrow-icon-size`. The box token is gh#931; before it, those were two bare literals and the target was the one dimension a theme could not reach."
    ],
    "useCases": [
      "Feature / onboarding highlight cards on a dashboard or landing surface, with CarouselDots showing position.",
      "Image or document thumbnail gallery (e.g. uploaded receipts / 物件写真) with looping and prev/next arrows.",
      "Horizontal stepping list of compact KPI or announcement cards that overflow the viewport width.",
      "Product/plan comparison cards on a marketing page where swiping between a few options is acceptable (not the primary action)."
    ]
  },
  {
    "absorbed": [
      "LocalePicker",
      "LanguagePicker",
      "TimezonePicker",
      "DateFormatPicker",
      "TimeFormatPicker",
      "ThemePicker",
      "DensityPicker",
      "AppSettingToggle"
    ],
    "example": "{`// Uncontrolled — AppProvider manages and persists every setting\nimport { AppProvider } from \"@godxjp/ui/app\";\nimport { AppSettingPicker } from \"@godxjp/ui/navigation\";\n\nexport function SettingsPanel() {\n  return (\n    <AppProvider defaultLocale=\"ja\" defaultTimezone=\"Asia/Tokyo\">\n      <AppSettingPicker kind=\"locale\" />\n      <AppSettingPicker kind=\"timezone\" />\n      <AppSettingPicker kind=\"dateFormat\" />\n      <AppSettingPicker kind=\"timeFormat\" />\n    </AppProvider>\n  );\n}\n\n// Controlled — no AppProvider required\nimport { useState } from \"react\";\nimport { AppSettingPicker } from \"@godxjp/ui/navigation\";\n\nexport function LocaleField() {\n  const [locale, setLocale] = useState(\"en\");\n  return <AppSettingPicker kind=\"locale\" value={locale} onValueChange={setLocale} />;\n}\n\n// Topbar locale switcher (globe) — a cell OF the bar, no CSS overrides\nimport { Topbar } from \"@godxjp/ui/layout\";\nimport { AppSettingPicker } from \"@godxjp/ui/navigation\";\n\nexport function TopbarLocale() {\n  // \"bar\" inside a Topbar slot; \"icon\" everywhere else.\n  return <Topbar end={<AppSettingPicker kind=\"locale\" appearance=\"bar\" />} />;\n}`}",
    "group": "navigation",
    "importPath": "@godxjp/ui/navigation",
    "name": "AppSettingPicker",
    "props": [
      {
        "defaultValue": "true",
        "description": "true (default): open-then-choose from a menu. false: ONE button that steps the setting to its NEXT value on each tap and shows that value as its glyph (theme Sun/Moon/Monitor, density rows, fontSize A-marks, timeFormat text) — only for the closed, short kinds \"theme\" | \"density\" | \"fontSize\" | \"timeFormat\" (a 400-entry IANA list is a menu, not a cycle). Same binding contract and the same option order as the menu, read from the same constants. Its `appearance` is then \"bar\" (default — a full-height TopbarItem cell) | \"icon\" (a square ghost Button off-bar). The accessible name carries the setting AND the current value (\"Theme: Dark\"). v32 folded `AppSettingToggle` in here.",
        "name": "menu",
        "type": "boolean"
      },
      {
        "description": "Which AppProvider setting this picker reads and writes. Determines the option list, icon, trigger width, and the context value/setter used. The theme-axis kinds (theme/brand/density/fontSize) write <html data-*>; brand's first option opts out (null → app token).",
        "name": "kind",
        "type": "\"locale\" | \"timezone\" | \"dateFormat\" | \"timeFormat\" | \"theme\" | \"brand\" | \"density\" | \"fontSize\""
      },
      {
        "description": "Controlled value for the chosen kind. When omitted, reads the current value from AppProvider context for that kind.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Controlled change handler. When omitted, calls the matching AppProvider setter (setLocale/setTimezone/setDateFormat/setTimeFormat). Required together with value when no AppProvider is present.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Trigger presentation. \"icon\" is the supported icon-only topbar trigger (e.g. a globe locale switcher): it structurally drops the value text and the picker's owned width and hides the chevron, squares the box to the density-aware --control-height tap target (≥44px on touch), and always keeps the localized aria-label so it can never ship nameless. \"bar\" is the SAME structural drops re-shaped as a cell OF the bar rather than a control in it — it fills the bar height and squares its corners (--topbar-item-radius, the knob TopbarItem uses), so the hover surface paints the whole strip. Reach for \"bar\" inside a Topbar slot or AppShell's bar and \"icon\" everywhere else: \"icon\" in a taller bar leaves a --control-height pill floating mid-strip, which reads as a different control family from the bar's own chrome. \"inline\" renders the selected value as a chrome-less text trigger for a legal/auth footer (no border, no box). DEFAULT IS KIND-DEPENDENT: kind=\"locale\" defaults to \"icon\" (its product contract is the compact language switcher); every other kind defaults to \"labeled\".",
        "name": "appearance",
        "type": "\"labeled\" | \"icon\" | \"bar\" | \"inline\""
      },
      {
        "defaultValue": "false",
        "description": "Compact trigger density: re-tiers the box to the official --control-height-sm tier and DROPS the picker's owned per-kind width, so a labelled trigger hugs its value. This is the supported auth/legal-footer locale switch — `<AppSettingPicker kind=\"locale\" appearance=\"labeled\" compact />` — when the square icon-only default reads as a stray button but the full labelled trigger is too tall. All geometry is tokenized (--app-setting-picker-compact-{control-height,padding-x,gap,font-size}); the accessible name and the visible value are both preserved. No effect on appearance=\"inline\", which is already chrome-less.",
        "name": "compact",
        "type": "boolean"
      },
      {
        "description": "Extra CSS classes merged onto the SelectTrigger.",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Disables the Select control.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "HTML id forwarded to the SelectTrigger for label association.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "AppProvider — required peer unless fully controlled. Supplies locale/timezone/dateFormat/timeFormat plus their setters and the i18n context.",
      "Select — the data-entry primitive AppSettingPicker is built on; reach for Select directly for any non-AppProvider dropdown.",
      "formatDate — reads the same AppProvider date/time context that kind='dateFormat'/'timeFormat' write to."
    ],
    "rules": [
      3,
      5,
      6,
      23
    ],
    "storyPath": "navigation/app-setting-picker.tsx",
    "tagline": "One provider-bound Select for a single AppProvider setting, chosen by `kind` (locale | timezone | dateFormat | timeFormat | theme | brand | density | fontSize) — covers locale/format AND the four theme axes. Throws if used without AppProvider AND without controlled value+onValueChange.",
    "usage": [
      "CYCLE (menu={false}): DO: Reach for it in a top bar when the value set is closed and short — <AppSettingPicker menu={false} kind=\"theme\" /> is the light/dark/system switcher, and a dropdown for three values is a menu nobody wanted to open.",
      "CYCLE (menu={false}): DO: Trust the accessible name — it always names BOTH the setting and the current value (\"Theme: Dark\"), because the glyph is the only visible state. Never override it with a kind-only aria-label.",
      "CYCLE (menu={false}): DON'T: Set a height, a radius or a background on it in a bar. The bar cell shape is TopbarItem's, and any utility you add outranks @layer components and re-creates the floating-pill defect.",
      "CYCLE (menu={false}): DON'T: Reach for it for locale, timezone, dateFormat or brand — those are not in `kind` on purpose. Use AppSettingPicker.",
      "CYCLE (menu={false}): DON'T: Hand-roll a theme button with useAppContext + a Sun/Moon ternary — that loses the localized value-bearing name, the shared option order, and the bar-cell shape.",
      "DO: Mount inside <AppProvider> for zero-config use — the picker reads and writes the context value named by kind, no value/onValueChange needed.",
      "DO: Use controlled mode (value + onValueChange) when managing state outside AppProvider, e.g. a standalone settings form or a Storybook story. Both are required together in this mode.",
      "DO NOT: Render without AppProvider and without both controlled props — it throws 'AppSettingPicker requires <AppProvider> or controlled value + onValueChange'.",
      "DO: Render four instances with different kind values to build a full preferences panel; they all share the same AppProvider context and stay in sync.",
      "DO: For an icon-only topbar utility (a globe language switcher), pass appearance=\"icon\" — the supported compact trigger. NEVER hand-roll it by hiding the value/width with descendant-selector CSS.",
      "DO: For an AUTH/LEGAL FOOTER locale switch, pass appearance=\"labeled\" compact — a small, content-hugging labelled trigger (readable language name, --control-height-sm box). Use appearance=\"inline\" instead when the footer wants no control chrome at all. NEVER re-size the trigger with a page-local height/width class.",
      "DON'T hand-roll a locale/timezone/format Select — AppSettingPicker already composes Select + the right icon + translated, context-wired options. There is no separate LocalePicker/TimezonePicker/DateFormatPicker/TimeFormatPicker anymore; use kind."
    ],
    "useCases": [
      "CYCLE (menu={false}): Topbar light/dark/system switcher: <AppSettingPicker menu={false} kind=\"theme\" /> in a Topbar `end` slot, beside the other TopbarItem cells.",
      "CYCLE (menu={false}): Density or font-size step-through in an admin bar, for users who resize the grid all day: <AppSettingPicker menu={false} kind=\"density\" />.",
      "CYCLE (menu={false}): Clock-format flip (24h/12h) next to a schedule view: <AppSettingPicker menu={false} kind=\"timeFormat\" /> — the only kind that shows its value as text, since no glyph can say \"24-hour\".",
      "CYCLE (menu={false}): Settings row outside a bar: <AppSettingPicker menu={false} kind=\"theme\" appearance=\"icon\" /> beside its label.",
      "App-shell top-nav language switcher: <AppSettingPicker kind=\"locale\" /> under AppProvider, persisting to localStorage with no extra state.",
      "Topbar locale switcher (globe): <AppSettingPicker kind=\"locale\" appearance=\"bar\" /> in a Topbar `end` slot — a CELL of the bar: full bar height, squared to --topbar-item-radius, so its hover surface matches the TopbarItem beside it. appearance=\"icon\" is the same structural drops shaped as a CONTROL, for a toolbar or card header; in a taller bar it leaves a --control-height pill floating mid-strip and reads as a foreign control family. This line said \"icon\" while the prop doc said \"bar\", and a consumer duly shipped the pill.",
      "Auth-footer locale switch: <AppSettingPicker kind=\"locale\" appearance=\"labeled\" compact /> inside an <AuthFooter locale={…}> slot — the readable language name at the small control tier, hugging its value.",
      "User settings page with all four preferences — render kind=locale, kind=timezone, kind=dateFormat, kind=timeFormat together under one AppProvider.",
      "Onboarding step that picks language/timezone before the rest of the app is configured — AppProvider persist={false} + controlled values to keep state local.",
      "Storybook/test harness without AppProvider — fully controlled: <AppSettingPicker kind=\"timeFormat\" value=\"24h\" onValueChange={fn} />."
    ]
  },
  {
    "example": "{`import { Field, Switch } from \"@godxjp/ui/data-entry\";\n\nexport function NotifyRow() {\n  return (\n    <Field id=\"notify\" label=\"メール通知\" description=\"重要な更新をメールで受け取る\">\n      <Switch id=\"notify\" defaultChecked />\n    </Field>\n  );\n}`}",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "Field",
    "props": [
      {
        "description": "id wired to the control via htmlFor; pass the same id to the child control.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "The field label, rendered as a <Label>.",
        "name": "label",
        "type": "ReactNode"
      },
      {
        "description": "Content rendered BESIDE the label and OUTSIDE the <label> element — a help button, a Tooltip trigger, a status chip, a short text action. Same semantics as FormField.labelAddon. It must not go through `label`: Field's label is a real <label htmlFor> pointing at the checkbox/radio/switch, so a browser forwards a click anywhere inside it to that control and an interactive addon placed there TOGGLES the control when pressed (gh#812). A non-interactive mark (Badge as=\"span\") is still fine inside `label`.",
        "name": "labelAddon",
        "type": "ReactNode"
      },
      {
        "description": "Muted hint rendered under the label, announced with the control via aria-describedby. Composes with `error` — both lines stack, they do not replace each other.",
        "name": "description",
        "type": "ReactNode"
      },
      {
        "description": "Validation message under the description (role=alert). The message IS the invalid state: the control is cloned with aria-invalid, and the message id reaches it through aria-errormessage AND aria-describedby (react-aria's hidden input drops the former). This is how a validated boolean stays in Field — FormField must not wrap a bare Switch.",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "description": "The control (Checkbox/Radio/Switch) placed beside the label.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Extra CSS classes on the wrapper.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "FormField — block label/helper/required layout for text inputs; use it instead when the control stacks under its label. Field now carries `labelAddon` and `error` with FormField's semantics, so the two read as one idea (gh#812); what Field still does NOT have is `required`, `helper`, `name`/error-bag binding, layout/width knobs and `staticText`.",
      "Switch / Checkbox / Radio — the controls Field typically wraps."
    ],
    "rules": [
      23
    ],
    "tagline": "Label + optional description laid out beside a single checkbox/radio/switch control — the inline alternative to FormField's full block layout.",
    "usage": [
      "DO: Use Field to label a single boolean/choice control (Switch, Checkbox, Radio) in a compact two-column row — control beside label + description.",
      "DO: Match the child control's id to Field's id so the label is correctly associated.",
      "DO: Pass a pressable help affordance (Tooltip trigger, icon Button) through `labelAddon`, never through `label` — inside the <label> a press toggles the control (gh#812).",
      "DO: Use `error` for a validated boolean instead of hand-wiring aria-invalid / aria-describedby or smuggling the message through `description`. Field wires aria-invalid, aria-errormessage and aria-describedby onto the control itself.",
      "DON'T: Use Field for text inputs needing the full block label/required/helper layout — use FormField instead. There is no ChoiceField anymore; Field is the canonical name."
    ],
    "useCases": [
      "A settings list of toggle rows (notifications, auto-save) where each Switch has a label + description.",
      "A consent checkbox with an explanatory description beside it.",
      "A radio option row in a preferences form."
    ]
  },
  {
    "example": "import { LineChart } from \"@godxjp/ui/charts\";\n\n<LineChart\n  label={t(\"dashboard.revenueTrend\")}\n  data={data}\n  categoryKey=\"month\"\n  series={[\n    { dataKey: \"plan\", label: t(\"metric.plan\") },\n    { dataKey: \"actual\", label: t(\"metric.actual\") },\n  ]}\n  numberFormat={{ style: \"currency\", currency: \"JPY\" }}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/charts",
    "name": "LineChart",
    "props": [
      {
        "description": "Row data: one category per row with a numeric value per series.",
        "name": "data",
        "required": true,
        "type": "ChartDatum[]"
      },
      {
        "description": "Plotted series: { dataKey, label?, color?, fillColor? }. Colour defaults to the --chart-1..6 palette; fillColor is the AreaChart band only.",
        "name": "series",
        "required": true,
        "type": "ChartSeriesProp[]"
      },
      {
        "description": "Key into each datum holding the x-axis category label.",
        "name": "categoryKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Accessible name + visible caption (role=img needs a name).",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Paint `label` as a visible caption. Set false when a CardTitle or section heading already says it — the caption stays in the DOM as sr-only, so role=img keeps its accessible name.",
        "name": "showCaption",
        "type": "boolean"
      },
      {
        "description": "Extra context appended to the screen-reader description.",
        "name": "description",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Canvas height preset. Ignored when `height` is set.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Explicit canvas height in px (overrides `size`).",
        "name": "height",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Show the series legend.",
        "name": "showLegend",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Show the cartesian background grid.",
        "name": "showGrid",
        "type": "boolean"
      },
      {
        "description": "Locale-aware formatting for axis ticks + tooltip values.",
        "name": "numberFormat",
        "type": "Intl.NumberFormatOptions"
      },
      {
        "description": "Explicit [min, max] for the VALUE axis (y vertical, x on a horizontal bar). Omit to let the data set it. A non-zero baseline is a charting-ETHICS call: legitimate where the SHAPE is the message and zero is not a reference (price, temperature, an index, a latency percentile), misleading wherever the reader compares magnitudes — which is every bar chart.",
        "name": "valueDomain",
        "type": "[number, number]"
      },
      {
        "description": "Explicit tick positions on the value axis, in data units. Values outside valueDomain are not drawn.",
        "name": "valueTicks",
        "type": "number[]"
      },
      {
        "defaultValue": "false",
        "description": "Render smooth (monotone) lines instead of straight segments.",
        "name": "curved",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw a marker at every data point. Off by default — markers crowd a dense series.",
        "name": "showDots",
        "type": "boolean"
      },
      {
        "description": "Message shown when `data` is empty (defaults to a localized 'no data').",
        "name": "emptyMessage",
        "type": "string"
      }
    ],
    "related": [
      "AreaChart — use when the filled magnitude under the line matters (cumulative/stacked volume).",
      "BarChart — use for discrete category comparison rather than a continuous trend.",
      "DataTable — use when exact per-row figures matter more than the trend shape."
    ],
    "rules": [],
    "tagline": "Trends over an ordered category axis — one or more series, locale-formatted ticks/tooltips, screen-reader text alternative built in. Data-visualization graph / plot.",
    "usage": [
      "DO import from the tree-shaken charts entry: `import { LineChart } from \"@godxjp/ui/charts\";`. Importing any other subpath never pulls in recharts.",
      "DO import only the chart a screen uses — `import { LineChart } from \"@godxjp/ui/charts/line-chart\";` — when the `./charts` barrel should not link the whole chart family. Without the `recharts` peer the build then fails ONCE, naming the package and the fix.",
      "DO install the `recharts` optional peer dependency in the consuming app — charts are the only part of @godxjp/ui that needs it, so apps without charts never pay for it.",
      "DO pass an i18n'd `label` — it is both the visible caption and the accessible name; the component also emits a screen-reader list of the plotted values (WCAG 1.1.1).",
      "DO pre-translate each series' `label`; pass `numberFormat` (e.g. { style: 'currency', currency: 'JPY' }) and the axis/tooltip numbers localize automatically via Intl.",
      "DON'T hand-roll an SVG/canvas chart or drop raw recharts into a page — LineChart owns the colour tokens, locale formatting, empty state, and accessibility wiring.",
      "DO retune the line weight and the grid dash with the `--chart-series-stroke-width` / `--chart-grid-line-dash` theme tokens, globally or on a scoped [data-tenant] region. They are house style; there is no prop for them.",
      "DON'T reach for `valueDomain` to make a flat trend look dramatic — cropping the axis magnifies every wobble and the reader cannot tell a 2% drift from a collapse. Crop only where zero is not a meaningful reference, and say the range."
    ],
    "useCases": [
      "Revenue / KPI trend over months in a dashboard.",
      "Multi-series comparison (e.g. plan vs actual) across a time axis.",
      "Any continuous metric where the shape of the trend is the message."
    ]
  },
  {
    "example": "import { BarChart } from \"@godxjp/ui/charts\";\n\n<BarChart\n  label={t(\"report.salesByRegion\")}\n  data={data}\n  categoryKey=\"region\"\n  series={[{ dataKey: \"sales\", label: t(\"metric.sales\") }]}\n  numberFormat={{ notation: \"compact\" }}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/charts",
    "name": "BarChart",
    "props": [
      {
        "description": "Row data: one category per row with a numeric value per series.",
        "name": "data",
        "required": true,
        "type": "ChartDatum[]"
      },
      {
        "description": "Plotted series: { dataKey, label?, color?, fillColor? }. `fillColor` paints the filled band independently of the line's `color`; it defaults to `color`.",
        "name": "series",
        "required": true,
        "type": "ChartSeriesProp[]"
      },
      {
        "description": "Key into each datum holding the category label.",
        "name": "categoryKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Accessible name + visible caption.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Paint `label` as a visible caption. Set false when a CardTitle or section heading already says it — the caption stays in the DOM as sr-only, so role=img keeps its accessible name.",
        "name": "showCaption",
        "type": "boolean"
      },
      {
        "description": "Extra context appended to the screen-reader description.",
        "name": "description",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Canvas height preset. Ignored when `height` is set.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Explicit canvas height in px (overrides `size`).",
        "name": "height",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Show the series legend.",
        "name": "showLegend",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Show the cartesian background grid.",
        "name": "showGrid",
        "type": "boolean"
      },
      {
        "description": "Locale-aware formatting for ticks + tooltip values.",
        "name": "numberFormat",
        "type": "Intl.NumberFormatOptions"
      },
      {
        "description": "Explicit [min, max] for the VALUE axis (y vertical, x on a horizontal bar). Omit to let the data set it. A non-zero baseline is a charting-ETHICS call: legitimate where the SHAPE is the message and zero is not a reference (price, temperature, an index, a latency percentile), misleading wherever the reader compares magnitudes — which is every bar chart.",
        "name": "valueDomain",
        "type": "[number, number]"
      },
      {
        "description": "Explicit tick positions on the value axis, in data units. Values outside valueDomain are not drawn.",
        "name": "valueTicks",
        "type": "number[]"
      },
      {
        "defaultValue": "false",
        "description": "Stack series into one bar instead of grouping side by side.",
        "name": "stacked",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Lay bars out horizontally (category on the y-axis).",
        "name": "horizontal",
        "type": "boolean"
      },
      {
        "description": "Message shown when `data` is empty.",
        "name": "emptyMessage",
        "type": "string"
      }
    ],
    "related": [
      "LineChart — continuous trend rather than discrete comparison.",
      "PieChart — single part-to-whole composition.",
      "DataTable — exact tabular figures."
    ],
    "rules": [],
    "tagline": "Compare a value across categories — grouped or `stacked`, vertical or `horizontal`, with localized ticks/tooltips and a built-in text alternative. Data-visualization graph / plot / diagram.",
    "usage": [
      "DO import from the charts entry: `import { BarChart } from \"@godxjp/ui/charts\";` (recharts optional peer required).",
      "DO import only the chart a screen uses — `import { BarChart } from \"@godxjp/ui/charts/bar-chart\";` — when the `./charts` barrel should not link the whole chart family. Without the `recharts` peer the build then fails ONCE, naming the package and the fix.",
      "DO use `horizontal` when category labels are long (they read better on the y-axis).",
      "DO use `stacked` for part-to-whole-per-category; keep grouped (default) for direct side-by-side comparison.",
      "DON'T use BarChart for a single part-to-whole total — that is PieChart. DON'T fake bars with styled divs."
    ],
    "useCases": [
      "Sales by region / category comparison.",
      "Stacked composition per period (e.g. expense breakdown by month).",
      "Ranking with long labels (horizontal)."
    ]
  },
  {
    "example": "import { CompactBarTrend } from \"@godxjp/ui/charts/compact-bar-trend\";\nimport { Text } from \"@godxjp/ui/general\";\n\n<CompactBarTrend\n  label={t(\"dashboard.newOrganizations7d\")}\n  description={t(\"dashboard.newOrganizationsHint\")}\n  data={trend}\n  categoryKey=\"date\"\n  valueKey=\"count\"\n  emphasizedIndex={-1}\n  size=\"xs\"\n  footer={<Text size=\"xs\" tone=\"muted\">{t(\"dashboard.lastUpdated\", { at: new Date().toISOString() })}</Text>}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/charts/compact-bar-trend",
    "name": "CompactBarTrend",
    "props": [
      {
        "description": "Row data — one bar per row. Any point count (7 is not hard-coded).",
        "name": "data",
        "required": true,
        "type": "ChartDatum[]"
      },
      {
        "description": "Key into each datum holding the category (tick) label.",
        "name": "categoryKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Key into each datum holding the plotted numeric value.",
        "name": "valueKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Accessible name + visible caption.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Paint `label` as a visible caption. Set false when a CardTitle or section heading already says it — the caption stays in the DOM as sr-only, so role=img keeps its accessible name.",
        "name": "showCaption",
        "type": "boolean"
      },
      {
        "description": "Extra context appended to the screen-reader description.",
        "name": "description",
        "type": "string"
      },
      {
        "defaultValue": "\"xs\"",
        "description": "Plot-height tier (--chart-trend-plot-height-*). xs is the dashboard summary-card density.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Index of the emphasized 'current' bar. Negative counts from the end (-1 = latest); out of range = no emphasis. Also annotated in the text alternative, so it is never colour-only.",
        "name": "emphasizedIndex",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Render the category tick labels under the plot.",
        "name": "showCategoryLabels",
        "type": "boolean"
      },
      {
        "description": "Locale-aware formatting for the values in the text alternative.",
        "name": "numberFormat",
        "type": "Intl.NumberFormatOptions"
      },
      {
        "description": "Activity footer slot below the plot, rendered OUTSIDE the role=img graphic so links/buttons in it stay reachable.",
        "name": "footer",
        "type": "ReactNode"
      },
      {
        "description": "Message shown when `data` is empty.",
        "name": "emptyMessage",
        "type": "string"
      },
      {
        "description": "Forwarded to the <figure> element.",
        "name": "ref",
        "type": "React.Ref<HTMLElement>"
      }
    ],
    "related": [
      "BarChart — full cartesian bar chart with axes/grid/tooltip/legend (requires the recharts optional peer).",
      "StatCard — the KPI headline this trend usually sits under.",
      "Progress — one ratio against a target, not a series over time."
    ],
    "rules": [],
    "storyPath": "charts/compact-bar-trend.tsx",
    "tagline": "DEPENDENCY-FREE compact vertical bar trend for dashboard summary cards — N category/value pairs, muted marks plus ONE emphasized 'current' bar, all geometry from --chart-trend-* tokens. Needs NO recharts. Sparkline / micro-chart / activity pulse / KPI trend strip.",
    "usage": [
      "DO import from the isolated entry: `import { CompactBarTrend } from \"@godxjp/ui/charts/compact-bar-trend\";` — unlike the charts barrel, this path never links modules that require the optional `recharts` peer.",
      "DO reach for it INSIDE a dashboard summary card (a 7-day signup/organization/activity strip under a StatCard headline) — that is the density `size=\"xs\"` is tuned for.",
      "DO mark the current period with `emphasizedIndex={-1}` (or an explicit index); the highlight is duplicated in the screen-reader text alternative, so it never depends on colour alone.",
      "DO retheme through the `--chart-trend-*` tokens (bar gap/radius/width, plot heights, muted + emphasis fills, opt-in baseline). NEVER add page-local CSS, an inline height calculation, or a hardcoded colour.",
      "DON'T use it when you need axes, a grid, tooltips, multiple series, or a continuous trend — that is BarChart / LineChart / AreaChart (recharts peer). DON'T fake bars with styled divs in the app.",
      "DON'T put interactive content in the plot: anything clickable goes in `footer`, which renders outside the `role=\"img\"` graphic."
    ],
    "useCases": [
      "Seven-day new-organizations / new-users trend inside an admin dashboard summary card (SCR-201).",
      "Weekly activity pulse beside a KPI headline, where the SHAPE matters more than exact figures.",
      "A dependency-constrained app (no recharts) that still needs a token-driven chart."
    ]
  },
  {
    "example": "import { HeatmapChart } from \"@godxjp/ui/charts/heatmap-chart\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\nconst { t, locale } = useTranslation();\nconst weekday = new Intl.DateTimeFormat(locale, { weekday: \"short\", timeZone: \"UTC\" });\nconst rows = [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"].map((key, day) => ({\n  key,\n  label: weekday.format(new Date(Date.UTC(2024, 0, 1 + day))), // 2024-01-01 is a Monday\n}));\nconst columns = Array.from({ length: 24 }, (_, hour) => ({ key: hour, label: new Intl.NumberFormat(locale).format(hour) }));\n\n<HeatmapChart\n  label={t(\"dashboard.revenueByHour\")}\n  rows={rows}\n  columns={columns}\n  data={slots /* [{ row: \"fri\", column: 19, value: 48000 }, …] — omit closed hours */}\n  numberFormat={{ style: \"currency\", currency: \"JPY\" }}\n  topN={3}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/charts/heatmap-chart",
    "name": "HeatmapChart",
    "props": [
      {
        "description": "Row categories, top to bottom (the y axis — weekdays). `label` is already translated.",
        "name": "rows",
        "required": true,
        "type": "{ key: string | number; label: string }[]"
      },
      {
        "description": "Column categories, start to end (the x axis — hours). `label` is already translated.",
        "name": "columns",
        "required": true,
        "type": "{ key: string | number; label: string }[]"
      },
      {
        "description": "Cell values keyed by row + column key. A cell that is absent, null, undefined or NaN is EMPTY ('no data') — never painted as a zero.",
        "name": "data",
        "required": true,
        "type": "{ row: string | number; column: string | number; value: number | null | undefined }[]"
      },
      {
        "description": "Accessible name of the figure and its grid + visible caption.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Paint `label` as a visible caption. Set false under a CardTitle that already says it — the caption stays sr-only so the grid keeps its name.",
        "name": "showCaption",
        "type": "boolean"
      },
      {
        "description": "Extra context prepended to the screen-reader summary.",
        "name": "description",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "CELL size tier (--chart-heatmap-cell-size-*: 24/28/32/40px) — not a canvas height. A cell never shrinks below it; when the columns do not fit, the matrix scrolls inside its own box with the row header pinned.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Locale-aware formatting for cell values, the tooltip, the legend ranges and the summary (e.g. `{ style: 'currency', currency: 'JPY' }`).",
        "name": "numberFormat",
        "type": "Intl.NumberFormatOptions"
      },
      {
        "description": "Emphasize the N highest cells with an outline ring plus a 'Top k' note in the tooltip, the cell text and the legend — never colour alone. Ties keep `data` order.",
        "name": "topN",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Colour-scale legend (min … max, one readable range per swatch) plus the 'no data' and 'top N' keys when they apply.",
        "name": "showLegend",
        "type": "boolean"
      },
      {
        "description": "Shown when there are no rows, no columns or no cells with a value.",
        "name": "emptyMessage",
        "type": "string"
      },
      {
        "description": "Forwarded to the <figure> element.",
        "name": "ref",
        "type": "React.Ref<HTMLElement>"
      }
    ],
    "related": [
      "CompactBarTrend — one row of values; the other dependency-free chart.",
      "BarChart — exact comparison across a single category axis (recharts peer).",
      "DataTable — when the reader needs to sort, filter or read exact numbers first."
    ],
    "rules": [],
    "storyPath": "charts/heatmap-chart.tsx",
    "tagline": "DEPENDENCY-FREE rows × columns heatmap — weekday × hour revenue / order counts, cell intensity from the sequential CVD-safe --chart-heat-1..5 ramp. Empty ≠ zero (dashed outline + 'no data' text), optional top-N ring, per-cell tooltip, APG grid keyboard, the table itself is the text alternative. Needs NO recharts. Matrix / punch card / activity heatmap / busy-hours chart.",
    "usage": [
      "DO import from the isolated entry: `import { HeatmapChart } from \"@godxjp/ui/charts/heatmap-chart\";` — it never links the optional `recharts` peer.",
      "DO derive axis labels with Intl in the active locale — weekdays via `Intl.DateTimeFormat(locale, { weekday: 'short' })`, hours via `Intl.NumberFormat(locale)` — and pass money through `numberFormat`, never a hand-built '¥' string.",
      "DO leave a slot OUT of `data` (or pass `value: null`) when there is no measurement — a closed hour, a day before the store opened. Pass `0` only for a real zero. The two render differently (dashed outline vs filled) and read differently ('No data' vs '¥0').",
      "DO use `topN` to call out the peak slots; the ring is a shape and the rank is text, so the emphasis survives greyscale and colour-vision deficiency.",
      "DO retheme through the tokens: the ramp is the foundation `--chart-heat-1..5` (light + dark), geometry and cues are `--chart-heatmap-*`. NEVER tint cells from the app, add page CSS, or fake the matrix with a styled Table.",
      "DON'T add a separate sr-only table: the matrix IS a native data table with `role=\"grid\"` and header cells, so the visible chart and its text alternative cannot drift apart.",
      "DON'T use it for a continuous series over time (LineChart / AreaChart) or one row of values (CompactBarTrend)."
    ],
    "useCases": [
      "Restaurant / retail HQ: revenue or order count by weekday × hour across the chain (POS busy-hours).",
      "Support desk: ticket volume by weekday × hour, to plan staffing.",
      "Any categorical matrix where intensity matters more than the exact figure: store × week, region × product."
    ]
  },
  {
    "example": "import { AreaChart } from \"@godxjp/ui/charts\";\n\n<AreaChart\n  label={t(\"dashboard.trafficByChannel\")}\n  data={data}\n  categoryKey=\"day\"\n  series={[\n    { dataKey: \"organic\", label: t(\"channel.organic\") },\n    { dataKey: \"paid\", label: t(\"channel.paid\") },\n  ]}\n  stacked\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/charts",
    "name": "AreaChart",
    "props": [
      {
        "description": "Row data: one category per row with a numeric value per series.",
        "name": "data",
        "required": true,
        "type": "ChartDatum[]"
      },
      {
        "description": "Plotted series: { dataKey, label?, color? }.",
        "name": "series",
        "required": true,
        "type": "ChartSeriesProp[]"
      },
      {
        "description": "Key into each datum holding the x-axis category label.",
        "name": "categoryKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Accessible name + visible caption.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Paint `label` as a visible caption. Set false when a CardTitle or section heading already says it — the caption stays in the DOM as sr-only, so role=img keeps its accessible name.",
        "name": "showCaption",
        "type": "boolean"
      },
      {
        "description": "Extra context appended to the screen-reader description.",
        "name": "description",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Canvas height preset. Ignored when `height` is set.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Explicit canvas height in px (overrides `size`).",
        "name": "height",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Show the series legend.",
        "name": "showLegend",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Show the cartesian background grid.",
        "name": "showGrid",
        "type": "boolean"
      },
      {
        "description": "Locale-aware formatting for ticks + tooltip values.",
        "name": "numberFormat",
        "type": "Intl.NumberFormatOptions"
      },
      {
        "defaultValue": "false",
        "description": "Stack series areas instead of overlaying them.",
        "name": "stacked",
        "type": "boolean"
      },
      {
        "description": "Explicit [min, max] for the VALUE axis (y vertical, x on a horizontal bar). Omit to let the data set it. A non-zero baseline is a charting-ETHICS call: legitimate where the SHAPE is the message and zero is not a reference (price, temperature, an index, a latency percentile), misleading wherever the reader compares magnitudes — which is every bar chart.",
        "name": "valueDomain",
        "type": "[number, number]"
      },
      {
        "description": "Explicit tick positions on the value axis, in data units. Values outside valueDomain are not drawn.",
        "name": "valueTicks",
        "type": "number[]"
      },
      {
        "defaultValue": "false",
        "description": "Render smooth (monotone) areas instead of straight segments.",
        "name": "curved",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Draw a marker at every data point. Off by default — markers crowd a dense series.",
        "name": "showDots",
        "type": "boolean"
      },
      {
        "description": "Message shown when `data` is empty.",
        "name": "emptyMessage",
        "type": "string"
      }
    ],
    "related": [
      "LineChart — when only the trend line matters, not the filled magnitude.",
      "BarChart — discrete category comparison."
    ],
    "rules": [],
    "tagline": "Magnitude over an ordered category axis — overlay or `stacked` areas, optional `curved` smoothing, localized formatting + text alternative. Data-visualization graph / plot.",
    "usage": [
      "DO import from the charts entry: `import { AreaChart } from \"@godxjp/ui/charts\";` (recharts optional peer required).",
      "DO import only the chart a screen uses — `import { AreaChart } from \"@godxjp/ui/charts/area-chart\";` — when the `./charts` barrel should not link the whole chart family. Without the `recharts` peer the build then fails ONCE, naming the package and the fix.",
      "DO use `stacked` to show how parts accumulate into a total over time.",
      "DON'T overlay more than 2-3 unstacked areas — fill opacity makes dense overlays unreadable; switch to LineChart.",
      "DO split the band's hue from the line's with `series[].fillColor` when a brand specifies both (a dark line over a lighter wash). The band's DENSITY is the `--chart-area-fill-alpha` theme token, not a prop.",
      "DON'T tune the band by dropping a page-local CSS rule on `.recharts-area-area` — `--chart-area-fill-alpha` is the supported knob and it follows a scoped [data-tenant] region."
    ],
    "useCases": [
      "Cumulative volume over time (e.g. total transactions per day).",
      "Stacked composition trend (traffic sources, revenue streams)."
    ]
  },
  {
    "example": "import { PieChart } from \"@godxjp/ui/charts\";\n\n<PieChart\n  label={t(\"dashboard.expenseSplit\")}\n  data={data}\n  dataKey=\"amount\"\n  nameKey=\"category\"\n  numberFormat={{ style: \"currency\", currency: \"JPY\" }}\n  donut\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/charts",
    "name": "PieChart",
    "props": [
      {
        "description": "Row data: one slice per row.",
        "name": "data",
        "required": true,
        "type": "ChartDatum[]"
      },
      {
        "description": "Key into each datum holding the slice's numeric value.",
        "name": "dataKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Key into each datum holding the slice's category label.",
        "name": "nameKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Accessible name + visible caption.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "Paint `label` as a visible caption. Set false when a CardTitle or section heading already says it — the caption stays in the DOM as sr-only, so role=img keeps its accessible name.",
        "name": "showCaption",
        "type": "boolean"
      },
      {
        "description": "Per-slice colours by index (defaults to the --chart-1..6 palette).",
        "name": "colors",
        "type": "string[]"
      },
      {
        "description": "Extra context appended to the screen-reader description.",
        "name": "description",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Canvas height preset. Ignored when `height` is set.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Explicit canvas height in px (overrides `size`).",
        "name": "height",
        "type": "number"
      },
      {
        "defaultValue": "true",
        "description": "Show the slice legend.",
        "name": "showLegend",
        "type": "boolean"
      },
      {
        "description": "Locale-aware formatting for tooltip values.",
        "name": "numberFormat",
        "type": "Intl.NumberFormatOptions"
      },
      {
        "defaultValue": "false",
        "description": "Render a donut (hollow centre) instead of a full pie.",
        "name": "donut",
        "type": "boolean"
      },
      {
        "description": "Message shown when `data` is empty.",
        "name": "emptyMessage",
        "type": "string"
      }
    ],
    "related": [
      "BarChart — when there are many categories or precise comparison matters.",
      "Progress — single ratio against a target rather than a multi-slice split."
    ],
    "rules": [],
    "tagline": "Part-to-whole composition across a small set of slices — `donut` option, localized tooltips, and a screen-reader breakdown of every slice. Data-visualization graph / diagram.",
    "usage": [
      "DO import from the charts entry: `import { PieChart } from \"@godxjp/ui/charts\";` (recharts optional peer required).",
      "DO import only the chart a screen uses — `import { PieChart } from \"@godxjp/ui/charts/pie-chart\";` — when the `./charts` barrel should not link the whole chart family. Without the `recharts` peer the build then fails ONCE, naming the package and the fix.",
      "DO keep slices few (≈2–6) — pies are unreadable past a handful; use BarChart for many categories.",
      "DO pass `numberFormat` (e.g. percent or currency) so tooltip values localize.",
      "DON'T use a pie for trends over time (LineChart/AreaChart) or precise comparison (BarChart)."
    ],
    "useCases": [
      "Budget / expense split across a few categories.",
      "Market or status share (e.g. paid vs overdue vs draft)."
    ]
  },
  {
    "example": "import { CommandPalette } from \"@godxjp/ui/data-entry\";\n\n<CommandPalette\n  groups={[{ id: \"pages\", label: \"Pages\", items: [{ id: \"home\", label: \"Home\" }] }]}\n  labels={{ open: \"Open commands\", title: \"Commands\", description: \"Choose a command\", placeholder: \"Search\", empty: \"No results\" }}\n  onSelect={(item) => navigate(item.id)}\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "CommandPalette",
    "props": [
      {
        "description": "Grouped command items.",
        "name": "groups",
        "required": true,
        "type": "CommandPaletteGroup[]"
      },
      {
        "description": "Decorative glyph before an item's label — a folder, a file, an emoji (gh#1159). Menu-row icon size (--menu-icon-size), aria-hidden: the label stays the option's name.",
        "name": "groups[].items[].icon",
        "type": "ReactNode"
      },
      {
        "description": "Localized dialog and search copy.",
        "name": "labels",
        "required": true,
        "type": "CommandPaletteLabels"
      },
      {
        "description": "Consumer-owned selection handler. The second argument carries the modifier keys held on the Enter or click that chose the item — build \"open in split / new tab\" on ⌘/Ctrl/Shift (gh#1126).",
        "name": "onSelect",
        "required": true,
        "type": "(item: CommandPaletteItem, modifiers: { metaKey, ctrlKey, shiftKey, altKey }) => void"
      },
      {
        "defaultValue": "true",
        "description": "The global toggle shortcut. `true` = ⌘K / Ctrl+K, `false` = none, or a combo string — `\"mod+o\"`, `\"mod+p\"`, `\"mod+shift+p\"` — so a quick switcher and a command palette can live side by side. `mod` is ⌘ on Apple platforms and Ctrl elsewhere — never both, so Ctrl+O / Ctrl+P keep their editing meaning on a Mac (gh#1129); Shift / Alt must match exactly. Never fires during IME composition; the default trigger shows the combo (gh#1126).",
        "name": "shortcut",
        "type": "boolean | string"
      },
      {
        "description": "Controlled open state.",
        "name": "open",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Initial uncontrolled open state.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Open-state callback.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Controlled search-box query. Pairs with `onSearchChange`.",
        "name": "search",
        "type": "string"
      },
      {
        "defaultValue": "\"\"",
        "description": "Initial uncontrolled query, and the value the palette resets to when it closes.",
        "name": "defaultSearch",
        "type": "string"
      },
      {
        "description": "Fires on every keystroke with the current query — the seam for search-as-you-type. Also fires with `defaultSearch` when the palette closes.",
        "name": "onSearchChange",
        "type": "(query: string) => void"
      },
      {
        "defaultValue": "true",
        "description": "Whether the palette filters `groups` itself. Set false for server-side search, which also hands the empty node to the palette (derived from `groups`).",
        "name": "shouldFilter",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Shows the supplied loading content.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Consumer-supplied error content.",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "description": "Recent queries (gh#1249), rendered as the FIRST group only while the query is empty, followed by a trailing \"Clear recent searches\" row. Choosing a query calls `onSelect(query)` and closes; the clear row calls `onClear()` and keeps the palette open. The library never reads or writes `localStorage` — the consumer stores, de-duplicates and caps `items`; duplicates are rendered as given. `label` replaces the default heading (`dataEntry.commandPalette.recent`).",
        "name": "recent",
        "type": "{ items: string[]; onSelect: (query: string) => void; onClear: () => void; label?: ReactNode }"
      }
    ],
    "rules": [],
    "storyPath": "data-entry/command-palette.tsx",
    "tagline": "Searchable command dialog with controlled or uncontrolled open state and consumer-owned selection.",
    "usage": [
      "Provide localized labels and real command groups; the component does not fetch commands.",
      "Recent searches: keep the list in your own store (state, URL, server, localStorage), de-duplicate it, and pass it as `recent` — the palette only renders it while the query is empty.",
      "Use either `open` plus `onOpenChange` or `defaultOpen`; do not mirror both state models.",
      "Search-as-you-type against an API: read the query from `onSearchChange`, pass the results back as `groups`, and set `shouldFilter={false}` so the rows are not scored a second time against the same string.",
      "The empty-state contract: with `shouldFilter` (default) cmdk decides — items exist, the query matches none. With `shouldFilter={false}` the PALETTE decides from props — `labels.empty` renders when `groups` carries no items, and never while `loading` or `error` is set. Hold `loading` for the whole in-flight window: a request that has not answered yet is not an empty result, and asserting over cmdk's own empty node races an async group that populates a frame late."
    ]
  },
  {
    "docPath": "docs/data-entry/emoji-picker.tsx",
    "example": "import { EmojiPicker } from \"@godxjp/ui/lab\";\n\n<EmojiPicker value={icon} onValueChange={setIcon} />",
    "group": "data-entry",
    "importPath": "@godxjp/ui/lab",
    "name": "EmojiPicker",
    "props": [
      {
        "description": "The chosen emoji (controlled with onValueChange).",
        "name": "value",
        "type": "string | null"
      },
      {
        "description": "Uncontrolled initial emoji.",
        "name": "defaultValue",
        "type": "string | null"
      },
      {
        "description": "Picked emoji, or null from \"remove icon\".",
        "name": "onValueChange",
        "type": "(emoji: string | null) => void"
      },
      {
        "description": "The popover.",
        "name": "open / defaultOpen / onOpenChange",
        "type": "boolean / boolean / (open) => void"
      },
      {
        "defaultValue": "true",
        "description": "Offer \"remove icon\" while a value is set.",
        "name": "removable",
        "type": "boolean"
      },
      {
        "defaultValue": "\"godxjp-ui:emoji-recents\"",
        "description": "localStorage key for the per-user recents.",
        "name": "recentsKey",
        "type": "string"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Trigger size.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Replace the default trigger (e.g. the page icon drawn large).",
        "name": "trigger",
        "type": "ReactNode"
      },
      {
        "description": "Disables the trigger.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Accessible name for the trigger; defaults to the localized \"Choose an icon\".",
        "name": "aria-label",
        "type": "string"
      },
      {
        "description": "Id on the trigger button.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "PageContainer — `icon` slot.",
      "CommandPalette — items can carry an emoji `icon`."
    ],
    "rules": [],
    "storyPath": "data-entry/emoji-picker.tsx",
    "tagline": "Page icon / reaction picker in a Popover (gh#1164): search by the active language's CLDR keywords (ja / en / vi — emojibase-data, loaded lazily for that locale only), categories, per-user recents, a WAI-ARIA grid (one tab stop, arrows, Home / End, Enter), and \"remove icon\". No skin tones.",
    "tier": "lab",
    "usage": [
      "DO use it for a page icon next to PageContainer `icon` — pass the icon itself as `trigger`.",
      "The data loads on first open (one locale, ~100 KB gzip); nothing ships in your main bundle."
    ]
  },
  {
    "docPath": "docs/data-entry/sortable-list.tsx",
    "example": "import { SortableList } from \"@godxjp/ui/lab\";\n\n<SortableList\n  aria-label=\"Widgets\"\n  items={[{ value: \"cal\", label: \"Calendar\" }, { value: \"mail\", label: \"Mail\" }]}\n  value={order}\n  onValueChange={setOrder}\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/lab",
    "name": "SortableList",
    "props": [
      {
        "description": "The rows to order. `label` is spoken in the grip's name and the announcements; a disabled item's grip is inert.",
        "name": "items",
        "required": true,
        "type": "{ value: string; label: string; disabled?: boolean }[]"
      },
      {
        "description": "The item keys in order (controlled with onValueChange).",
        "name": "value",
        "type": "string[]"
      },
      {
        "description": "Uncontrolled initial order.",
        "name": "defaultValue",
        "type": "string[]"
      },
      {
        "description": "Called ONCE per drop with the new order — not on every preview step.",
        "name": "onValueChange",
        "type": "(keys: string[]) => void"
      },
      {
        "description": "Draws an item's content beside its grip. Default: the label.",
        "name": "renderItem",
        "type": "(item, { index, dragging }) => ReactNode"
      },
      {
        "defaultValue": "\"list\"",
        "description": "Stacked rows, or tiles auto-filled into columns (--sortable-list-grid-item-min-inline-size). The keyboard order is the same either way.",
        "name": "layout",
        "type": "\"list\" | \"grid\""
      },
      {
        "description": "Every grip is inert.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Accessible name of the list.",
        "name": "aria-label",
        "type": "string"
      },
      {
        "description": "Id on the root.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Class on the root.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Tree — `draggable` for moving nodes within a hierarchy.",
      "Transfer — choose items, not order them."
    ],
    "rules": [],
    "storyPath": "data-entry/sortable-list.tsx",
    "tagline": "Reorder a flat list or a grid of tiles (gh#1173). Each item has a grip: drag it with a pointer (touch included — the grip sets touch-action: none), or focus it and press Space/Enter to lift, the arrows (Home / End) to move, Space/Enter to drop, Esc to cancel. Every step is spoken in a polite live region (\"2 of 3\"). The order is the value.",
    "tier": "lab",
    "usage": [
      "DO persist the order in onValueChange — it fires once per drop, so it is safe to save there.",
      "DO give every item a human `label`: a screen reader hears it on lift, every move and drop.",
      "DON'T nest SortableList, and don't put another drag gesture inside an item: the grip is the one drag surface.",
      "Hierarchy (move INTO another node) is Tree `draggable`, not SortableList — this orders a flat list."
    ],
    "useCases": [
      "Desktop / dashboard editor: reorder widget tiles (layout=\"grid\").",
      "Settings: order of tabs, columns, menu entries."
    ]
  },
  {
    "example": "import { TwoFactorSetup } from \"@godxjp/ui/feedback\";\n\n<TwoFactorSetup\n  open={open}\n  onOpenChange={setOpen}\n  qrValue={enrollment.qr}\n  manualKey={enrollment.secret}\n  code={code}\n  onCodeChange={setCode}\n  onConfirm={verify}\n  recoveryCodes={recoveryCodes}\n  onAcknowledge={finish}\n  labels={labels}\n/>",
    "group": "feedback",
    "importPath": "@godxjp/ui/feedback",
    "name": "TwoFactorSetup",
    "props": [
      {
        "description": "Controlled dialog state.",
        "name": "open",
        "required": true,
        "type": "boolean"
      },
      {
        "description": "Dialog state callback.",
        "name": "onOpenChange",
        "required": true,
        "type": "(open: boolean) => void"
      },
      {
        "description": "Consumer-provided enrollment secret.",
        "name": "manualKey",
        "required": true,
        "type": "string"
      },
      {
        "description": "Optional QR payload for the same enrollment secret.",
        "name": "qrValue",
        "type": "string"
      },
      {
        "description": "Current verification code.",
        "name": "code",
        "required": true,
        "type": "string"
      },
      {
        "description": "Verification-code callback.",
        "name": "onCodeChange",
        "required": true,
        "type": "(code: string) => void"
      },
      {
        "description": "Consumer-owned verification action.",
        "name": "onConfirm",
        "required": true,
        "type": "() => void"
      },
      {
        "description": "Codes shown only after successful enrollment.",
        "name": "recoveryCodes",
        "type": "string[]"
      },
      {
        "description": "Recovery-code acknowledgement action.",
        "name": "onAcknowledge",
        "required": true,
        "type": "() => void"
      },
      {
        "description": "All localized copy.",
        "name": "labels",
        "required": true,
        "type": "TwoFactorSetupLabels"
      },
      {
        "defaultValue": "false",
        "description": "Disables actions while the consumer request runs.",
        "name": "pending",
        "type": "boolean"
      }
    ],
    "rules": [],
    "storyPath": "feedback/two-factor-setup.tsx",
    "tagline": "Canonical two-factor enrollment dialog for QR/manual-key verification and recovery-code acknowledgement.",
    "usage": [
      "Keep enrollment, verification, persistence, and secret lifecycle in the consumer; this component is presentational.",
      "Only pass recovery codes after verification succeeds, and clear them when the dialog lifecycle ends."
    ]
  },
  {
    "example": "import { AuthDivider } from \"@godxjp/ui/layout\";\n\n<AuthDivider label=\"or\" />",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AuthDivider",
    "props": [
      {
        "description": "Localized conjunction such as “or”.",
        "name": "label",
        "required": true,
        "type": "string"
      },
      {
        "description": "Optional structural class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Separator — the primitive this presets; use it directly for any labelled rule outside auth."
    ],
    "rules": [
      44,
      45
    ],
    "storyPath": "layout/auth-divider.tsx",
    "tagline": "The auth-scoped PRESET over `Separator label` — a localized conjunction between two equal rules.",
    "usage": [
      "DO use AuthDivider only INSIDE an auth form. It is a thin preset over `<Separator label>`: it re-points the --separator-* knobs at the --auth-shell-divider-* layer (the 11px auth micro-scale, the auth rule/label colours), so using it elsewhere drags auth geometry into an unrelated screen.",
      "DON'T use it for a message stream's day divider or a \"new messages\" watermark — that is `<Separator label>` with a `labelAlign` and a `tone`. Misusing AuthDivider recreates the exact defect the preset removed: a service retuning its login divider silently retuned every divider in its chat."
    ],
    "useCases": [
      "An “or” conjunction between a credential form and a provider action row"
    ]
  },
  {
    "example": "import { AuthFooter } from \"@godxjp/ui/layout\";\n\n<AuthFooter product=\"Acme ID\" terms={<a href=\"/terms\">Terms</a>} privacy={<a href=\"/privacy\">Privacy</a>} locale=\"English\" />",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AuthFooter",
    "props": [
      {
        "description": "Product identity content.",
        "name": "product",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Terms link or localized text.",
        "name": "terms",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Privacy link or localized text.",
        "name": "privacy",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Optional consumer-owned locale control.",
        "name": "locale",
        "type": "ReactNode"
      },
      {
        "description": "Optional structural class override merged onto the footer root.",
        "name": "className",
        "type": "string"
      }
    ],
    "rules": [
      45
    ],
    "storyPath": "layout/auth-footer.tsx",
    "tagline": "Compact hosted-auth footer for consumer-owned product, legal, privacy, and locale content.",
    "usage": [
      "Pass real links and locale controls from the consumer; AuthFooter never invents navigation.",
      "DO drop it into the auth CenteredShell's `footer` slot — that slot supplies the contentinfo landmark, so AuthFooter itself renders a plain div and can also sit inside an existing footer without nesting landmarks.",
      "AuthFooter owns ONLY the geometry: mono ramp, wrap, and a `·` separator between the slots that are actually PRESENT (omit `locale` and its separator disappears). Don't hand-write separators into the slot content.",
      "Public type: `AuthFooterProp` (alias `AuthFooterProps`) from `@godxjp/ui/layout` — registered in the prop registry, not a local interface.",
      "Retune the line through `--auth-footer-content-gap` / `--auth-footer-text-font-size`, never page CSS (rule #45)."
    ]
  },
  {
    "example": "import { AuthIdentity } from \"@godxjp/ui/layout\";\nimport { Logo } from \"@godxjp/ui/general\";\n\n<AuthIdentity title=\"Sign in\" requester=\"Acme Portal is requesting access\" />\n\n// The product's own lockup, with no artwork in the consumer's repo:\n<AuthIdentity title=\"GoDX ID\" brand={<Logo mark=\"godx-lockup\" productSuffix=\"ID\" />} />",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AuthIdentity",
    "props": [
      {
        "description": "Primary auth heading.",
        "name": "title",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Brand artwork in the MARK's place, for a service whose design ships a real lockup (mark + wordmark, sometimes a product suffix). Omitted → the active preset's `brandMark` (`<AppProvider preset={godxPreset}>` supplies the GoDX mark), otherwise NO mark: a neutral package draws no product's logo. `null` → no mark, preset or not. data-slot, .ui-auth-identity spacing and the h1 are untouched, so swapping artwork never forks the block.",
        "name": "brand",
        "type": "ReactNode"
      },
      {
        "description": "Optional real requesting-client context.",
        "name": "requester",
        "type": "ReactNode"
      },
      {
        "description": "Optional structural class override merged onto the identity root.",
        "name": "className",
        "type": "string"
      }
    ],
    "rules": [
      45
    ],
    "storyPath": "layout/auth-identity.tsx",
    "tagline": "Hosted-auth identity heading with the shared mark (or the product's own lockup) and optional requesting-client context.",
    "usage": [
      "Only show `requester` when the consumer has authoritative client context.",
      "It renders the page h1 plus a mark: the `brand` you pass, else the active preset's `brandMark`, else none (v32: the neutral default draws no product logo). Under `<AppProvider preset={godxPreset}>` (from `@godxjp/ui/themes/godx`, with `@godxjp/ui/themes/godx.css`) that is the brand-green GoDX mark — don't add a second Logo or heading above it.",
      "To show YOUR OWN lockup instead, pass it as `brand` — never wrap or rebuild the block. `brand` replaces the mark only, so the h1 stays; if your lockup already spells the product name, make `title` the SCREEN'S PURPOSE (\"Sign in\") rather than repeating the brand.",
      "DO keep `brand` non-interactive. The slot is DECORATIVE at every fill: the mark it replaces was always aria-hidden, and a real lockup is not a mark — `Logo mark=\"godx-lockup\" productSuffix=\"ID\"` exposes \"GoDX ID\" as real text, so left in the tree beside the h1 the block announces the product twice (measured: \"GoDXIDGoDX ID\"). aria-hidden is merged onto your element, and aria-hidden over a focusable child is a WCAG failure.",
      "Centring and rhythm are token-owned (`--auth-identity-gap` / `--auth-requester-*`); no page CSS (rule #45).",
      "Public type: `AuthIdentityProp` (alias `AuthIdentityProps`) from `@godxjp/ui/layout` — registered in the prop registry, not a local interface."
    ]
  },
  {
    "example": "import { AccountChip } from \"@godxjp/ui/layout\";\n\n<PageContainer title=\"Bug report\" extra={<><Button variant=\"outline\">History</Button><AccountChip name={user.name} email={user.email} actionLabel={t(\"auth.signOut\")} onAction={signOut} /><Button>Send</Button></>}>…</PageContainer>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AccountChip",
    "props": [
      {
        "description": "Display name; its first character is the avatar fallback.",
        "name": "name",
        "required": true,
        "type": "string"
      },
      {
        "description": "Shown as the chip's title (hover / assistive tooltip).",
        "name": "email",
        "type": "string"
      },
      {
        "description": "Optional avatar image URL.",
        "name": "avatarSrc",
        "type": "string"
      },
      {
        "description": "Overrides the first-character fallback.",
        "name": "avatarFallback",
        "type": "ReactNode"
      },
      {
        "description": "Accessible name of the action button, e.g. a localized sign-out label.",
        "name": "actionLabel",
        "type": "ReactNode"
      },
      {
        "description": "Action handler; omit it to render no button.",
        "name": "onAction",
        "type": "() => void"
      },
      {
        "description": "Disables the action button.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Optional structural class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "rules": [
      40
    ],
    "storyPath": "layout/account-chip.tsx",
    "tagline": "Signed-in user for a PageContainer extra slot, Topbar or footer row: avatar, name and one ghost action at the control tier height — never a hand-rolled rounded-full border div.",
    "usage": [
      "Put it in PageContainer `extra` beside Buttons and a Select width=\"auto\": every control in that row shares --control-height.",
      "Pass a localized actionLabel with onAction; the component owns no route, session or permission behaviour.",
      "DO compose from the package (it is Avatar + Text + Button in a Flex row); DON'T hand-roll <div className=\"inline-flex rounded-full border py-0.5\"> — it drifts from the control height and the audit flags it (no-hand-rolled-surface)."
    ]
  },
  {
    "example": "import { AuthAccountSummary } from \"@godxjp/ui/layout\";\n\n<AuthAccountSummary email={user.email} actionLabel={t(\"auth.switchAccount\")} onAction={switchAccount} />",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AuthAccountSummary",
    "props": [
      {
        "description": "Authoritative signed-in email.",
        "name": "email",
        "required": true,
        "type": "string"
      },
      {
        "description": "Optional real avatar URL.",
        "name": "avatarSrc",
        "type": "string"
      },
      {
        "description": "Optional localized/text fallback; defaults to a user glyph.",
        "name": "avatarFallback",
        "type": "ReactNode"
      },
      {
        "description": "Localized visible switch-account action label.",
        "name": "actionLabel",
        "required": true,
        "type": "ReactNode"
      },
      {
        "description": "Consumer-owned account-switch navigation/action.",
        "name": "onAction",
        "required": true,
        "type": "() => void"
      },
      {
        "description": "Disable the action while the consumer cannot switch.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Optional structural class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "AuthIdentity — page identity heading and requesting-client context; use AuthAccountSummary inside the card for the signed-in user.",
      "Avatar — use Avatar directly for general profile surfaces; AuthAccountSummary owns only the compact hosted-auth row."
    ],
    "rules": [
      45
    ],
    "storyPath": "layout/auth-account-summary.tsx",
    "tagline": "Compact signed-in account row for hosted auth: avatar fallback, authoritative email and switch action.",
    "usage": [
      "Pass only the authoritative signed-in email; AuthAccountSummary never fetches or invents identity data.",
      "Pass localized actionLabel and the existing account-switch handler. The component owns no route, mutation or permission behavior.",
      "The package owns avatar fallback, long-email truncation, responsive action wrapping and keyboard focus. Retune only through --auth-account-summary-* tokens."
    ],
    "useCases": [
      "OAuth device consent and authorization cards where the user must confirm or switch the signed-in account.",
      "Hosted consent screens that need a compact identity confirmation row without a second profile card."
    ]
  },
  {
    "example": "import { AuthStack } from \"@godxjp/ui/layout\";\n\n<AuthStack><PasskeyAction /><CredentialsForm /></AuthStack>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AuthStack",
    "props": [
      {
        "description": "Auth sections in visual order.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Optional structural class override.",
        "name": "className",
        "type": "string"
      }
    ],
    "rules": [],
    "storyPath": "layout/auth-stack.tsx",
    "tagline": "Token-spaced vertical stack for direct sections inside a canonical auth card."
  },
  {
    "example": "import { OrgSwitcher } from \"@godxjp/ui/layout\";\nimport { Badge } from \"@godxjp/ui/data-display\";\n\n<OrgSwitcher\n  organizations={[\n    {\n      id: \"dxs\",\n      name: \"DXS Holdings\",\n      meta: t(\"org.role.owner\"),\n      badge: <Badge variant=\"secondary\">{t(\"org.plan.trial\")}</Badge>,\n      badgeLabel: t(\"org.plan.trial.sr\"),\n    },\n  ]}\n  value={organizationId}\n  onValueChange={setOrganizationId}\n  labels={labels}\n/>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "OrgSwitcher",
    "props": [
      {
        "description": "Consumer-provided organization choices.",
        "name": "organizations",
        "required": true,
        "type": "readonly OrgSwitcherOrganization[]"
      },
      {
        "description": "Selected organization id.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Selection callback; persistence remains consumer-owned.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Localized trigger, search, state, and retry copy.",
        "name": "labels",
        "required": true,
        "type": "OrgSwitcherLabels"
      },
      {
        "defaultValue": "false",
        "description": "Compact trigger presentation.",
        "name": "collapsed",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Disables the trigger.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Shows the loading state.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Consumer-supplied error state.",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "description": "Consumer-owned retry callback.",
        "name": "onRetry",
        "type": "() => void"
      },
      {
        "defaultValue": "\"auto\"",
        "description": "Which SURFACE the panel opens on. \"auto\" (default) is a desktop popover above --sheet-responsive-breakpoint-width (48rem/768px) and a focus-trapped bottom Sheet at/below it. \"dialog\" is a centred modal above that breakpoint and the SAME Sheet below it — reach for it once a row carries more than a name (a role, a plan, a member count, a create-organization action): a popover is anchored to its trigger, clipped by the viewport and sized by --org-switcher-menu-width, while a dialog has a real title, a scrolling body and room for a footer, and takes full attention, which is the right trade when switching organization re-scopes everything on screen. \"popover\" and \"sheet\" pin one surface at every width, for deterministic embedded surfaces and component tests. \"auto\" and \"dialog\" are the two RESPONSIVE pairs and share their mobile half, because a centred modal on a phone is a Sheet with worse ergonomics. All four resolve the breakpoint through the shared useSheetResponsiveMode() hook, so moving that one token moves the line for every overlay. Width of the dialog surface is --org-switcher-dialog-width (26rem), separate from --dialog-width-default so re-tuning the picker does not re-tune every dialog.",
        "name": "responsive",
        "type": "\"auto\" | \"popover\" | \"sheet\" | \"dialog\""
      },
      {
        "description": "Controlled open state.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Open-state callback.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      }
    ],
    "related": [
      "AppShell (navRail) — WHERE this control goes when switching organization is constant: the rail is the docked platform-scope column, and its own prop doc is the authority on which of the three columns owns which scope. Pass the collapsed trigger; the rail is 3.5rem.",
      "AppLauncher — the OTHER platform-scope control: which APP, not which ORGANIZATION. They compose (a launcher in the bar and a switcher in the rail is one coherent platform surface); neither replaces the other.",
      "Sidebar — APP scope, and therefore NOT where this goes. Its `brand` slot is the app's own lockup; putting the organization switcher there mixes two scopes in one header."
    ],
    "rules": [],
    "storyPath": "layout/org-switcher.tsx",
    "tagline": "Searchable organization switcher with canonical trigger geometry, desktop popover, and mobile sheet.",
    "usage": [
      "Provide only organizations the current user may select; the component does not authorize or fetch tenants.",
      "Keep persistence and navigation in `onValueChange`; use loading/error props for the real query state.",
      "DO put a plan/status affordance in `organization.badge` (a <Badge>) instead of stuffing it into `meta` — the badge is end-aligned in the expanded trigger and in the menu row, and hidden in the collapsed rail. ALWAYS pair it with a localized `organization.badgeLabel`: the trigger's accessible name comes from `labels.trigger`, so the badge is announced as an aria-describedby DESCRIPTION and the raw node is marked presentational (WCAG 1.1.1). `badgeLabel` also becomes a search keyword.",
      "DON'T wrap OrgSwitcher in your own media query to pick popover vs sheet — `responsive=\"auto\"` already reads the shared --sheet-responsive-breakpoint-width token.",
      "DON'T put it in the SIDEBAR — not as a row, not in `Sidebar`'s `brand` slot, not stacked under the product lockup. Which organization you are in is PLATFORM scope: it survives changing app, while everything else in the sidebar is this app's own sections. Its two legal homes are `AppShell`'s `navRail` (a permanent 3.5rem column — the Slack shape, when switching is frequent enough to deserve the width) and the topbar (a `TopbarItem`-height control, with `topbarSpan=\"full\"` so the bar outranks the section beneath it). Stacking it under the sidebar's own logo also puts two lockups of different heights in one header, which is what visibly skews the sidebar's top edge against the topbar's."
    ]
  },
  {
    "example": "import { AppLauncher, Topbar } from \"@godxjp/ui/layout\";\n\n<Topbar\n  start={brand}\n  end={\n    <AppLauncher\n      apps={[\n        { id: \"console\", name: t(\"app.console\"), href: \"/console\", icon: <BarChart3 />, current: true },\n        { id: \"billing\", name: t(\"app.billing\"), href: \"/billing\", icon: <Receipt /> },\n      ]}\n      groups={[{ label: t(\"app.more\"), apps: [{ id: \"support\", name: t(\"app.support\"), href: supportUrl, external: true }] }]}\n      linkComponent={inertiaSidebarLink(Link)}\n      labels={labels}\n    />\n  }\n/>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "AppLauncher",
    "props": [
      {
        "description": "Ungrouped app tiles, rendered first with no heading. Each is { id, name, href, icon?, current?, external? } and each tile is a REAL <a href> — pass only apps the viewer may open; there is no disabled tile.",
        "name": "apps",
        "required": true,
        "type": "readonly AppLauncherApp[]"
      },
      {
        "description": "Labelled sections rendered after `apps` — the \"more from …\" band. Each group is a named role=\"group\", not a heading, so the same markup is correct inside the popover and inside the Sheet.",
        "name": "groups",
        "type": "readonly AppLauncherGroup[]"
      },
      {
        "description": "Localized trigger name, panel title, empty/loading/retry copy, and the optional `externalHint` announced on an external tile (WCAG 3.2.5). `trigger` is a plain string, not a function of the current app: the nine-dot button shows no current value.",
        "name": "labels",
        "required": true,
        "type": "AppLauncherLabels"
      },
      {
        "defaultValue": "3",
        "description": "Grid column count, written inline to the `--app-launcher-columns` custom property. Omit it and `.ui-app-launcher-panel` keeps its own declaration of 3 — the default sits in the stylesheet, where a theme can reach it, rather than in a component token (the token NAME vocabulary has no word for a count).",
        "name": "columns",
        "type": "number"
      },
      {
        "description": "Framework router link — the SAME contract `Sidebar` and `NavList` take, so `inertiaSidebarLink(Link)` / `createSidebarLink(Link, \"to\")` is reused verbatim. The launcher still composes the tile; `external` apps bypass it and render a plain anchor.",
        "name": "linkComponent",
        "type": "SidebarLinkComponentProp"
      },
      {
        "defaultValue": "false",
        "description": "Shows the loading state; the trigger stays openable.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "description": "Consumer-supplied error state.",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "description": "Consumer-owned retry callback.",
        "name": "onRetry",
        "type": "() => void"
      },
      {
        "defaultValue": "\"auto\"",
        "description": "Presentation contract. \"auto\" resolves through the SHARED Sheet hook useSheetResponsiveMode(): desktop popover above --sheet-responsive-breakpoint-width (48rem/768px), focus-trapped bottom Sheet at/below it. \"fullscreen\" is the LAUNCHPAD — one full-viewport modal at every width, the page behind it blurred and the grid floating on that ground (macOS Launchpad / Windows Start), with the column count stepping 3·4·5·6 on the house container ladder. Reach for it in a PLATFORM start bar, where the launcher is the primary way to move between products; keep \"auto\" for a launcher inside one app's topbar, where the grid is a shortcut and the work behind it should stay visible.",
        "name": "responsive",
        "type": "\"auto\" | \"popover\" | \"sheet\" | \"fullscreen\""
      },
      {
        "defaultValue": "\"bar\"",
        "description": "The BOX the trigger takes — the same split `AppSettingPicker menu={false}` draws, for the same reason. `bar` (default) is a TopbarItem: a cell as tall as the bar, whose hover IS the bar's surface. `icon` is a square ghost Button, for chrome that is NOT a bar — a nav rail (GoDX Dock puts it there), a card header, a toolbar. A TopbarItem outside a bar has nothing to bleed to: it stretches to a container that never set a band height, and its squared corners and full-bleed hover read as a broken cell rather than a control. The panel, the grid, the labels and the responsive contract are identical in both.",
        "name": "appearance",
        "type": "\"bar\" | \"icon\""
      },
      {
        "description": "Which way the panel opens. DERIVED from `appearance` when unset — `bottom` in a bar (the panel drops below the trigger, the only direction that does not cover the bar itself), `right` otherwise, because a rail is vertical and its panel goes beside it. State it when the chrome can be RE-DOCKED: `appearance` says the trigger is NOT in a bar, and it cannot say which way is out — a rail pinned to the top edge is not a bar and still opens downward. Measured without it, an embedded bar trigger at (2,50) put its panel at (12,90), lying over the host application's sidebar. Not used by `responsive=\"fullscreen\"`, which has no side.",
        "name": "side",
        "type": "\"top\" | \"right\" | \"bottom\" | \"left\""
      },
      {
        "description": "Where the panel sits along the `side` edge — the cross-axis half of the same decision, and derived the same way: `end` in a bar (the Workspace shape, flush with the bar's end), `start` otherwise (aligned to the rail trigger's own start). State it alongside `side` when you state either.",
        "name": "align",
        "type": "\"start\" | \"center\" | \"end\""
      },
      {
        "description": "Controlled open state.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Open-state callback.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Secondary links under the grid — \"Request access\", \"All apps\" (the Workspace / Okta / My Apps row, gh#1172). Pass the links; the launcher draws the row (hairline above, small type, --app-launcher-footer-*) outside the scrolling grid, after the tiles in focus order, in every state including empty/loading/error. Not a tile: never fake it as a one-tile group.",
        "name": "footer",
        "type": "ReactNode"
      }
    ],
    "related": [
      "ServiceLauncherCard — the PAGE-SIZED launcher tile (status, hostname, plan, action, locked reason) for a service-catalogue page, where choosing is a considered act. AppLauncher's tile is bar-sized: mark + name, the whole tile one link, because changing app is a reflex. Neither is built out of the other; a grid of ServiceLauncherCards inside a popover is the wrong component.",
      "AppShell (navRail) — the SAME platform scope expressed as a docked column instead of a bar control. Pick ONE: the launcher for a platform with MANY apps where switching is occasional (Google Workspace), the rail for a single product where switching workspace is constant enough to deserve permanent screen width (Slack).",
      "OrgSwitcher — the other platform-scope control: which ORGANIZATION you are in, not which app. They compose; they do not replace each other.",
      "TopbarItem — the bar cell the trigger is built from; use it directly for a one-off bar control."
    ],
    "rules": [],
    "storyPath": "layout/app-launcher.tsx",
    "tagline": "Nine-dot topbar app grid — the platform-standard way to switch app (the Google Workspace shape).",
    "usage": [
      "DO drop it straight into a Topbar slot. It renders NO wrapper element, so the trigger is the slot's own flex child and `.ui-topbar-item { align-self: stretch }` reaches the bar's height.",
      "DON'T hand-roll the trigger as `Button variant=\"ghost\"` IN A BAR. A Button in a bar is a --control-height pill floating in a taller strip, with its own hover fill and its own focus ring — the exact regression corrected in 20.0.0. In a bar the trigger is a `TopbarItem`, and that is what `appearance=\"bar\"` (the default) renders. OUTSIDE a bar the square ghost Button is correct, and `appearance=\"icon\"` renders it for you — still hand-roll nothing.",
      "DO mark the app the viewer is inside with `current` — it becomes `aria-current=\"page\"`, which is what both the tint and the announcement key off.",
      "DO set `external` on a destination outside this SPA. It renders a plain anchor with target/rel and skips `linkComponent`, because a client-side router link to another origin routes nowhere.",
      "DON'T wrap it in your own media query to pick popover vs sheet — `responsive=\"auto\"` already reads the shared --sheet-responsive-breakpoint-width token.",
      "`className`, `id` and every `data-*` land on the TRIGGER, so an end-to-end selector can hold it without depending on the localized accessible name."
    ],
    "useCases": [
      "A platform with several apps (console, billing, chat, people) where the bar must offer all of them from every page.",
      "Replacing a hand-rolled dropdown of product links in the topbar end slot."
    ]
  },
  {
    "docPath": "data-display/permission-matrix.tsx",
    "example": "import { Card, CardContent, PermissionMatrix } from \"@godxjp/ui/data-display\";\nimport { grantKey } from \"@godxjp/ui/lib/permission-grid\";\n\nconst grants = new Set<string>(rolePermissions.map((rp) => grantKey(rp.roleId, rp.permissionId)));\n\n<Card>\n  <CardContent flush>\n    <PermissionMatrix\n      roles={roles}\n      permissions={permissions}\n      grants={grants}\n      onGrantChange={(roleId, permissionId, granted) => mutate({ roleId, permissionId, granted })}\n    />\n  </CardContent>\n</Card>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "PermissionMatrix",
    "props": [
      {
        "description": "Role COLUMNS in render order. `locked` keeps that role's cells read-only (with a localized lock badge) even in an editable matrix.",
        "name": "roles",
        "required": true,
        "type": "{ id: string; name: string; description?: string; locked?: boolean }[]"
      },
      {
        "description": "Permission ROWS in render order, with an optional category caption.",
        "name": "permissions",
        "required": true,
        "type": "{ id: string; name: string; description?: string; group?: string }[]"
      },
      {
        "description": "The grant relation: the grantKey(roleId, permissionId) Set from @godxjp/ui/lib/permission-grid (canonical, O(1)), or a plain pair array normalized through the same encoding.",
        "name": "grants",
        "required": true,
        "type": "ReadonlySet<string> | { roleId: string; permissionId: string }[]"
      },
      {
        "description": "Its PRESENCE makes the matrix editable (real Checkbox cells, Space toggles). Omitted, the matrix is the canonical read-only ✓/— grid.",
        "name": "onGrantChange",
        "type": "(roleId: string, permissionId: string, granted: boolean) => void"
      },
      {
        "defaultValue": "false",
        "description": "Force the read-only grid even when onGrantChange is present (viewer permission).",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Two role ids compared side by side: their columns tint, and rows where they disagree carry a localized difference badge.",
        "name": "compare",
        "type": "[string, string] | null"
      },
      {
        "defaultValue": "false",
        "description": "With `compare`, keep only the rows the two roles disagree on (差分のみ).",
        "name": "diffOnly",
        "type": "boolean"
      },
      {
        "description": "`true` renders the built-in localized surface; a node replaces it; onRetry adds Retry to the built-in error only.",
        "name": "loading / denied / error / empty / onRetry",
        "type": "boolean | ReactNode / handler"
      },
      {
        "description": "Accessible table name (localized default).",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id on the grid root. Worth setting on a permissions page that also renders a summary or a legend elsewhere: it is the anchor those can point at, and the stable handle for an E2E selector that must not depend on the localized `label`.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "lib/permission-grid — the pure grant/diff data helpers the matrix (and any custom RBAC UI) shares.",
      "DataTable — general tabular data with sorting/selection/pagination; PermissionMatrix is the fixed role-grid specialization with a sticky FIRST column (which DataTable cannot pin).",
      "ServiceRolePanel — the master-detail roles surface a matrix typically renders inside."
    ],
    "rules": [
      24
    ],
    "storyPath": "data-display/permission-matrix.tsx",
    "tagline": "Domain data is 100% consumer-supplied.",
    "usage": [
      "DO import it — it is a real export from @godxjp/ui/data-display (the lesson: a docs page is not importable). Never hand-compose the sticky-column grid per app.",
      "DO keep grants in the lib/permission-grid grantKey Set form when you already hold role:permission tuples — the pair-array form exists for convenience and is normalized through the same encoding.",
      "DO put it in a Card with CardContent flush: <Card><CardContent flush><PermissionMatrix …/></CardContent></Card>. Below its natural measure the grid scrolls horizontally INSIDE its own container (390px keeps the sticky permission column).",
      "DO NOT encode platform roles/permissions in the library — roles, permissions and grants are consumer data by contract.",
      "DO NOT pass compare pickers/toggles into the matrix — compose Select + Switch beside it and drive `compare`/`diffOnly` (see the showcase)."
    ],
    "useCases": [
      "RBAC role tab on a service detail screen: read-only matrix + role compare.",
      "Org role editor: editable matrix (onGrantChange) with the system role locked.",
      "Permission-denied / failed read states without hand-rolling: denied / error / onRetry."
    ]
  },
  {
    "docPath": "data-entry/branch-scope-picker.tsx",
    "example": "import { BranchScopePicker, FormField } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"適用範囲\" required error={errors.scope}>\n  <BranchScopePicker\n    branches={branches}\n    value={scope}\n    onValueChange={setScope}\n    error={scope.mode === \"selected\" && !scope.branchIds?.length ? \"1件以上選択してください\" : undefined}\n  />\n</FormField>",
    "group": "data-entry",
    "importPath": "@godxjp/ui/data-entry",
    "name": "BranchScopePicker",
    "props": [
      {
        "description": "Giá trị có kiểm soát: phạm vi đang chọn.",
        "name": "value",
        "type": "BranchScopeValueProp"
      },
      {
        "description": "Giá trị khởi tạo khi không kiểm soát.",
        "name": "defaultValue",
        "type": "BranchScopeValueProp"
      },
      {
        "description": "Phát khi phạm vi đổi.",
        "name": "onValueChange",
        "type": "(value: BranchScopeValueProp) => void"
      },
      {
        "description": "The selectable branches — consumer domain data.",
        "name": "branches",
        "required": true,
        "type": "{ id: string; name: string; description?: string; disabled?: boolean }[]"
      },
      {
        "description": "The controlled triad; default { mode: \"all\" }. Mode flips PRESERVE branchIds so switching back to all never destroys a curated subset.",
        "name": "value / defaultValue / onValueChange",
        "type": "{ mode: \"all\" | \"selected\"; branchIds?: string[] }"
      },
      {
        "description": "Field VALIDATION message — rendered under the control and wired via aria-invalid/aria-errormessage on the radiogroup. Collection read failures are `listError`, not this.",
        "name": "error",
        "type": "ReactNode"
      },
      {
        "description": "`true` renders the built-in localized message.",
        "name": "listError / denied / loading / empty",
        "type": "boolean | ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "Locked view: the current scope as a static summary (mode + branch badges).",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Controls visible but inert.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "true",
        "description": "Built-in branch search above the checkbox list.",
        "name": "searchable",
        "type": "boolean"
      },
      {
        "description": "antd `notFoundContent` — shown when the branch SEARCH matches nothing (default: localized `dataEntry.branchScope.noMatches`). An empty `branches` list is `empty`'s job, not this.",
        "name": "notFoundContent",
        "type": "ReactNode"
      },
      {
        "description": "Override the localized radio labels (e.g. domain wording like 全店舗).",
        "name": "allLabel / selectedLabel",
        "type": "ReactNode"
      },
      {
        "description": "Native form name, forwarded to the MODE radio group — the all / selected choice is what submits under it. The checked branch ids are not native fields; they live in the single `{ mode, branchIds }` value and are yours to serialise.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "DOM id on the picker root, and the SEED for the ids beneath it — the validation message is `${id}-error`, which is what `aria-errormessage` points at. Left out, a `useId`-based id is generated, so the association still holds; set it when a server-rendered page needs those ids to be stable.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "CheckboxGroup / RadioGroup — the primitives underneath; use them directly for non-scope choices.",
      "Transfer — large two-list assignment; BranchScopePicker is the compact all-vs-subset scope idiom.",
      "TreeSelect — hierarchical selection when branches nest."
    ],
    "rules": [
      24
    ],
    "storyPath": "data-entry/branch-scope-picker.tsx",
    "tagline": "Canonical scope control: all branches vs an explicit subset.",
    "usage": [
      "DO treat scope as ONE form field: the single { mode, branchIds } value goes through FormField like any other control.",
      "DO import it from @godxjp/ui/data-entry — never compose an ad-hoc radio+checkbox scope block per screen.",
      "DO use `error` ONLY for validation (e.g. mode=selected with zero branches checked); a failed branch fetch is `listError`, a 403 is `denied`.",
      "DO NOT preselect branches for the user — default is { mode: \"all\" }; an explicit subset is a user decision."
    ],
    "useCases": [
      "Role/permission assignment scoped to branches (service role forms).",
      "Report or notification audience: whole org vs selected branches.",
      "Read-only scope display on a detail screen (readOnly)."
    ]
  },
  {
    "docPath": "layout/service-role-panel.tsx",
    "example": "import { PermissionMatrix } from \"@godxjp/ui/data-display\";\nimport { ServiceRolePanel } from \"@godxjp/ui/layout\";\n\n<ServiceRolePanel\n  roles={roles}\n  value={selectedRoleId}\n  onValueChange={setSelectedRoleId}\n  onDeleteRole={(roleId) => deleteRole.mutate(roleId)}\n>\n  {(role) =>\n    role && <PermissionMatrix roles={[role]} permissions={permissions} grants={grants} readOnly={role.locked} />\n  }\n</ServiceRolePanel>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "ServiceRolePanel",
    "props": [
      {
        "description": "Giá trị có kiểm soát: id vai trò đang chọn.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Id vai trò khởi tạo khi không kiểm soát.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Phát khi vai trò đổi.",
        "name": "onValueChange",
        "type": "(roleId: string) => void"
      },
      {
        "description": "The role collection — consumer domain data. `locked` = system role: lock badge, never deletable. memberCount is CLDR-pluralized.",
        "name": "roles",
        "required": true,
        "type": "{ id: string; name: string; description?: string; memberCount?: number; locked?: boolean }[]"
      },
      {
        "description": "Controlled selection triad; defaults to the first role.",
        "name": "value / defaultValue / onValueChange",
        "type": "string / (roleId: string) => void"
      },
      {
        "description": "The detail surface. A render function receives the selected role — typically renders a PermissionMatrix + role metadata.",
        "name": "children",
        "type": "ReactNode | (role) => ReactNode"
      },
      {
        "description": "Its PRESENCE arms a per-role delete affordance behind the built-in destructive AlertDialog; it fires only after the user confirms. Locked roles never offer it.",
        "name": "onDeleteRole",
        "type": "(roleId: string) => void"
      },
      {
        "defaultValue": "false",
        "description": "Hide every mutating affordance.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "The #216 lifecycle vocabulary (precedence loading → denied → error → empty), same semantics as DataTable/PermissionMatrix.",
        "name": "loading / denied / error / empty / onRetry",
        "type": "boolean | ReactNode / handler"
      },
      {
        "description": "Forwarded to MasterDetail (localized region labels by default). Never re-derive tracks or breakpoints in the app.",
        "name": "railWidth / masterViewport / collapseBelow / masterLabel / detailLabel",
        "type": "MasterDetail geometry + region labels"
      },
      {
        "description": "DOM id on the panel root — the two-region MasterDetail wrapper, not the rail or the detail. It is the handle for a deep link onto the roles panel of a settings page, and for an E2E selector that must survive `masterLabel` being localized.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "MasterDetail — the geometry underneath; use it directly for non-role collections.",
      "PermissionMatrix — the canonical detail body for a selected role.",
      "AlertDialog — the confirm primitive the panel embeds; use directly for other destructive flows."
    ],
    "rules": [
      24,
      40
    ],
    "storyPath": "layout/service-role-panel.tsx",
    "tagline": "Geometry (1440/1024 two-track, 390 stacked) is MasterDetail's tokens.",
    "usage": [
      "DO compose the detail from real primitives — the panel deliberately owns NO detail layout; PermissionMatrix + Descriptions is the typical body.",
      "DO rely on the built-in AlertDialog for deletion — never wire a bare onClick delete; the confirm gate is the contract.",
      "DO pass masterViewport=\"compact\" for long role collections so the rail scrolls inside its region after stacking.",
      "DO NOT nest interactive controls in a role row — the select button and the delete button are SIBLINGS by design; keep any custom row content non-interactive."
    ],
    "useCases": [
      "Service detail → roles tab: role list rail + permission matrix detail.",
      "Org role management screen with locked system roles and confirmed deletion.",
      "Read-only role browser for auditors (readOnly)."
    ]
  },
  {
    "docPath": "data-display/chat-bubble.tsx",
    "example": "import { Avatar, AvatarFallback } from \"@godxjp/ui/data-display\";\nimport { ChatBubble } from \"@godxjp/chat\";\nimport { formatAppTime } from \"@godxjp/ui/datetime\";\n\n<ChatBubble\n  placement=\"start\"\n  variant=\"filled\"\n  avatar={<Avatar aria-hidden=\"true\"><AvatarFallback>AI</AvatarFallback></Avatar>}\n  header={t(\"chat.bubble.assistant\")}\n  footer={formatAppTime(message.sentAt)}\n  typing={{ step: 2, interval: 24 }}\n>\n  {message.text}\n</ChatBubble>\n\n// The reply has been requested and has not arrived yet.\n<ChatBubble placement=\"start\" header={t(\"chat.bubble.assistant\")} loading />\n\n// A failed send: colour AND a localized sr-only word, never colour alone.\n<ChatBubble placement=\"end\" tone=\"destructive\" header={t(\"chat.bubble.you\")}>\n  {draft}\n</ChatBubble>",
    "group": "data-display",
    "importPath": "@godxjp/chat",
    "name": "ChatBubble",
    "props": [
      {
        "description": "The message. Pass a plain STRING to make `typing` animatable — a ReactNode has no character count to reveal, so it always renders whole.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "defaultValue": "\"start\"",
        "description": "Side of the conversation, on the LOGICAL inline axis: \"start\" is the other party, \"end\" is the reader's own message. It flips automatically under dir=\"rtl\" — never branch on locale.",
        "name": "placement",
        "type": "\"start\" | \"end\""
      },
      {
        "defaultValue": "\"filled\"",
        "description": "Structural treatment of the body. Ant Design X's `shadow` is absent on purpose: this is a 1px-border system with no drop shadows. Status colour is `tone`, never `variant`.",
        "name": "variant",
        "type": "\"filled\" | \"borderless\" | \"outlined\""
      },
      {
        "description": "The author's mark — a real <Avatar>, never a styled div. Mark it aria-hidden when `header` already names the author.",
        "name": "avatar",
        "type": "ReactNode"
      },
      {
        "description": "Line above the body naming the turn. When present it becomes the bubble's ACCESSIBLE NAME (aria-labelledby), so keep it text.",
        "name": "header",
        "type": "ReactNode"
      },
      {
        "description": "Line below the body — a timestamp (format it with formatAppTime from @godxjp/ui/datetime), per-message actions, a token count.",
        "name": "footer",
        "type": "ReactNode"
      },
      {
        "defaultValue": "false",
        "description": "The reply was requested and has not arrived: renders Skeleton bars (aria-hidden, they are decorative) plus a localized sr-only line, and sets aria-busy on the article.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Stream the text in. `true` = 1 character every 50ms; the object form retunes it. While streaming the half-typed text is aria-hidden and the article is aria-busy, so the log announces the finished message ONCE. Under prefers-reduced-motion: reduce the full text renders immediately and no timer starts.",
        "name": "typing",
        "type": "boolean | { step?: number; interval?: number }"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Type step and inner inset. Never \"default\".",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "defaultValue": "\"default\"",
        "description": "Status intent for a message that is not ordinary conversation (a failed send, a rate-limit warning). Washes the role colour like Alert AND renders a localized sr-only tone word — never colour alone.",
        "name": "tone",
        "type": "\"default\" | \"info\" | \"success\" | \"warning\" | \"destructive\""
      },
      {
        "description": "DOM id forwarded to the bubble's `<article>`. Supply one when something outside the bubble has to point at it (an `aria-describedby` from a retry button, a deep link to a single message); otherwise leave it off and let React generate the ids the header/label wiring needs.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "ChatBubbleList — the feed. Use it whenever there is more than one message: it owns stick-to-bottom, the jump-to-latest affordance and the single aria-live region.",
      "ListRow — a single-line entity row for short lists (sessions, tokens). Use ListRow for records, ChatBubble for conversation turns.",
      "Alert — a page-level status banner. A toned ChatBubble is a status INSIDE a conversation; an Alert is a status about the screen.",
      "Prose — use it inside `children` when the assistant returns rendered Markdown; ChatBubble owns the container, Prose owns the typography of the body.",
      "Skeleton — what `loading` renders. Do not compose it yourself around a bubble; the prop also sets aria-busy and the localized status line."
    ],
    "rules": [
      6,
      23,
      44,
      45
    ],
    "storyPath": "data-display/chat-bubble.tsx",
    "tagline": "One message in a conversation: an author mark, a header naming the turn, the body, and a footer. Renders as an <article> inside ChatBubbleList's log; `loading` shows Skeleton and `typing` streams a string in, dropping the animation entirely under prefers-reduced-motion.",
    "usage": [
      "DO let ChatBubbleList own the feed. A lone ChatBubble is for a single quoted message; a conversation is `items` + `roles` on the list, which is where stick-to-bottom and the one aria-live region live.",
      "DO pass a real <Avatar> to `avatar` and a plain string to `header`. The header is the accessible name of the bubble, so a decorative avatar next to it should be aria-hidden.",
      "DO pass `typing` on the streaming message only, and only when its body is a string. Do not re-mount the bubble on every token — change `children` and let the component reveal it.",
      "DO format timestamps for `footer` with formatAppTime / formatAppDate from @godxjp/ui/datetime (Intl.DateTimeFormat + the AppProvider timezone). Never hand-build a date string.",
      "DO NOT put an aria-live region inside a bubble. The feed owns exactly one; one per bubble floods a screen reader on every token.",
      "DO NOT reach for `variant` to signal an error — that is `tone`. `variant` is structural (filled/borderless/outlined) and has no status values.",
      "DO NOT hand-roll the bubble as a rounded div with a background: placement, the RTL flip, the max measure and the tone wash are all tokens on this component (--chat-bubble-*)."
    ],
    "useCases": [
      "An AI assistant panel — assistant turns on the start side, the operator's own prompts on the end side, with the in-flight reply streaming via `typing`.",
      "A support-inbox thread where an agent reads the customer's history: the same feed renders both parties and marks a failed send with tone=\"destructive\".",
      "A tool/agent transcript where a system note (tone=\"info\", variant=\"outlined\") sits between conversational turns.",
      "A quoted single message inside a Card — a reported message in a moderation screen, rendered as one ChatBubble with a footer carrying the report time."
    ]
  },
  {
    "docPath": "data-display/chat-bubble.tsx",
    "example": "import { ChatBubbleList, type ChatMessageProp } from \"@godxjp/chat\";\n\nconst messages: ChatMessageProp[] = [\n  { id: \"m1\", role: \"assistant\", content: \"How can I help?\" },\n  { id: \"m2\", role: \"user\", content: \"Summarise yesterday's invoices.\" },\n  { id: \"m3\", role: \"assistant\", content: reply, typing: true },\n];\n\n<ChatBubbleList\n  label={t(\"chat.list.label\")}\n  items={messages}\n  roles={{\n    assistant: { placement: \"start\", variant: \"filled\", avatar: assistantAvatar },\n    user: { placement: \"end\", variant: \"outlined\" },\n    system: { placement: \"start\", variant: \"borderless\", tone: \"info\", size: \"sm\" },\n  }}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/chat",
    "name": "ChatBubbleList",
    "props": [
      {
        "description": "`{ id, role?, content?, ...ChatBubbleProp }` in conversation order, oldest first. `id` is the React key AND the rendered <article>'s DOM id.",
        "name": "items",
        "required": true,
        "type": "ChatMessageProp[]"
      },
      {
        "description": "Per-role bubble defaults, merged UNDER each message's own props: `{ assistant: { placement: \"start\" }, user: { placement: \"end\", variant: \"outlined\" } }`. Written once instead of on every message.",
        "name": "roles",
        "type": "Record<string, Partial<ChatBubbleProp>>"
      },
      {
        "defaultValue": "true",
        "description": "Keep the newest message in view WHILE the reader is at the bottom. It is not \"scroll to the bottom when content arrives\": scrolling up revokes the pin until the reader asks for it back, because yanking them down mid-sentence is a change of context they did not request (WCAG 3.2.5).",
        "name": "autoScroll",
        "type": "boolean"
      },
      {
        "description": "Accessible name of the feed; lands on aria-label. Defaults to the localized t(\"chat.list.label\").",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id forwarded to the feed's scroll container (`role=\"log\"`). Supply one when a control outside the feed must reference it — an `aria-controls` on a \"jump to latest\" button of your own, or a skip link that moves focus into the transcript.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "ChatBubble — one message; this list renders them and supplies per-role defaults.",
      "ScrollArea — the scrolling primitive underneath. Use ScrollArea anchor=\"bottom\" directly for a non-conversational live stream (an audit log); use ChatBubbleList when the rows are conversation turns.",
      "Timeline — an ordered event rail with no scale and no live region. Use Timeline for a record's history, ChatBubbleList for a dialogue.",
      "DataTable — for many rows that need sorting, filtering and pagination. A conversation is neither sorted nor paged."
    ],
    "rules": [
      6,
      23,
      44,
      45
    ],
    "storyPath": "data-display/chat-bubble.tsx",
    "tagline": "The message feed. It owns STICK-TO-BOTTOM (auto-scroll only while the reader is already at the bottom; the moment they scroll up the pin is revoked and a focusable jump-to-latest button appears), one aria-live=\"polite\" role=\"log\" region for the whole conversation, and per-role bubble defaults.",
    "usage": [
      "DO drive the feed from `items` and shape it with `roles`. Mapping ChatBubbles yourself inside a ScrollArea loses stick-to-bottom, the jump affordance and the single live region — the three reasons this component exists.",
      "DO give the list a DEFINITE height — `className=\"h-96\"`, or a flex/grid parent that hands it a track. It scrolls inside itself; measured at 1280px, a list with no height grew to its content (1,952px) inside a 256px Card, scrollHeight === clientHeight, and stick-to-bottom had nothing to anchor.",
      "DO keep `id` stable per message. It is the React key, so a regenerated id re-mounts the bubble and restarts any typing animation.",
      "DO leave autoScroll on for a live conversation and turn it off for an archived transcript, where landing on the newest message is not what the reader wants.",
      "DO NOT add your own aria-live region inside the feed, and do not put one on a bubble. This list already declares role=\"log\" with aria-live=\"polite\"; a second region double-announces every message.",
      "DO NOT scroll the viewport yourself on every append. That is the defect stick-to-bottom exists to prevent; the reader's scroll position is the only thing that grants the pin."
    ],
    "useCases": [
      "The transcript pane of an AI assistant screen, above a ChatComposer, streaming the assistant's reply into the last bubble.",
      "A support conversation in an admin detail page, where an operator scrolls up to re-read an earlier message while new ones keep arriving.",
      "An agent/tool run log rendered as a conversation, with system notes as toned bubbles between turns.",
      "An archived thread opened read-only from a report (autoScroll={false}), so the reader lands where the citation is rather than at the end."
    ]
  },
  {
    "docPath": "data-entry/chat-composer.tsx",
    "example": "import { ChatComposer } from \"@godxjp/chat\";\n\nconst [draft, setDraft] = useState(\"\");\nconst [streaming, setStreaming] = useState(false);\n\n<ChatComposer\n  value={draft}\n  onValueChange={setDraft}\n  onSubmit={(text) => { send(text); setDraft(\"\"); }}\n  loading={streaming}\n  onCancel={() => setStreaming(false)}\n  placeholder={t(\"chat.placeholder\")}\n  footer={t(\"dataEntry.chatComposer.hintEnter\")}\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/chat",
    "name": "ChatComposer",
    "props": [
      {
        "description": "Controlled draft text. Pair with onValueChange or the box freezes.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial draft text.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Draft-text change handler; fires on every keystroke, including during an IME conversion.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "description": "Send the draft. Never fires for empty or whitespace-only text (unless allowEmptySubmit, which passes \"\"), nor while loading/disabled/readOnly.",
        "name": "onSubmit",
        "type": "(value: string) => void"
      },
      {
        "description": "Stop the in-flight response. Only reachable while loading.",
        "name": "onCancel",
        "type": "() => void"
      },
      {
        "defaultValue": "false",
        "description": "A response is streaming: the trailing action BECOMES cancel. Send and cancel never render together.",
        "name": "loading",
        "type": "boolean"
      },
      {
        "defaultValue": "\"enter\"",
        "description": "\"enter\": Enter sends, Shift+Enter breaks the line. \"shiftEnter\": the inverse, for long deliberate drafts. \"modEnter\" (library extension; antd X Sender has only the first two): ⌘+Enter on Apple platforms, Ctrl+Enter elsewhere sends, while Enter and Shift+Enter both break the line — the record-comment convention (GitHub, Jira, Linear).",
        "name": "submitType",
        "type": "\"enter\" | \"shiftEnter\" | \"modEnter\""
      },
      {
        "defaultValue": "false",
        "description": "Let an empty or whitespace-only draft be sent when header/footer carry payload of their own (a status change on a record). The send button stays enabled and the button and keyboard submit call onSubmit(\"\"). Still blocked while loading/disabled/readOnly.",
        "name": "allowEmptySubmit",
        "type": "boolean"
      },
      {
        "description": "Empty-state text of the draft box — pass it through t() at the call site.",
        "name": "placeholder",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Disable the composer and every action.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "defaultValue": "false",
        "description": "Show the draft without allowing an edit; still focusable.",
        "name": "readOnly",
        "type": "boolean"
      },
      {
        "description": "Slot ABOVE the draft row — attachments, a reply-to banner, a model picker.",
        "name": "header",
        "type": "React.ReactNode"
      },
      {
        "description": "Slot at the inline START of the draft row — an attach Button, an Avatar.",
        "name": "prefix",
        "type": "React.ReactNode"
      },
      {
        "description": "Slot BELOW the draft row — a hint line, a token counter, OR (Ant Design X Sender NodeRender) a function that receives the real SubmitButton/CancelButton components, pre-wired, so they can be rendered in the footer instead of beside the draft box. Pair with actions={false} to hide the inline pair.",
        "name": "footer",
        "type": "React.ReactNode | ((info: { components: ChatComposerActionComponents }) => React.ReactNode)"
      },
      {
        "description": "Extra trailing actions, rendered BEFORE the send/cancel action — or false (Ant Design X Sender suffix semantics) to hide the ENTIRE inline trailing-action cell, built-in send/cancel button included, once footer has taken it over. Omitted renders exactly as before.",
        "name": "actions",
        "type": "React.ReactNode | false"
      },
      {
        "defaultValue": "\"md\"",
        "description": "Height tier on the shared --control-height ladder; moves both auto-grow bounds together.",
        "name": "size",
        "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "Hard ceiling on the draft length, forwarded to the textarea.",
        "name": "maxLength",
        "type": "number"
      },
      {
        "description": "Validation state the frame paints. error also reports aria-invalid (colour alone fails WCAG 1.4.1).",
        "name": "status",
        "type": "\"error\" | \"warning\""
      },
      {
        "description": "Accessible name override for the send action (localized default otherwise).",
        "name": "submitLabel",
        "type": "string"
      },
      {
        "description": "Accessible name override for the cancel action (localized default otherwise).",
        "name": "cancelLabel",
        "type": "string"
      },
      {
        "description": "Keydown on the draft box — how ChatSuggestion drives its list. A handler that calls preventDefault() owns the key, and the composer will not treat it as a send.",
        "name": "onKeyDown",
        "type": "React.KeyboardEventHandler<HTMLTextAreaElement>"
      },
      {
        "description": "Native form name, forwarded to the textarea.",
        "name": "name",
        "type": "string"
      },
      {
        "description": "DOM id of the textarea (the semantic focus target FormField labels).",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "Textarea — the primitive underneath. Use it directly for an ordinary multi-line form field with no send action.",
      "ChatSuggestion — wraps ChatComposer to add trigger-character (/) autocomplete.",
      "ChatBubbleList — the feed the composer sends into.",
      "SearchInput — a single-line query field; a composer is multi-line and holds a draft."
    ],
    "rules": [
      2,
      6,
      43,
      45
    ],
    "storyPath": "data-entry/chat-composer.tsx",
    "tagline": "The message input of a conversation (Ant Design X Sender): an auto-growing Textarea plus exactly ONE trailing action — send, or cancel while a response streams. Enter / Shift+Enter / ⌘-or-Ctrl+Enter is configurable and never fires during an IME conversion.",
    "usage": [
      "DO pair a controlled `value` with `onValueChange` — a controlled value with no synchronised handler is the classic frozen-input bug, and it freezes the whole conversation.",
      "DO wrap it in FormField when the composer is a labelled field; the label/helper/error contract lands on the <textarea>, which is the semantic focus target (ref goes there too).",
      "DON'T hand-roll Enter-to-send. An IME conversion (ja/vi) fires a real Enter to ACCEPT a candidate; ChatComposer already guards compositionstart/compositionend, and skipping that guard makes Japanese and Vietnamese input impossible.",
      "DON'T render your own stop button beside the send button — set `loading` and the trailing action becomes cancel. Exactly one trailing action exists at a time (the picker trailing-action discipline).",
      "DO put a hint in `footer` (t('dataEntry.chatComposer.hintEnter') / 'hintShiftEnter') when you flip `submitType` — the keystroke contract is invisible otherwise. For submitType=\"modEnter\" use t('dataEntry.chatComposer.hintModEnter', { modifier: isApplePlatform() ? '⌘' : 'Ctrl' }) with isApplePlatform from @godxjp/ui/lib/utils — the same platform test the composer uses to pick metaKey vs ctrlKey.",
      "DO set `allowEmptySubmit` (not a hidden fake draft) when the composer also submits field changes from `header`/`footer`; your onSubmit receives \"\" and decides whether anything changed.",
      "DON'T size it with a className height: the box grows between --chat-composer-min-height and --chat-composer-max-height, both derived from the --control-height tier. Use `size`, or re-tune the two tokens in your theme.",
      "DO move the toolbar BELOW a full-width draft box with `actions={false}` + a function `footer` — the function receives `{ components: { SubmitButton, CancelButton } }`, both already wired to onSubmit/onCancel/disabled/the loading swap, so nothing about the send contract is reimplemented at the call site. Put the attach control and a keyboard hint on the START side of the footer row and `<SubmitButton />` on the END side (a `Flex justify=\"between\"`), exactly like the record-comment box case. Leaving `actions` unset keeps today's inline row — this is opt-in, not a breaking change."
    ],
    "useCases": [
      "The message box of an AI assistant or support chat, under a ChatBubbleList feed.",
      "A comment composer on a record detail screen (prefix = attach Button, footer = character counter).",
      "A record-detail comment box with a full-width textarea and a toolbar row BELOW it — attach on the start side, a keyboard hint in the middle, clear/send on the end side: `actions={false}` + `footer={({ components }) => <Flex justify=\"between\">…<components.SubmitButton /></Flex>}`.",
      "A long-form reply box where Enter must break the line: submitType=\"shiftEnter\".",
      "A comment bar on an issue/record where Enter breaks the line and ⌘/Ctrl+Enter posts, and a status change may be posted without text: submitType=\"modEnter\" + allowEmptySubmit.",
      "A streaming answer the user can stop: loading + onCancel."
    ]
  },
  {
    "docPath": "data-entry/chat-composer.tsx",
    "example": "import { ChatComposer, ChatSuggestion } from \"@godxjp/chat\";\n\nconst [draft, setDraft] = useState(\"\");\n\n<ChatSuggestion\n  items={[\n    { value: \"summarize\", label: \"要約する\", description: \"Summarize the thread\" },\n    { value: \"translate\", label: \"翻訳する\" },\n  ]}\n  onValueChange={(value) => setDraft(\"/\" + value + \" \")}\n>\n  {({ onTrigger, onKeyDown }) => (\n    <ChatComposer\n      value={draft}\n      onValueChange={(next) => { setDraft(next); onTrigger(next); }}\n      onKeyDown={onKeyDown}\n      onSubmit={(text) => send(text)}\n    />\n  )}\n</ChatSuggestion>",
    "group": "data-entry",
    "importPath": "@godxjp/chat",
    "name": "ChatSuggestion",
    "props": [
      {
        "description": "The rows to offer: { value, label?, description?, icon?, disabled?, children? }. One level of children is honoured — picking a parent drills into it instead of emitting.",
        "name": "items",
        "required": true,
        "type": "ChatSuggestionItemProp[]"
      },
      {
        "description": "Fires with the picked row's value. The CALLER owns what that does to the draft text — the component never rewrites the textarea behind your back.",
        "name": "onValueChange",
        "type": "(value: string) => void"
      },
      {
        "defaultValue": "\"/\"",
        "description": "The character(s) that open the list when typed at a boundary — the start, whitespace, or any non-ASCII character, so `/` opens right after Japanese text (gh#1127); an ASCII letter still blocks a ONE-character trigger, so `https://x` opens nothing. A multi-character trigger (`[[`) opens anywhere, `abc[[` included (gh#1129). After Escape the list stays shut until a new trigger is typed. Use \"@\" for mentions, \"[[\" for a link picker.",
        "name": "triggerCharacter",
        "type": "string"
      },
      {
        "defaultValue": "false",
        "description": "Spaces may appear inside the query (a `[[` link picker searching \"Meeting notes\"); a line break still ends it (gh#1127).",
        "name": "allowSpaces",
        "type": "boolean"
      },
      {
        "description": "Typing this inside the query finishes the token and closes the list, e.g. `]]` (gh#1127).",
        "name": "terminator",
        "type": "string"
      },
      {
        "defaultValue": "true",
        "description": "`false` when the host answers the query itself (async, via `onQueryChange`) and `items` already holds the matches — they are shown as given, not filtered again (gh#1127).",
        "name": "shouldFilter",
        "type": "boolean"
      },
      {
        "description": "The query typed after the trigger, on every change while the list is open — the seam for server-side search (gh#1127).",
        "name": "onQueryChange",
        "type": "(query: string) => void"
      },
      {
        "description": "Controlled open state of the list.",
        "name": "open",
        "type": "boolean"
      },
      {
        "description": "Uncontrolled initial open state.",
        "name": "defaultOpen",
        "type": "boolean"
      },
      {
        "description": "Open-state change handler.",
        "name": "onOpenChange",
        "type": "(open: boolean) => void"
      },
      {
        "description": "Render prop wrapping the composer. Call onTrigger from the composer's onValueChange and forward onKeyDown to its onKeyDown.",
        "name": "children",
        "required": true,
        "type": "(props: { onTrigger: (value?: string | false) => void; onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> }) => React.ReactNode"
      },
      {
        "description": "Shown when the query matches nothing (localized default otherwise).",
        "name": "emptyMessage",
        "type": "string"
      },
      {
        "description": "Accessible name of the listbox (localized default otherwise).",
        "name": "listLabel",
        "type": "string"
      },
      {
        "description": "DOM id of the anchor wrapping the composer.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "ChatComposer — the control it wraps; use it alone when there is nothing to suggest.",
      "Command / CommandPalette — a full-screen command surface opened by a shortcut, not by a character in a draft.",
      "Select (showSearch) — the searchable single-select; a suggestion list edits free text, it does not hold a value."
    ],
    "rules": [
      2,
      3,
      6
    ],
    "storyPath": "data-entry/chat-composer.tsx",
    "tagline": "In @godxjp/chat since v32, not @godxjp/ui: add @godxjp/chat as a dependency at the same version as @godxjp/ui. Trigger-character autocomplete over a ChatComposer (Ant Design X Suggestion): type / at a word boundary and a Command list opens against the composer, driven from the textarea without ever taking focus off it.",
    "usage": [
      "DO wire BOTH halves of the render prop: `onTrigger` from the composer's onValueChange and `onKeyDown` from its onKeyDown. With only one wired the list either never opens or cannot be driven.",
      "DO decide yourself what a pick does to the draft — onValueChange hands you the value; the typed /query is still in the box, so replace it or append to it as your screen needs.",
      "DON'T hand-roll a listbox next to a textarea. This composes the real Command (cmdk) inside a Popover, which already ships the listbox/option roles, active-row bookkeeping and scroll-into-view.",
      "DO rely on Escape: it closes the list, returns focus to the textarea and leaves the typed text intact. It also stops propagating, so a composer inside a Dialog does not close the Dialog too.",
      "DON'T expect it to filter server-side — filtering is a plain substring match over label/value/description. For a remote list, filter `items` yourself as the query changes."
    ],
    "useCases": [
      "Slash commands over an assistant composer (/summarize, /translate, /explain).",
      "Mention picker in a comment composer (triggerCharacter=\"@\").",
      "Prompt-template inserter grouped one level deep (a category row that drills into its templates)."
    ]
  },
  {
    "docPath": "navigation/conversations.tsx",
    "example": "import { Conversations } from \"@godxjp/chat\";\n\nconst [active, setActive] = useState(\"c1\");\n\n<Conversations\n  activeKey={active}\n  onActiveChange={setActive}\n  groupable={{ collapsible: true }}\n  creation={{ onClick: () => startNewChat() }}\n  items={[\n    { key: \"c1\", label: \"請求書の下書き\", group: \"today\" },\n    { key: \"c2\", label: \"経費精算の規則\", group: \"today\" },\n    { key: \"c3\", label: \"出張手当の確認\", group: \"earlier\" },\n  ]}\n  menu={{\n    items: [\n      { key: \"rename\", label: \"名前を変更\" },\n      { key: \"delete\", label: \"削除\", danger: true },\n    ],\n    onClick: ({ key, conversation }) => run(key, conversation.key),\n  }}\n/>",
    "group": "navigation",
    "importPath": "@godxjp/chat",
    "name": "Conversations",
    "props": [
      {
        "description": "The rows. A conversation is { key, label?, group?, icon?, disabled? }; a rule between runs is { type: \"divider\", key?, dashed? }. Ant Design X `items`.",
        "name": "items",
        "type": "(ConversationsItemProp | ConversationsDividerProp)[]"
      },
      {
        "description": "Controlled selection — the key of the conversation on screen. Ant Design X `activeKey`.",
        "name": "activeKey",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial selection. Ant Design X `defaultActiveKey`.",
        "name": "defaultActiveKey",
        "type": "string"
      },
      {
        "description": "Fires with the picked key and the entry behind it. Ant Design X `onActiveChange`.",
        "name": "onActiveChange",
        "type": "(key: string, item?: ConversationsItemProp | ConversationsDividerProp) => void"
      },
      {
        "description": "The per-row overflow menu: { items: [{ key, label, icon?, danger?, disabled? }], onClick?, triggerLabel? }. Pass a function to vary it per row, or return undefined for a row that has no menu. Ant Design X `menu` (antd MenuProps there).",
        "name": "menu",
        "type": "ConversationsMenuProp | ((conversation: ConversationsItemProp) => ConversationsMenuProp | undefined)"
      },
      {
        "description": "Bucket rows by their `group` field. The object form takes label (node or (group) => node), collapsible (boolean or (group) => boolean), defaultExpandedKeys, expandedKeys and onExpand. Ant Design X `groupable`.",
        "name": "groupable",
        "type": "boolean | ConversationsGroupableProp"
      },
      {
        "description": "The \"new conversation\" button pinned above the rail: { label?, icon?, disabled?, onClick? }. Ant Design X `creation`.",
        "name": "creation",
        "type": "ConversationsCreationProp"
      },
      {
        "description": "Accessible name of the rail (a plain string — it lands on aria-label). Localized default otherwise.",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id of the rail root.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "ChatBubbleList — the feed beside this rail; Conversations picks WHICH feed is shown.",
      "ListRow — a single-line entity row with a trailing action, for short lists inside a Card. It has no selection, no roving focus and no grouping.",
      "Sidebar / NavList — route navigation. Use those when a row changes the URL; use Conversations when a row changes which conversation the surface is on.",
      "DropdownMenu — what the per-row `menu` renders; compose it directly when the menu is not attached to a conversation row."
    ],
    "rules": [
      2,
      6,
      23,
      44,
      45
    ],
    "storyPath": "navigation/conversations.tsx",
    "tagline": "The session rail of a chat surface (Ant Design X Conversations): past conversations, the current one marked with aria-current, a per-row overflow menu, and recency buckets — the whole rail one roving-tabindex tab stop, not one tab stop per conversation.",
    "usage": [
      "DO give every conversation a stable `key` — it is what activeKey, onActiveChange and the menu callback all address. A key that changes on re-render moves the selection.",
      "DO reach for `menu` for rename/delete instead of adding a second Button to each row. The trigger is keyboard-reachable with the forward arrow (→ in LTR, ← in RTL), so a row's second action costs no extra tab stop.",
      "DO pass `menu.triggerLabel` when the rows are user content: the default names the row, and twelve identical \"More actions\" buttons are indistinguishable in a screen reader's element list.",
      "DON'T hand-roll the rail out of full-width Buttons plus aria-current. That is one tab stop PER conversation; this is one for the whole rail, with ↑/↓/Home/End inside it.",
      "DON'T expect `styles`/`classNames` from Ant Design X — they are deliberately not ported. Retune the rail through the --conversations-* tokens (rules #44/#45).",
      "DO use `groupable={{ collapsible: true }}` for \"Today / Previous 7 days\": the bucket headings join the same roving order, so collapsing a bucket is reachable without leaving the rail."
    ],
    "useCases": [
      "The assistant rail of a chat product — past sessions, the current one marked, rename and delete per row.",
      "Recency buckets over a long history (Today / Yesterday / Previous 7 days) with the older buckets collapsed.",
      "A rail beside ChatBubbleList and ChatComposer: the three are one surface, and Conversations is the half that used to be missing."
    ]
  },
  {
    "docPath": "navigation/mega-menu.tsx",
    "example": "import { MegaMenu } from \"@godxjp/ui/lab\";\n\n<MegaMenu\n  label=\"メインナビゲーション\"\n  value={route}\n  onValueChange={setRoute}\n  triggerAction=\"hover\"\n  items={[\n    {\n      key: \"products\",\n      label: \"製品\",\n      panel: {\n        groups: [\n          {\n            key: \"core\",\n            label: \"コア\",\n            description: \"毎日使う業務アプリ\",\n            links: [\n              { key: \"hr\", label: \"人事管理\", href: \"/hr\", description: \"従業員台帳と異動\" },\n              { key: \"payroll\", label: \"給与計算\", href: \"/payroll\" },\n            ],\n          },\n        ],\n        footer: <a href=\"/products\">すべての製品を見る</a>,\n      },\n    },\n    { key: \"pricing\", label: \"料金\", href: \"/pricing\" },\n  ]}\n/>",
    "group": "navigation",
    "importPath": "@godxjp/ui/lab",
    "name": "MegaMenu",
    "props": [
      {
        "description": "The bar. An item WITH a `panel` is a disclosure button (antd SubMenuType); an item WITHOUT one is a plain link (antd MenuItemType). { key, label, href?, icon?, disabled?, panel? } where panel is { groups: [{ key, label?, description?, icon?, links: [{ key, label, href?, description?, icon?, disabled? }] }], footer? }. Ant Design `items`.",
        "name": "items",
        "type": "MegaMenuItemProp[]"
      },
      {
        "description": "Key of the OPEN PANEL, or null for none. Ant Design `openKeys` collapsed to one level — a megamenu bar is one level deep, so at most one panel is open and the array would only ever hold zero or one key.",
        "name": "open",
        "type": "string | null"
      },
      {
        "defaultValue": "null",
        "description": "Initial uncontrolled open panel. Ant Design `defaultOpenKeys`.",
        "name": "defaultOpen",
        "type": "string | null"
      },
      {
        "description": "Fires with the newly open panel's key, or null when everything closed. Ant Design `onOpenChange`.",
        "name": "onOpenChange",
        "type": "(key: string | null) => void"
      },
      {
        "description": "Key of the item for the CURRENT ROUTE — renders aria-current=\"page\" on the bar item and on the matching panel link. Ant Design `selectedKeys`, singular because a route is singular.",
        "name": "value",
        "type": "string"
      },
      {
        "description": "Uncontrolled initial current route. Ant Design `defaultSelectedKeys`.",
        "name": "defaultValue",
        "type": "string"
      },
      {
        "description": "Fires with the activated key (a top-level link or a panel link). Activation always closes the open panel. Ant Design `onClick`.",
        "name": "onValueChange",
        "type": "(key: string) => void"
      },
      {
        "defaultValue": "md",
        "description": "Bar density. The trigger box tracks the matching --control-height tier.",
        "name": "size",
        "type": "xs | sm | md | lg"
      },
      {
        "defaultValue": "click",
        "description": "Ant Design `triggerSubMenuAction`. Default is `click` here where antd defaults to `hover`, because a hover-only trigger has no equivalent on a touch screen; `hover` still accepts click, so touch is never stranded. antd's third value `contextMenu` is not ported.",
        "name": "triggerAction",
        "type": "click | hover"
      },
      {
        "defaultValue": "0",
        "description": "Ant Design `subMenuOpenDelay`, in MILLISECONDS (antd uses seconds). `hover` only.",
        "name": "openDelay",
        "type": "number"
      },
      {
        "defaultValue": "100",
        "description": "Ant Design `subMenuCloseDelay`, in MILLISECONDS. The hover-intent grace period: the pointer may cross a diagonal toward the panel for this long before anything closes. `hover` only.",
        "name": "closeDelay",
        "type": "number"
      },
      {
        "description": "Ant Design `expandIcon`, verbatim including its `false` to remove the chevron.",
        "name": "expandIcon",
        "type": "React.ReactNode | false"
      },
      {
        "description": "Router link component for every href in the bar and the panels — same contract and same spelling as Sidebar.linkComponent / NavList.linkComponent.",
        "name": "linkComponent",
        "type": "React.ComponentType<AnchorHTMLAttributes & { href?: string }>"
      },
      {
        "description": "Accessible name of the <nav> landmark (a plain string — it lands on aria-label). Localized default otherwise.",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id of the nav root.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "DropdownMenu — a menu of COMMANDS on a trigger (role=\"menu\"). Use it for actions; use MegaMenu for navigation to places.",
      "NavList — the same idea laid out vertically inside a page (a settings nav). antd's `Menu mode=\"inline\"` is Sidebar; `mode=\"vertical\"` is NavList.",
      "Topbar / TopbarItem — the APP shell's bar, and NOT where this goes. Measured, both slots break it: `topbar-center` is `display: none` below roughly 1280px (flex at 1440, none at 1024) so the nav vanishes on a laptop, and `topbar-start` is one `overflow: clip` / `flex-wrap: nowrap` row, so the narrow accordion runs out of it (58 elements past the viewport at 375). Put MegaMenu in the site header's own row beside the logo, and give phone width a `Sheet` behind a trigger — which is what real sites do anyway.",
      "Tabs — switches which panel of the SAME page is shown. A nav goes somewhere else.",
      "Breadcrumb — where you are in the hierarchy, not where you can go."
    ],
    "rules": [
      2,
      6,
      23,
      44,
      45
    ],
    "storyPath": "navigation/mega-menu.tsx",
    "tagline": "A primary site navigation whose top-level items disclose a full-width panel of grouped links (Ant Design `Menu mode=\"horizontal\"` whose SubMenu renders through popupRender). Implements the WAI-ARIA APG Disclosure Navigation pattern — NOT menu/menubar roles — with a roving tabindex across the bar, hover intent, Escape-to-trigger, and a narrow layout where the same disclosure lays out in flow.",
    "tier": "lab",
    "usage": [
      "DO reach for this INSTEAD of DropdownMenu for a site nav. DropdownMenu is react-aria-components Menu, i.e. role=\"menu\" / role=\"menuitem\": a row of them announces a desktop application menubar for what is actually a set of links, and Tab then leaves the whole widget instead of walking the links. That is the classic megamenu a11y defect and it is why this component exists.",
      "DO give the bar its landmark name through `label` when a page has more than one nav (a primary bar plus a footer nav): two unnamed <nav> landmarks are indistinguishable in a landmark list.",
      "DO drive `value` from your router. A change to it CLOSES the open panel, which is the close-on-route-change half of the contract and needs no router dependency here.",
      "DO use `linkComponent` for a client-side router; the library composes the row and your component renders only the <a>.",
      "DON'T nest a second level inside a panel. A panel is exactly one level deep by construction (`groups[].links[]`) — antd's arbitrary SubMenu nesting is `Sidebar`/`NavList` territory, not a bar.",
      "DON'T set `triggerAction=\"hover\"` and then also hide the trigger's own affordance: hover still opens on click here precisely so a touch user is not stranded, and WCAG 1.4.13 applies to anything hover-revealed.",
      "DON'T add a `theme=\"dark\"` prop expecting antd's. This library inverts by role scoping ([data-tenant] / per-region), and every surface here is a --mega-menu-* token (rules #44/#45).",
      "KNOW that the open panel is `position: fixed` and its geometry is MEASURED from the bar, not inherited. That is not a preference: an absolutely-positioned panel is clipped away by both surfaces a megamenu lives in (`Topbar`'s slots are `overflow: clip`, `Card` is `overflow: hidden` — measured at 331px of 348px gone, and not hit-testable). The consequence for you is that an open panel OVERLAYS what is beneath it, so do not leave one open by default in the middle of a scrolling page."
    ],
    "useCases": [
      "A marketing or product site's primary navigation, where Products / Solutions / Resources each open a panel of grouped links rather than a narrow list.",
      "An admin console with several product areas: a top bar where a section opens a panel of its screens, grouped with headings and one-line descriptions.",
      "A documentation site's top bar, where the current page is marked with aria-current in both the bar and the open panel."
    ]
  },
  {
    "docPath": "data-display/welcome.tsx",
    "example": "import { Welcome } from \"@godxjp/chat\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Bot } from \"lucide-react\";\n\n<Welcome\n  icon={<Bot />}\n  title=\"こんにちは\"\n  description=\"請求、経費、勤怠のことならお手伝いできます。\"\n  extra={<Button variant=\"ghost\" size=\"sm\">閉じる</Button>}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/chat",
    "name": "Welcome",
    "props": [
      {
        "description": "Leading glyph. A STRING beginning with http(s) is rendered as a decorative <img alt=\"\"> (Ant Design X does the same, with alt=\"icon\"); any other string renders as text.",
        "name": "icon",
        "type": "React.ReactNode | string"
      },
      {
        "description": "The greeting. Renders as an <h4>, which is Ant Design X's hardcoded Typography.Title level={4}.",
        "name": "title",
        "type": "React.ReactNode"
      },
      {
        "description": "The line under the greeting.",
        "name": "description",
        "type": "React.ReactNode"
      },
      {
        "description": "Trailing slot on the TITLE row — a dismiss button, a model picker. Not under the description.",
        "name": "extra",
        "type": "React.ReactNode"
      },
      {
        "defaultValue": "\"filled\"",
        "description": "filled gives the block its own tinted ground and hairline; borderless lets it sit on the page.",
        "name": "variant",
        "type": "\"filled\" | \"borderless\""
      },
      {
        "description": "DOM id of the block.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "EmptyState — the general 'nothing here yet' block for a list or a table. Welcome is the chat surface's greeting and carries an icon/title/description/extra shape of its own.",
      "PageContainer — owns the PAGE header; Welcome sits inside the page body.",
      "ChatSuggestion / ChatBubbleList — the rest of the same surface."
    ],
    "rules": [
      2,
      6,
      44,
      45
    ],
    "storyPath": "data-display/welcome.tsx",
    "tagline": "The greeting block at the head of an empty conversation (Ant Design X Welcome): glyph, greeting, one line under it, and a trailing slot ON THE TITLE ROW — which is the placement a hand-roll gets wrong.",
    "usage": [
      "DO put it above the composer on an empty chat, with ChatSuggestion or a Prompts row beneath it — that is the surface it belongs to.",
      "DO pass `extra` for the one action the greeting carries (dismiss, switch model). It lands beside the title, top-aligned, so a two-line title does not float it.",
      "DON'T reach for it as a generic page header — that is PageContainer's title/subtitle/extra, which owns the page rhythm.",
      "DON'T expect a heading-level prop: Ant Design X hardcodes level 4 and this ports that. Wrap it in your own heading hierarchy if the page needs a different rung.",
      "DON'T expect `styles`/`classNames` from Ant Design X — retune through the --welcome-* tokens."
    ],
    "useCases": [
      "The first screen of an assistant, before the first message.",
      "The head of a fresh conversation started from the Conversations rail.",
      "A feature introduction card inside a chat surface, dismissed through `extra`."
    ]
  },
  {
    "docPath": "general/actions.tsx",
    "example": "import { Actions, ActionsCopy, ActionsFeedback } from \"@godxjp/ui/general\";\nimport { RefreshCw, Share2 } from \"lucide-react\";\n\n<Actions\n  label=\"回答の操作\"\n  items={[\n    { key: \"retry\", label: \"やり直す\", icon: <RefreshCw />, onItemClick: () => regenerate() },\n    {\n      key: \"more\",\n      label: \"その他\",\n      subItems: [{ key: \"share\", label: \"共有\", icon: <Share2 /> }],\n    },\n    { key: \"copy\", actionRender: <ActionsCopy text={answer} /> },\n    { key: \"feedback\", actionRender: <ActionsFeedback value={vote} onChange={setVote} /> },\n  ]}\n  onClick={({ key }) => run(key)}\n/>",
    "group": "general",
    "importPath": "@godxjp/ui/general",
    "name": "Actions",
    "props": [
      {
        "description": "The actions: { key, label?, icon?, onItemClick?, disabled?, danger?, subItems?, actionRender? }. `label` is the accessible name AND the tooltip. `disabled` makes a plain action `aria-disabled` and ignores its click, but keeps it focusable and in the arrow-key order (WAI-ARIA toolbar) — use it for a formatting toolbar while previewing or read-only, not to hide an action (gh#1109). `subItems` folds the action into a menu; `actionRender` replaces it entirely.",
        "name": "items",
        "required": true,
        "type": "ActionsItemsProp[]"
      },
      {
        "description": "Fires for any action WITHOUT its own onItemClick — a per-item handler wins and this does not also fire, exactly as in Ant Design X. A sub-item reports keyPath [subKey, parentKey].",
        "name": "onClick",
        "type": "(info: { item, key, keyPath, domEvent }) => void"
      },
      {
        "defaultValue": "\"borderless\"",
        "description": "Chrome of the STRIP, not the intent of the buttons (that is `danger` per item).",
        "name": "variant",
        "type": "\"borderless\" | \"filled\" | \"outlined\""
      },
      {
        "description": "The strip fades in on mount. Zeroed under prefers-reduced-motion.",
        "name": "fadeIn",
        "type": "boolean"
      },
      {
        "description": "The same fade, arriving along the LOGICAL inline axis (so it mirrors under dir=rtl).",
        "name": "fadeInLeft",
        "type": "boolean"
      },
      {
        "description": "Accessible name of the toolbar (a plain string). Localized default otherwise.",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id of the strip.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "Toolbar / FilterBar — the list-page filter strip. Actions is the per-message action cluster, not a page-level control bar.",
      "DropdownMenu — what `subItems` renders; compose it directly when the menu is not one action in a strip.",
      "ChatBubble — the message the strip belongs to.",
      "CredentialReveal — a copy affordance for a SECRET field; ActionsCopy copies message text."
    ],
    "rules": [
      2,
      6,
      23,
      44,
      45
    ],
    "storyPath": "general/actions.tsx",
    "subParts": [
      "ActionsItem",
      "ActionsCopy",
      "ActionsFeedback"
    ],
    "tagline": "The strip of actions under an assistant message (Ant Design X Actions): copy, retry, like, and a menu for the rest — a WAI-ARIA toolbar with ONE tab stop, where Ant X's own strip is <div onClick> with no role and no accessible name.",
    "usage": [
      "DO give every action a `label`. It becomes the accessible name and the tooltip; without one the key is used, which is better than nameless but worse than a sentence.",
      "DO use `subItems` once the strip passes about five actions — it folds them behind one trigger instead of widening the row under every message.",
      "DO reach for ActionsCopy and ActionsFeedback instead of hand-rolling copy and thumbs: ActionsCopy announces the copy through a live region (a tick alone is invisible to a screen reader), and ActionsFeedback keeps BOTH buttons on screen with aria-pressed rather than hiding the one you did not pick.",
      "DON'T put a form control in the strip. It is a toolbar of buttons with one tab stop; a field inside would be unreachable by Tab.",
      "DON'T expect `dropdownProps`, `triggerSubMenuAction`, `styles` or `classNames` from Ant Design X — they are not ported; the strip is retuned through the --actions-* tokens."
    ],
    "useCases": [
      "Under an assistant answer: copy, regenerate, like/dislike, and a menu with share and report.",
      "Under a streaming answer: an ActionsItem with status=\"running\" while the audio plays back, error when it fails.",
      "In a message hover strip inside ChatBubbleList."
    ]
  },
  {
    "docPath": "data-display/thought-chain.tsx",
    "example": "import { ThoughtChain } from \"@godxjp/chat\";\n\n<ThoughtChain\n  label=\"思考の手順\"\n  defaultExpandedKeys={[\"search\"]}\n  items={[\n    { key: \"read\", title: \"資料を読む\", description: \"3件\", status: \"success\" },\n    {\n      key: \"search\",\n      title: \"社内規程を検索\",\n      status: \"loading\",\n      collapsible: true,\n      blink: true,\n      content: <pre>{hits}</pre>,\n    },\n    { key: \"write\", title: \"下書きを書く\", status: \"abort\" },\n  ]}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/chat",
    "name": "ThoughtChain",
    "props": [
      {
        "description": "The steps: { key?, icon?, title?, description?, content?, footer?, status?, collapsible?, blink?, destroyOnHidden? }. `icon: false` drops the glyph column; omitted, the step shows its 1-based ordinal (Ant Design X's own default).",
        "name": "items",
        "type": "ThoughtChainItemsProp[]"
      },
      {
        "description": "Uncontrolled initially-open steps.",
        "name": "defaultExpandedKeys",
        "type": "string[]"
      },
      {
        "description": "Controlled open steps.",
        "name": "expandedKeys",
        "type": "string[]"
      },
      {
        "description": "Fires with the NEXT open set.",
        "name": "onExpand",
        "type": "(keys: string[]) => void"
      },
      {
        "defaultValue": "true",
        "description": "The connector drawn between steps. false draws none. (Ant Design X's own type spells the third with a stray U+200C, so `line=\"dotted\"` does not type-check there; the clean spelling is used here.)",
        "name": "line",
        "type": "boolean | \"solid\" | \"dashed\" | \"dotted\""
      },
      {
        "description": "Accessible name of the chain (a plain string). Localized default otherwise.",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id of the chain root.",
        "name": "id",
        "type": "string"
      }
    ],
    "related": [
      "Timeline — the same vertical rail for events that ALREADY happened. Use it when nothing is in flight.",
      "Steps — a wizard's progress across a form. ThoughtChain is the assistant's own reasoning, not the user's path.",
      "Accordion — a general disclosure list with no rail, no ordinal and no status.",
      "ChatBubble — the answer the chain explains."
    ],
    "rules": [
      2,
      6,
      23,
      44,
      45
    ],
    "storyPath": "data-display/thought-chain.tsx",
    "subParts": [
      "ThoughtChainItem"
    ],
    "tagline": "The assistant's reasoning, step by step (Ant Design X ThoughtChain): an ORDERED list of steps, each with an ordinal or a glyph, a status, and a body it can collapse — where Ant X's own step is a <div onClick> with no role and no aria-expanded.",
    "usage": [
      "DO give every step a stable `key` — it is what expandedKeys addresses and what onExpand reports.",
      "DO set `collapsible` on a step whose `content` is long (a tool's raw output, a retrieved passage). The title then becomes a real disclosure button with aria-expanded, keyboard-reachable; without `collapsible` the body is simply always shown.",
      "DO use `status` for how a step ENDED — loading / success / error / abort. The word rides along in a visually hidden span, so the state is never carried by the tint alone.",
      "DO use `blink` while a step is still streaming; it pulses the title and body and collapses to nothing under prefers-reduced-motion.",
      "DON'T reach for it for events that already happened — that is Timeline. A thought chain is a run IN PROGRESS, which is why it has loading and abort states and a body that opens.",
      "DON'T expect `styles`/`classNames` from Ant Design X — retune through the --thought-chain-* tokens."
    ],
    "useCases": [
      "An agent's tool calls under its answer: read the documents, search the policy, draft the reply — each with its output collapsed.",
      "A long-running job's progress inside a chat: the current step blinking, the finished ones ticked, an aborted one greyed.",
      "ThoughtChainItem alone: the chip an assistant drops inline to name the tool it just reached for."
    ]
  },
  {
    "docPath": "data-display/org-chart.tsx",
    "example": "import { Avatar, AvatarFallback, Badge } from \"@godxjp/ui/data-display\";\nimport { OrgChart } from \"@godxjp/ui/lab\";\n\n<OrgChart\n  label=\"Company org chart\"\n  data={[\n    {\n      key: \"ceo\", name: \"Haruka Tanaka\", title: \"CEO\",\n      avatar: <Avatar size=\"sm\"><AvatarFallback>HT</AvatarFallback></Avatar>,\n      children: [\n        { key: \"cto\", name: \"Kenji Watanabe\", title: \"CTO\" },\n        { key: \"bot\", name: \"Review Agent\", title: \"Code review\", variant: \"agent\",\n          extra: <Badge tone=\"success\">Running</Badge> },\n      ],\n    },\n  ]}\n/>",
    "group": "data-display",
    "importPath": "@godxjp/ui/lab",
    "name": "OrgChart",
    "props": [
      {
        "description": "The hierarchy: { key, name, title?, avatar?, extra?, variant?: \"person\" | \"agent\", children? }. `key` is unique across the chart. `avatar` is usually an <Avatar>; `extra` a Badge or status.",
        "name": "data",
        "type": "OrgChartNodeProp[]"
      },
      {
        "description": "Replace a box's content. The box, its border, the connectors and the keyboard stay the library's; the narrow Tree form uses it too.",
        "name": "renderNode",
        "type": "(node: OrgChartNodeProp) => ReactNode"
      },
      {
        "description": "Accessible name of the role=\"tree\" (a plain string). Localized default \"Organization chart\".",
        "name": "label",
        "type": "string"
      },
      {
        "description": "DOM id of the root.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Root class.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Tree — the indented, collapsible outline. OrgChart renders it as its narrow form.",
      "Avatar — the mark in each box.",
      "Badge — a status in a box's `extra` slot."
    ],
    "rules": [
      2,
      6,
      23,
      44,
      45
    ],
    "storyPath": "data-display/org-chart.tsx",
    "tagline": "An organization chart: boxes (avatar, name, title, extra) joined by CSS connector lines, top-down; `agent` nodes are dashed. Scrolls horizontally in its own named region when wider than its container, and turns into an indented Tree when the CONTAINER is under 40rem. APG tree view (tree/treeitem/group, roving tabindex, arrow keys).",
    "tier": "lab",
    "usage": [
      "DO mark AI agents with `variant: \"agent\"` — the box is dashed AND its accessible name ends in a localized \"AI agent\", so the kind is never carried by the stroke alone.",
      "DO give it the width it has; the breakpoint is a container query, so a chart in a narrow side panel switches to the Tree form on a wide screen too.",
      "DO retune boxes and lines through the --org-chart-* tokens (node size, gaps, line width/colour, agent border style).",
      "DON'T wrap it in your own overflow-x scroller — it owns its scroll region, which only becomes a named tab stop when the chart actually overflows.",
      "DON'T use it for an outline users expand and collapse — that is Tree. OrgChart always shows every node."
    ],
    "useCases": [
      "A company or team org chart with people and AI agents side by side.",
      "Reporting lines on an admin screen, falling back to an indented list in a narrow panel or on a phone."
    ]
  },
  {
    "absorbed": [
      "Thumbnail"
    ],
    "docPath": "data-display/image.tsx",
    "example": "import { Image, ImagePreviewGroup, Prose } from \"@godxjp/ui/data-display\";\n\n<ImagePreviewGroup>\n  <Prose>\n    <p><Image src=\"/shots/before.png\" alt=\"Before the fix\" width={320} /></p>\n    <p><Image src=\"/shots/after.png\" alt=\"After the fix\" width={320} /></p>\n    <p><Image src=\"/logo.svg\" alt=\"\" width={24} preview={false} /></p>\n  </Prose>\n</ImagePreviewGroup>",
    "group": "data-display",
    "importPath": "@godxjp/ui/data-display",
    "name": "Image",
    "props": [
      {
        "description": "Image URL.",
        "name": "src",
        "type": "string"
      },
      {
        "description": "Required (as on Thumbnail). Also names the trigger \"Preview: {alt}\". Pass \"\" for a decorative picture.",
        "name": "alt",
        "type": "string"
      },
      {
        "description": "antd shape. `false` = a plain, non-clickable picture that also leaves its group. `src` shows a different (larger) file in the preview. Defaults: scaleStep 0.5, minScale 1, maxScale 50. `download` adds a Download action to the preview toolbar (gh#1122): `true` = a link to the picture on show (`<a href download>`; a cross-origin URL opens instead), a function `({ src, index }) => void` = a button for a signed URL or a blob. The same key works on `ImagePreviewGroup preview`.",
        "name": "preview",
        "type": "boolean | { visible?, onVisibleChange?(visible, prevVisible), src?, mask?, scaleStep?, minScale?, maxScale?, download? }"
      },
      {
        "description": "Shown when `src` fails. A failed picture does not open a preview (antd).",
        "name": "fallback",
        "type": "string"
      },
      {
        "description": "Painted over the frame until the picture loads; `true` = a muted block.",
        "name": "placeholder",
        "type": "ReactNode | boolean"
      },
      {
        "description": "antd Image: size the FRAME (number = px, or a CSS length); the picture fills it. `width` alone keeps the picture's ratio; a fixed height crops per `fit`. Also set on the img.",
        "name": "width / height",
        "type": "number | string"
      },
      {
        "description": "Thumbnail frame on the Thumbnail height scale (64 / 96 / 160px) at `--thumbnail-width-ratio` (4 / 3), cropped to fill. godx extension; `width`/`height` override the side they name.",
        "name": "size",
        "type": "\"sm\" | \"md\" | \"lg\""
      },
      {
        "description": "object-fit inside a fixed frame (`cover` crops, `contain` letterboxes). Default `cover` once the height is fixed (size or height). `intrinsic` keeps the picture's OWN ratio: the frame is the `size` height (default md) at the picture's own width — never cropped — with the hairline thumbnail frame on a muted backdrop, clamped to its column; `width`/`height` then stay `<img>` attributes (the file's real pixels) so the width is reserved before the bytes land. Use it for a wrapping row of mixed portrait/landscape screenshots at one height — what the retired `Thumbnail` was (v32). Pair with `preview={false}` for a plain picture.",
        "name": "fit",
        "type": "\"cover\" | \"contain\" | \"intrinsic\""
      },
      {
        "description": "Text under the picture in a figure held to the picture's width — a long file name truncates instead of widening the tile. godx extension.",
        "name": "caption",
        "type": "ReactNode"
      },
      {
        "description": "Class on the wrapper (the button).",
        "name": "className",
        "type": "string"
      },
      {
        "description": "Explicit gallery instead of the Images rendered under the group. A clicked child opens at the item with its own src.",
        "name": "ImagePreviewGroup.items",
        "type": "(string | { src, alt? })[]"
      },
      {
        "description": "antd shape and argument order. `false` turns every Image under the group into a plain picture.",
        "name": "ImagePreviewGroup.preview",
        "type": "boolean | { current?, onChange?(current, prevCurrent), visible?, onVisibleChange?(visible, prevVisible), scaleStep?, minScale?, maxScale? }"
      },
      {
        "description": "Shown in the preview for a picture that fails to load.",
        "name": "ImagePreviewGroup.fallback",
        "type": "string"
      }
    ],
    "related": [
      "Image `fit=\"intrinsic\" preview={false}` — a framed picture at a fixed height and its own width, with no preview (the former Thumbnail).",
      "Prose — the Markdown body the group usually wraps.",
      "Carousel — slides on the page itself, not a full-viewport preview."
    ],
    "rules": [
      2,
      6,
      23,
      44,
      45
    ],
    "storyPath": "data-display/image.tsx",
    "subParts": [
      "ImagePreviewGroup"
    ],
    "tagline": "antd `Image` + `Image.PreviewGroup`: a picture that opens in a full-viewport preview (zoom in/out by button, wheel or double-click; rotate; flip; reset; drag to pan). Inside an `ImagePreviewGroup` every Image at any depth joins one gallery in document order — prev/next buttons, ←/→, a \"2 / 3\" counter. Esc closes; focus is trapped and returned to the picture.",
    "usage": [
      "DO wrap a rendered Markdown body in ImagePreviewGroup and map its `img` to `<Image>` — every screenshot of the article then pages in one preview, in reading order.",
      "DO pass `preview={false}` for a logo or icon inside that body so it neither opens nor counts.",
      "DO use `Image.PreviewGroup` or `ImagePreviewGroup` — the same component.",
      "DON'T hand-roll a lightbox from Dialog + Carousel: it has no zoom, no arrow keys and a 32rem box. That composition is what this replaces.",
      "DON'T nest an Image inside a link or button: the thumbnail is itself a button.",
      "DO size an attachment gallery with `size=\"md\"` (96px, 4:3, cropped) and put the file name in `caption` — never an inline style reading `--thumbnail-block-size`."
    ],
    "useCases": [
      "Screenshots in a test result or wiki page that the reader opens large and pages through.",
      "A product or evidence gallery with zoom and rotate."
    ]
  },
  {
    "docPath": "data-entry/attachments.tsx",
    "example": "import { Attachments } from \"@godxjp/chat\";\n\n<Attachments\n  items={files}\n  onChange={({ fileList }) => setFiles(fileList)}\n  overflow=\"scrollX\"\n  maxCount={5}\n/>",
    "group": "data-entry",
    "importPath": "@godxjp/chat",
    "name": "Attachments",
    "props": [
      {
        "description": "Controlled attachment list. Ant Design X `items` (= antd Upload `fileList`).",
        "name": "items",
        "type": "AttachmentsItemProp[]"
      },
      {
        "description": "antd Upload `onChange` — NOT `onValueChange`.",
        "name": "onChange",
        "type": "(info: { file: AttachmentsItemProp; fileList: AttachmentsItemProp[] }) => void"
      },
      {
        "description": "Ant Design X `overflow`.",
        "name": "overflow",
        "type": "\"wrap\" | \"scrollX\" | \"scrollY\""
      },
      {
        "description": "Empty-state copy for inline and drop surfaces.",
        "name": "placeholder",
        "type": "AttachmentsPlaceholderProp | ((type) => AttachmentsPlaceholderProp)"
      },
      {
        "description": "Host for a full-screen drop overlay.",
        "name": "getDropContainer",
        "type": "() => HTMLElement | null"
      },
      {
        "description": "Maximum files (antd Upload `maxCount`).",
        "name": "maxCount",
        "type": "number"
      },
      {
        "defaultValue": "false",
        "description": "Blocks selection and drop; the control stays focusable so a reader can still reach it.",
        "name": "disabled",
        "type": "boolean"
      },
      {
        "description": "Forwarded to the hidden file input.",
        "name": "accept",
        "type": "string"
      },
      {
        "description": "Child mode: visible trigger; upload runs through a hidden input beside it.",
        "name": "children",
        "type": "ReactElement"
      },
      {
        "description": "antd Upload `onRemove`, narrowed to the attachment row. RETURNING `false` (or a promise of it) VETOES the removal and the card stays — anything else, including `undefined`, lets it go. That is how you gate a removal behind a confirm dialog without owning `items` yourself. It is awaited, so an async guard works.",
        "name": "onRemove",
        "type": "(item: AttachmentsItemProp) => boolean | void | Promise<boolean | void>"
      },
      {
        "description": "Ant Design X `classNames` — per-part classes (root, list, card, file, upload, placeholder). DECLARED AND FORWARDED, and the only semantic part map in this package: docs/DESIGN-AUTHORITY.md rules that antd's `classNames`/`styles` maps are NOT adopted because this library answers that layer with tokens (cardinal rule #45), and Attachments is the one component that carries them anyway. Recorded there as a contradiction, not a pattern — do not copy it onto another component, and retune through `--attachments-*` instead.",
        "name": "classNames",
        "type": "Partial<Record<AttachmentsSemanticProp, string>>"
      },
      {
        "description": "Ant Design X `styles` — the same part map as `classNames`, as inline styles, and under the same standing ruling against it. Inline styles beat every stylesheet rule, so this is the one handle in the package that can take a part off the design system entirely. The `--attachments-*` tokens are the supported route.",
        "name": "styles",
        "type": "Partial<Record<AttachmentsSemanticProp, React.CSSProperties>>"
      },
      {
        "description": "Ant Design X `rootClassName` — the outermost node. It is not a duplicate of `className`: in the full-screen-drop mode (`getDropContainer`) the outermost node is the OVERLAY rather than the inline tray, which is why antd separates the two. Both are applied.",
        "name": "rootClassName",
        "type": "string"
      },
      {
        "description": "ACCEPTED AND INERT. Ant Design X forwards it to its own Image preview; this package has no Image primitive yet, so the prop exists only so an Ant X call site type-checks, and passing it changes nothing on screen. Do not reach for it expecting a preview knob.",
        "name": "imageProps",
        "type": "Record<string, unknown>"
      }
    ],
    "related": [
      "Upload",
      "ChatComposer",
      "ChatBubbleList"
    ],
    "rules": [
      6,
      44,
      45
    ],
    "storyPath": "data-entry/attachments.tsx",
    "tagline": "The chat-surface attachment collection (Ant Design X Attachments): file cards, inline placeholder, optional full-screen drop target, and ref.select/ref.upload — inherits antd Upload props but names the list `items`.",
    "usage": [
      "DO keep antd field names on each item (`thumbUrl`, `originFileObj`, `uid`) — an Ant X call site should compile unchanged.",
      "DO use `ref.select({ accept, multiple })` to open the picker programmatically (Ant X 2.0).",
      "Ant X's `classNames` / `styles` / `rootClassName` ARE declared and forwarded, although docs/DESIGN-AUTHORITY.md rules that antd's semantic part maps are not adopted here. The older note in this slot said not to expect them at all; the type has never agreed with it, so an agent reading only the catalog was told the opposite of what autocomplete offered. Retune through the `--attachments-*` tokens, and treat the maps as a recorded contradiction on this one component rather than a pattern to reuse.",
      "The card is a FIXED box — 268x68 (Ant X's own), from `--attachments-card-size` (inline) and `--attachments-card-block-size`. The block size is also the `+` tile's square and the `overflow=\"scrollY\"` one-row viewport, so retune it once and all three follow. The file input is `sr-only`: never style it visible."
    ],
    "useCases": [
      "The attachment tray above a ChatComposer in an assistant surface.",
      "A Sender.Header slot showing picked files before send."
    ]
  },
  {
    "docPath": "data-display/marquee.tsx",
    "example": "import { Marquee } from \"@godxjp/ui/lab\";\nimport { Text } from \"@godxjp/ui/general\";\n\n<Marquee fade pauseOnHover label={t(\"partners.pauseLogos\")}>\n  {partners.map((partner) => (\n    <Text key={partner.id} size=\"sm\" tone=\"muted\">\n      {partner.name}\n    </Text>\n  ))}\n</Marquee>",
    "group": "data-display",
    "importPath": "@godxjp/ui/lab",
    "name": "Marquee",
    "props": [
      {
        "description": "The row of content. Rendered ONCE for real; every further copy that fills the track is an aria-hidden + inert clone, so the accessibility tree and the tab order see it exactly once however wide the viewport is.",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "Controlled motion state; pass with `onPlayChange`. Exists so one page-level 'stop all motion' switch can halt every track at once. The built-in pause control stays either way.",
        "name": "play",
        "type": "boolean"
      },
      {
        "description": "Uncontrolled initial state. Default true. Set false where the motion is not the point of the screen: WebAIM recommends animated content be paused by default.",
        "name": "defaultPlay",
        "type": "boolean"
      },
      {
        "description": "Fires on every transition, from the built-in control or a controlled write.",
        "name": "onPlayChange",
        "type": "(play: boolean) => void"
      },
      {
        "description": "The edge the content travels TOWARDS, spelled logically. Default `start`, which follows the reading direction; both members mirror under dir=\"rtl\" with no prop change.",
        "name": "direction",
        "type": "\"start\" | \"end\""
      },
      {
        "description": "Pace ordinal over the `--marquee-interval` motion token (a duration per SCREENFUL, so the pace is the same with one item or forty). Default `base`. Not px/s: a raw number is neither themeable nor width-aware.",
        "name": "speed",
        "type": "\"slow\" | \"base\" | \"fast\""
      },
      {
        "description": "Space between items AND between copies, as a token step. Defaults to the `--marquee-gap-inline` token.",
        "name": "gap",
        "type": "GapProp"
      },
      {
        "description": "Also pause under the pointer. Default false. An ADDITION to the pause control, never the mechanism. Focus inside the track always pauses it regardless.",
        "name": "pauseOnHover",
        "type": "boolean"
      },
      {
        "description": "Mask both edges over `--marquee-mask-width`. Default false. A mask, not an opaque gradient colour, so it follows a themed or dark background.",
        "name": "fade",
        "type": "boolean"
      },
      {
        "description": "Names the CONTENT (\"partner logos\"), not the button. The control's accessible name is composed from it and the state verb ('Pause scrolling: partner logos'), so the name still flips when the state does. A non-string node is ignored.",
        "name": "label",
        "type": "ReactNode"
      },
      {
        "description": "Structural class on the root.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Carousel — the answer whenever the reader should STEP through discrete items. It is user-driven, has prev/next and dots, and does not move on its own.",
      "Activity — ambient motion with no travel: a pulse or three dots that say something is happening, with no content to read.",
      "ScrollArea — what a Marquee becomes under prefers-reduced-motion, and what to reach for directly when the row should simply be scrollable.",
      "Flex / ResponsiveGrid — the static logo wall. Start here; add motion only when something asks for it."
    ],
    "rules": [
      2,
      44,
      45
    ],
    "storyPath": "data-display/marquee.tsx",
    "tagline": "A track of content that travels continuously, carrying the WCAG 2.2.2 pause control that makes it conformant. The clone count is MEASURED against the viewport and re-measured on resize; under prefers-reduced-motion it does not move at all.",
    "tier": "lab",
    "usage": [
      "DO leave the pause control alone. WCAG 2.2 SC 2.2.2 makes it a conformance requirement for anything that moves automatically for more than five seconds beside other content, and technique F16 names 'a scrolling news ticker without a mechanism to pause it' as the failure by example.",
      "DO ask first whether it should move at all. Nielsen Norman Group and WebAIM both discourage auto-moving content, and no major design system ships a ticker. A static `Flex wrap` or `ResponsiveGrid` logo wall is usually the better screen; `defaultPlay={false}` is the middle ground.",
      "DO use `play` / `onPlayChange` for a page-level 'reduce motion on this screen' switch across several tracks.",
      "DON'T reach for `pauseOnHover` as the pause mechanism. A keyboard user never hovers; that is exactly the gap in `react-fast-marquee`, whose clones also carry no `aria-hidden`.",
      "DON'T put essential, non-duplicated information in it. A reader who looks away misses it, and under prefers-reduced-motion the row becomes a scrollable strip instead.",
      "DON'T wrap it in your own overflow/animation CSS. The clone count and the lap distance are measured; a second overflow box breaks the measurement and the seam.",
      "DON'T add a role. `role=\"marquee\"` is a LIVE REGION for frequently CHANGING content; here nothing changes, only its position. Wrap it in a labelled <section> when the collection needs a name."
    ],
    "useCases": [
      "A partner or customer logo wall on a marketing page, where the row is wider than the viewport.",
      "An announcement or status strip above an application shell (maintenance windows, release notes) that must stay pausable.",
      "A 'now processing' rail of job identifiers beside a dashboard, where the motion signals activity rather than carrying the data."
    ]
  },
  {
    "docPath": "layout/masonry.tsx",
    "example": "import { Masonry } from \"@godxjp/ui/lab\";\nimport { Card, CardContent } from \"@godxjp/ui/data-display\";\nimport { Text } from \"@godxjp/ui/general\";\n\n// T flows from `items`, so `data` in itemRender is whatever you put there — give the\n// collection a shape and the render callback needs no annotation.\ntype Note = { id: string; body: string };\nconst notes: Note[] = useNotes();\n\n<Masonry\n  columns={{ base: 1, sm: 2, lg: 3 }}\n  gap=\"md\"\n  items={notes.map((note) => ({ key: note.id, data: note }))}\n  itemRender={({ data }) => (\n    <Card>\n      <CardContent>\n        <Text>{data?.body}</Text>\n      </CardContent>\n    </Card>\n  )}\n  onLayoutChange={(layout) => console.log(layout.length, \"tiles placed\")}\n/>",
    "group": "layout",
    "importPath": "@godxjp/ui/lab",
    "name": "Masonry",
    "props": [
      {
        "description": "The tiles, in READING order — which is also DOM order. Each is { key, children?, column?, height?, data? }, antd's MasonryItem field for field.",
        "name": "items",
        "type": "MasonryItemProp<TData>[]"
      },
      {
        "description": "Renders a tile that carries no `children`. antd's rule is ported exactly: `children` wins over `itemRender`, and the callback receives the live index and column.",
        "name": "itemRender",
        "type": "(item: MasonryItemProp<TData> & { index: number; column: number }) => ReactNode"
      },
      {
        "defaultValue": "3",
        "description": "Column count, fixed or per VIEWPORT breakpoint (sm 40rem · md 48rem · lg 64rem · xl 80rem), taking the widest matching declared step and falling back to `base`. antd's 3 is kept. antd spells the steps xs..xxl; `xs` IS `base` here and there is no `xxl` — both are a TypeScript error and a dev-time warning, never a silent drop.",
        "name": "columns",
        "type": "number | { base?: number; sm?: number; md?: number; lg?: number; xl?: number }"
      },
      {
        "defaultValue": "(none — the --masonry-gap-* tokens, which default to 0 like antd)",
        "description": "Spacing between tiles. THIS IS ANT DESIGN'S `gutter`, renamed: this package already owns the axis as `gap` (Flex, ResponsiveGrid, AuthStack) and takes a GapProp token step rather than a pixel number, so a masonry breathes with --scaling. The tuple is antd's [Gap, Gap] = [inline, block]. Passing `gutter` is a compile error that names `gap`.",
        "name": "gap",
        "type": "GapProp | [GapProp, GapProp]"
      },
      {
        "defaultValue": "false",
        "description": "antd `fresh`. Wraps EVERY tile in its own ResizeObserver so a tile that changes height in place — a chart settling, a 'show more', a late image — re-packs the columns. Off, only the container's own resize and a descendant load/error re-measure.",
        "name": "fresh",
        "type": "boolean"
      },
      {
        "description": "Fires once every tile has a resolved position and the count matches `items`, and is deduped on an unchanged assignment. antd DOCUMENTS `{ key, column }[]` but its implementation spreads the whole item; the implementation is what is ported, so `data` comes back with it.",
        "name": "onLayoutChange",
        "type": "(layout: (MasonryItemProp<TData> & { column: number })[]) => void"
      },
      {
        "description": "DOM id of the container.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Structural class on the container.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "ResponsiveGrid — the answer whenever the tiles are or can be EQUAL height. It is pure CSS grid with container queries, needs no measurement, and keeps DOM order and visual order identical. Reach for Masonry only when the heights genuinely differ.",
      "Flex — a single row or column of mixed-width content; no columns, no packing.",
      "Card / CardContent — the usual tile. Masonry positions the tile box and never styles what is inside it.",
      "AspectRatio — pair it with `height` when the tiles are images of a known ratio; the layout then settles with no measurement pass at all."
    ],
    "rules": [
      2,
      40,
      44,
      45
    ],
    "storyPath": "layout/masonry.tsx",
    "tagline": "Ant Design `Masonry` (6.0.0): tiles of UNEQUAL height packed into columns, each tile dropped into whichever column is shortest when its turn comes. DOM order stays `items` order at every width, so the reading order never follows the packing.",
    "tier": "lab",
    "usage": [
      "DO order `items` by IMPORTANCE, never by height. The tiles are absolutely positioned, so DOM order — what a screen reader reads and what Tab visits — is always the `items` order, while the visual order is the packing. A later tile can sit visually above an earlier one; that is the form, not a bug.",
      "DO give every tile a stable `key`. It is the identity the measurement cache and `onLayoutChange` are keyed on, not React's element key alone.",
      "DO pass `height` when you already know it (a fixed-ratio thumbnail, a server-rendered feed). It skips the measurement pass, so the first paint lands in the right place instead of settling a frame later.",
      "DO reach for `fresh` only when tiles change height ON THEIR OWN. It is one ResizeObserver per tile; a static feed does not need it.",
      "DON'T use `gutter` — that is antd's name for `gap` here, and it is typed `never` so the compiler says so. `gap` takes a token step: gap=\"md\", gap={4}, gap={[\"sm\", \"md\"]}.",
      "DON'T write `columns={{ xs: 1 }}`. The mobile-first floor is `base`, and `xxl` does not exist here; both spellings are rejected at compile time and warned about at runtime.",
      "DON'T add a role. A masonry is a layout and WAI-ARIA 1.2 has no role for one; `region` would collide with landmark-unique, `list` would announce structure the tiles already carry. Wrap it in a labelled <section> when the COLLECTION needs a name.",
      "DON'T use it for equal-height tiles — that is ResponsiveGrid, which needs no measurement and no JavaScript."
    ],
    "useCases": [
      "A media or document gallery where thumbnails have different aspect ratios and a fixed-row grid would leave a ragged band of whitespace under every row.",
      "A feed of notes, comments or activity cards whose text length varies wildly — a one-line entry beside a twelve-line one.",
      "A dashboard of report cards of different depth (a two-row summary beside a ten-row table) that should still fill the page evenly.",
      "A pinned-column layout: one tile held in the first column with `column: 0` (a filter panel, a promoted card) while the rest of the feed packs around it."
    ]
  },
  {
    "docPath": "layout/affix.tsx",
    "example": "import { Affix } from \"@godxjp/ui/layout\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\nconst [pinned, setPinned] = useState(false);\n\n<Affix offsetBlockStart={64} onChange={setPinned}>\n  <Flex align=\"center\" justify=\"between\" gap=\"sm\">\n    <Text weight=\"medium\">{pinned ? \"Invoices\" : \"All invoices, 2026\"}</Text>\n    <Button size={pinned ? \"sm\" : \"md\"}>New invoice</Button>\n  </Flex>\n</Affix>",
    "group": "layout",
    "importPath": "@godxjp/ui/layout",
    "name": "Affix",
    "props": [
      {
        "description": "What gets pinned. Must not itself be `position: absolute` (antd's note applies here for the same reason: the pin works by making this content `position: fixed`).",
        "name": "children",
        "type": "ReactNode"
      },
      {
        "description": "antd `offsetTop`, on its LOGICAL axis. Pixels from the scrollport's block-start edge at which it pins, and the distance it sits at once pinned. Overrides the `--affix-inset-block-start` token per instance; omit it to take the theme's. Default `0` (antd's) via that token.",
        "name": "offsetBlockStart",
        "type": "number"
      },
      {
        "description": "antd `offsetBottom`, on its LOGICAL axis — the sticky FORM FOOTER rather than the sticky header. Passing it (and no `offsetBlockStart`) is what selects block-end pinning, exactly as in antd; block-start wins when both are given.",
        "name": "offsetBlockEnd",
        "type": "number"
      },
      {
        "description": "antd `target` — the scroll box to pin against, which need not be the nearest scrolling ancestor. Omitted, it IS the nearest block-axis scroller (`position: sticky`'s rule), else the viewport (gh#984); `() => window` is the viewport. Same lazy-getter shape `FloatButton.BackTop.target` uses here, and for the same reason: the element does not exist on the render that declares it.",
        "name": "target",
        "type": "() => Window | HTMLElement | null"
      },
      {
        "description": "antd `onChange`. Fires on the pin TRANSITION and only on it — never on a scroll frame that did not change the state. Structural, not guarded: the value is React state, so the effect cannot run without it having flipped, and the mount is skipped.",
        "name": "onChange",
        "type": "(affixed: boolean) => void"
      },
      {
        "description": "NOT props. antd's physical spellings, kept in the type so that arriving from antd's docs is a COMPILE ERROR naming `offsetBlockStart` / `offsetBlockEnd`, plus a development console.warn. See docs/DESIGN-AUTHORITY.md.",
        "name": "offsetTop / offsetBottom",
        "type": "never"
      },
      {
        "description": "DOM id of the outer (in-flow) box.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Structural class on the outer box.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Anchor — takes an `AffixProp` subset as its own `affix` prop and pins its section nav with this.",
      "PageContainer — `stickyFooter` / `footerReveal=\"onScroll\"` is the page-level answer; it shares the same `useIntersects` observer this uses.",
      "FloatButton.BackTop — the other public scroll-position API, and where `target`'s lazy-getter shape comes from.",
      "Topbar / AppShell — the app chrome an affixed bar usually sits UNDER; `--affix-inset-block-start` is how it clears it."
    ],
    "rules": [
      2,
      40,
      44,
      45
    ],
    "storyPath": "layout/affix.tsx",
    "tagline": "Ant Design `Affix` (6.6.5): pin an element to its scrollport once the page scrolls past it, and REPORT that it is pinned. `position: sticky` pins and then says nothing, so a sticky header cannot shrink, swap its logo or raise its shadow; this one exposes the boolean as `data-affixed` for CSS and `onChange` for JavaScript, and holds the flow open with a MEASURED placeholder so the page does not jump by the bar's height at the moment it pins.",
    "usage": [
      "DO use it whenever the pinned state has to be VISIBLE — a header that condenses, a toolbar that gains a shadow over the rows sliding under it, a filter rail that swaps to a compact form. That state is the entire reason this exists; if you need none of it, plain `position: sticky` is lighter and correct.",
      "DO style the pinned state off `[data-affixed]` on `data-slot=\"affix-content\"`, and transition on `--duration-fast` / `--ease-standard`. The condensed bar must still be there under `prefers-reduced-motion` — it snaps to the smaller size, it never fades out.",
      "DO set the resting offset ONCE in your theme (`:root { --affix-inset-block-start: 4rem; }`) when every affixed bar in the product has to clear the same app header. `offsetBlockStart` is the per-instance override, not the place to repeat a global decision.",
      "DO pass `target` when the page scrolls inside a pane rather than the document — an AppShell main region, a dialog body, a MasterDetail column.",
      "DON'T put a `transform`, `filter` or `backdrop-filter` on an ancestor. That ancestor becomes the containing block for `position: fixed` and the bar pins to it instead of the scrollport. The same limitation applies to `position: sticky` and to antd's Affix; there is no fix inside the component.",
      "DON'T give the children `position: absolute`, and DON'T expect a horizontally scrolling container to work — antd documents both, and both are true here.",
      "DON'T hand-roll the placeholder. The height is MEASURED, so it is right at every width and after any re-wrap; a number written once in CSS is wrong at every other width, which is the page-jump bug this component exists to remove."
    ],
    "useCases": [
      "A site or app header that condenses on scroll: the wordmark shrinks to a monogram and the bar loses half its height, driven by `data-affixed` alone.",
      "A table toolbar (search, filters, bulk actions) that stays reachable over a thousand-row list and gains a shadow the moment it starts covering rows.",
      "A form's action footer pinned to the bottom of a long form with `offsetBlockEnd`, released once the real end of the form scrolls into view.",
      "The substrate under `Anchor affix` — antd specifies Anchor's own `affix` prop as `AffixProps`, so the two are one stack."
    ]
  },
  {
    "docPath": "navigation/anchor.tsx",
    "example": "import { Anchor } from \"@godxjp/ui/lab\";\n\n<Anchor\n  offsetBlockStart={64}\n  targetOffsetBlockStart={88}\n  label=\"On this page\"\n  items={[\n    { key: \"overview\", href: \"#overview\", title: \"Overview\" },\n    {\n      key: \"pricing\",\n      href: \"#pricing\",\n      title: \"Pricing\",\n      children: [{ key: \"tiers\", href: \"#tiers\", title: \"Tiers\" }],\n    },\n    { key: \"faq\", href: \"#faq\", title: \"FAQ\" },\n  ]}\n  onValueChange={(href) => console.log(\"reading\", href)}\n/>",
    "group": "navigation",
    "importPath": "@godxjp/ui/lab",
    "name": "Anchor",
    "props": [
      {
        "description": "antd `items`, in document order. Each is `{ key, href, title, target?, children?, replace?, targetOffsetBlockStart? }` — antd's AnchorItem, field for field, with its per-link `targetOffset` on the logical axis. `children` is ONE level of nesting and is dropped (with a warning) when `direction=\"horizontal\"`, as in antd.",
        "name": "items",
        "type": "AnchorItemProp[]"
      },
      {
        "description": "antd `direction`, default `\"vertical\"`. Horizontal is one scrolling row: the ink rail moves along the block-end edge and nesting is not available.",
        "name": "direction",
        "type": "\"vertical\" | \"horizontal\""
      },
      {
        "description": "antd `affix`, default `true` — literally antd's own `AffixProps` minus the fields Anchor supplies, which is why Affix is the dependency. `false` leaves the nav in the flow.",
        "name": "affix",
        "type": "boolean | Omit<AffixProp, 'offsetBlockStart' | 'offsetTop' | 'target' | 'children'>"
      },
      {
        "description": "antd `bounds`, default `5`. Pixel tolerance added to the decision line.",
        "name": "bounds",
        "type": "number"
      },
      {
        "description": "The active `href`, as the controlled triad. `onValueChange` is antd's `onChange` under this library's name, and it reports the link the SCROLL POSITION resolved (antd's own note) rather than what `getCurrentAnchor` substituted. antd's `onChange` is typed `never` and warns.",
        "name": "value / defaultValue / onValueChange",
        "type": "string / string / (href: string) => void"
      },
      {
        "description": "antd `getCurrentAnchor` — the last word on the highlight, running INSIDE the resolution with no render round-trip. A controlled `value` outranks it.",
        "name": "getCurrentAnchor",
        "type": "(activeLink: string) => string"
      },
      {
        "description": "antd `offsetTop`, on its LOGICAL axis: where the decision line sits, and the distance `Affix` pins the nav at. Default `0`.",
        "name": "offsetBlockStart",
        "type": "number"
      },
      {
        "description": "antd `targetOffset`, on its LOGICAL axis: where a CLICKED section lands — the room a pinned header needs. Defaults to `offsetBlockStart`, and (as in antd) it moves the decision line too, so a click can never leave the entry it just selected unselected.",
        "name": "targetOffsetBlockStart",
        "type": "number"
      },
      {
        "description": "gh#890. The scroll box the sections are measured in AND `Affix` pins the nav against — one function, both halves. `Affix`'s own name and shape (`AffixTargetProp`), the same lazy getter `FloatButton.BackTop.target` already spells here. `null` means the viewport; absent (with no `getContainer`), the nearest block-axis scroller, else the viewport — `Affix`'s default (gh#984). Wins over `getContainer` when both are given.",
        "name": "target",
        "type": "() => Window | HTMLElement | null"
      },
      {
        "description": "antd `getContainer` — the scroll box holding the sections; omitted → the nearest block-axis scroller, else the viewport (gh#984). Superseded by `target` (gh#890), which mirrors `Affix`'s own spelling for the identical idea; kept live for a call site written before `target` existed.",
        "name": "getContainer",
        "type": "() => HTMLElement | Window"
      },
      {
        "description": "antd `showInkInFixed`, default `false`. Draw the travelling ink even with `affix={false}`; the static rule is always drawn.",
        "name": "showInkInFixed",
        "type": "boolean"
      },
      {
        "description": "antd `replace`, default `false`. Replace the hash in history rather than pushing it; `items[].replace` overrides it per entry.",
        "name": "replace",
        "type": "boolean"
      },
      {
        "description": "antd `onClick`, fired before the scroll. A modified click is never hijacked.",
        "name": "onClick",
        "type": "(event: React.MouseEvent<HTMLAnchorElement>, item: AnchorItemProp) => void"
      },
      {
        "description": "SCREEN-READER-ONLY accessible name of the <nav> landmark — it is never drawn (unlike Progress `label`, which is a visible caption). Need a visible heading over the links? Render a Heading yourself and keep this as the landmark name. antd ships an unnamed <div>; a page routinely carries a breadcrumb, a rail and this, so a localized default (\"On this page\") applies when omitted.",
        "name": "label",
        "type": "string"
      },
      {
        "description": "NOT props. antd's spellings, kept in the type so arriving from antd's docs is a COMPILE ERROR naming `onValueChange` / `offsetBlockStart` / `targetOffsetBlockStart`, plus a development console.warn.",
        "name": "onChange / offsetTop / targetOffset",
        "type": "never"
      },
      {
        "description": "DOM id of the <nav>.",
        "name": "id",
        "type": "string"
      },
      {
        "description": "Structural class on the <nav>.",
        "name": "className",
        "type": "string"
      }
    ],
    "related": [
      "Affix — the pin underneath it. antd specifies `Anchor.affix` as `AffixProps`, so the two ship as one stack.",
      "NavList — ROUTE navigation inside a page; its `activeId` is the answer Anchor computes, for a different kind of destination.",
      "LegalDocumentShell — carries its own built-in contents rail for the legal-document case; Anchor is the general one, for content the shell does not own.",
      "Breadcrumb — where you ARE in the hierarchy (`aria-current=\"page\"`), not where you are on the page."
    ],
    "rules": [
      2,
      6,
      40,
      44,
      45
    ],
    "storyPath": "navigation/anchor.tsx",
    "tagline": "Ant Design `Anchor` (6.6.5): the in-page section navigation, and the only thing in this library that COMPUTES which section is current. `NavList activeId` takes that answer as a prop. Resolves by antd's rule — the last section whose edge has crossed a single decision line — which is a pure function of scroll position and therefore cannot flicker, suppresses itself during the programmatic scroll a click starts, reads the landing hash before any scroll event, and jumps rather than tweens under `prefers-reduced-motion`. Keyboard focus uses the shared inset ring in clipping surfaces; scrollports reveal partially clipped keyboard targets without changing their layout.",
    "tier": "lab",
    "usage": [
      "DO give every target a real `id` and point `href` at it as `#id`. The links are real anchors: middle-clickable, deep-linkable, and correct before JavaScript boots.",
      "DO set `targetOffsetBlockStart` to the height of whatever is pinned above the content, or a clicked section lands under the header. Leave `offsetBlockStart` alone unless the DECISION LINE also needs moving; it follows `targetOffsetBlockStart` by default.",
      "DO use `value` / `onValueChange` when the router owns the current section; use `getCurrentAnchor` only for antd's narrower job of rewriting the resolved link in place.",
      "DON'T reach for NavList for this. NavList is ROUTE navigation — each row is a page the router renders, and its `activeId` is an answer you already have. Anchor's entries are fragments of the page being read, which is also why the current one is `aria-current=\"location\"` and not `\"page\"`.",
      "DON'T use Tabs either: the sections are all on the page at once and scroll past each other, where a tab panel shows one at a time.",
      "DON'T nest more than one level, and don't nest at all when horizontal — antd allows neither, and this warns rather than rendering a second row the ink rail cannot follow.",
      "DON'T hand-roll the scrollspy with an IntersectionObserver band. A section taller than the band reports nothing, and with several short sections in the band at once the answer depends on scroll direction; both are why this measures against a line instead."
    ],
    "useCases": [
      "A long settings or policy page whose sections a reader jumps between, with the current one always visible in the rail.",
      "API or product documentation: the on-this-page rail beside the article, tracking the reader down the page.",
      "A marketing landing page's section nav, pinned under the site header via `affix` and `offsetBlockStart`.",
      "A horizontal section bar on a narrow viewport, where a vertical rail has no column to live in."
    ]
  }
]
