@godxjp/ui-mcp 26.4.0 → 27.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +10 -11
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import{Server as je}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Ge}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as qe,ListResourcesRequestSchema as We,ListToolsRequestSchema as _e,ReadResourceRequestSchema as Ke}from"@modelcontextprotocol/sdk/types.js";var v=[{name:"inertiaUpload",group:"data-entry",importPath:"@godxjp/ui/inertia",tagline:"Bridge Inertia multipart visits to Upload progress, cancellation, retry and error recovery.",props:[{name:"send",type:"(file: File, callbacks: InertiaUploadCallbacks) => void",description:"Start a router.post visit with the supplied callbacks and file."},{name:"errorMessage",type:"string",description:"Localized fallback for network or HTTP failures."}],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."],example:`import { inertiaUpload } from "@godxjp/ui/inertia";
3
3
  const upload = inertiaUpload((file, callbacks) => router.post(endpoint, { file }, callbacks), t("Upload failed"));
4
- <Upload onUpload={upload} />;`,docPath:"FORMS.md",storyPath:"data-entry/Upload.stories.tsx",rules:[1],related:["Upload","FormRoot"]},{name:"FormRoot",tagline:"Client or server-adapted forms with shared layout, validation, submission, reset and error recovery.",props:[{name:"form",type:"UseFormReturn<T>",description:"RHF form returned by useZodForm; use either form or adapter."},{name:"adapter",type:"FormStateAdapter",description:"External store with getValue/setValue/getError/getValues/isSubmitting and optional reset."},{name:"onSubmit",type:"(values: T) => void | Promise<void>",description:"Validated submit callback."},{name:"onSubmitFailed",type:"(errors: FieldErrors<T>) => void",description:"Validation failure callback."},{name:"onSubmitError",type:"(error: unknown) => void",description:"Handle rejected submission."},{name:"onReset",type:"() => void",description:"Called after values reset."},{name:"scrollToFirstError",type:"boolean",description:"Scroll the first invalid field into view."},{name:"disabled",type:"boolean",description:"Disable field mutations."},{name:"layout",type:"FormLayoutProp",description:"Shared Form layout."},{name:"labelWidth",type:"WidthProp",description:"Shared label width."},{name:"controlWidth",type:"WidthProp",description:"Shared control width."},{name:"labelAlign",type:'"start" | "end"',description:"Shared label alignment."},{name:"collapseBelow",type:"BreakpointProp | false",description:"Responsive stacking breakpoint."},{name:"density",type:"DensityProp",description:"Control density."},{name:"errors",type:"ErrorBagProp",description:"Server error bag."},{name:"requiredMark",type:'boolean | "optional"',description:"Required/optional marking policy."},{name:"columns",type:"ResponsiveGridColumnsProp",description:"Responsive field grid rendered inside the form element."}],example:`import { FormRoot, FormFieldControl, useZodForm } from "@godxjp/ui/form";
4
+ <Upload onUpload={upload} />;`,docPath:"FORMS.md",storyPath:"data-entry/Upload.stories.tsx",rules:[1],related:["Upload","FormRoot"]},{name:"FormRoot",tagline:"Client or server-adapted forms with shared layout, validation, submission, reset and error recovery.",props:[{name:"form",type:"UseFormReturn<T>",description:"RHF form returned by useZodForm; use either form or adapter."},{name:"adapter",type:"FormStateAdapter",description:"External store with getValue/setValue/getError/getValues/isSubmitting and optional reset."},{name:"onSubmit",type:"(values: T) => void | Promise<void>",description:"Validated submit callback."},{name:"onSubmitFailed",type:"(errors: FieldErrors<T>) => void",description:"Validation failure callback."},{name:"onSubmitError",type:"(error: unknown) => void",description:"Handle rejected submission."},{name:"submitFailedMessage",type:"ReactNode | false",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:"onReset",type:"() => void",description:"Called after values reset."},{name:"scrollToFirstError",type:"boolean",description:"Scroll the first invalid field into view."},{name:"disabled",type:"boolean",description:"Disable field mutations."},{name:"layout",type:"FormLayoutProp",description:"Shared Form layout."},{name:"labelWidth",type:"WidthProp",description:"Shared label width."},{name:"controlWidth",type:"WidthProp",description:"Shared control width."},{name:"labelAlign",type:'"start" | "end"',description:"Shared label alignment."},{name:"collapseBelow",type:"BreakpointProp | false",description:"Responsive stacking breakpoint."},{name:"density",type:"DensityProp",description:"Control density."},{name:"errors",type:"ErrorBagProp",description:"Server error bag."},{name:"requiredMark",type:'boolean | "optional"',description:"Required/optional marking policy."},{name:"columns",type:"ResponsiveGridColumnsProp",description:"Responsive field grid rendered inside the form element."}],example:`import { FormRoot, FormFieldControl, useZodForm } from "@godxjp/ui/form";
5
5
  import { Input } from "@godxjp/ui/data-entry";
6
6
  import { z } from "zod";
7
7
  const schema = z.object({ email: z.string().email() });
8
8
  // Inside your component:
9
9
  const form = useZodForm(schema, { defaultValues: { email: "" } });
10
- <FormRoot form={form} onSubmit={save} layout="horizontal"><FormFieldControl name="email" label="Email">{field => <Input {...field} value={String(field.value ?? "")} />}</FormFieldControl></FormRoot>;`,docPath:"docs/data-entry/form-root.tsx",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"FormFieldControl",tagline:"Bind a typed field to RHF or a server adapter, with shared FormField presentation and validation.",props:[{name:"name",type:"FieldPath<T>",description:"Typed field path, including nested list rows."},{name:"label",type:"React.ReactNode",description:"Visible field label."},{name:"dependencies",type:"FieldPath<T>[]",description:"Dependent fields that trigger revalidation."},{name:"getValueFromEvent",type:"(...args: unknown[]) => unknown",description:"Extract the control value."},{name:"normalize",type:"(value: unknown, previous: unknown) => unknown",description:"Normalize before storage."},{name:"preserve",type:"boolean",description:"Keep value after unmount, default true."},{name:"help",type:"React.ReactNode",description:"Override validation error text."},{name:"disabled",type:"boolean",description:"Field-level disabled state."},{name:"validateStatus",type:'"success" | "warning" | "error" | "validating"',description:"Validation feedback state."},{name:"hasFeedback",type:"boolean",description:"Show accessible validation feedback."},{name:"feedback",type:"React.ReactNode",description:"Custom feedback icon/content."},{name:"children",type:"(field) => React.ReactNode",description:"Render a godx-ui control bound to the field."},{name:"id",type:"string",description:"Optional DOM identity; defaults to a unique id even across sibling forms."},{name:"layout",type:"FormLayoutProp",description:"Per-field layout override."},{name:"labelWidth",type:"WidthProp",description:"Per-field label width."},{name:"controlWidth",type:"WidthProp",description:"Per-field control width."},{name:"labelAddon",type:"React.ReactNode",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:"colSpan",type:"number",description:"Grid column span."}],example:`import { FormFieldControl } from "@godxjp/ui/form";
10
+ <FormRoot form={form} onSubmit={save} layout="horizontal"><FormFieldControl name="email" label="Email">{field => <Input {...field} value={String(field.value ?? "")} />}</FormFieldControl></FormRoot>;`,docPath:"docs/data-entry/form-root.tsx",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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 \u2014 the fields / `<FormErrors />` show it, as antd shows field errors instead of a form banner \u2014 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) \u2014 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 \u2014 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 \u2014 pass the bag to `errors` and the form renders it once."],useCases:["Validated settings forms","Nested repeating data entry"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"FormFieldControl",tagline:"Bind a typed field to RHF or a server adapter, with shared FormField presentation and validation.",props:[{name:"name",type:"FieldPath<T>",description:"Typed field path, including nested list rows."},{name:"label",type:"React.ReactNode",description:"Visible field label."},{name:"dependencies",type:"FieldPath<T>[]",description:"Dependent fields that trigger revalidation."},{name:"getValueFromEvent",type:"(...args: unknown[]) => unknown",description:"Extract the control value."},{name:"normalize",type:"(value: unknown, previous: unknown) => unknown",description:"Normalize before storage."},{name:"preserve",type:"boolean",description:"Keep value after unmount, default true."},{name:"help",type:"React.ReactNode",description:"Override validation error text."},{name:"disabled",type:"boolean",description:"Field-level disabled state."},{name:"validateStatus",type:'"success" | "warning" | "error" | "validating"',description:"Validation feedback state."},{name:"hasFeedback",type:"boolean",description:"Show accessible validation feedback."},{name:"feedback",type:"React.ReactNode",description:"Custom feedback icon/content."},{name:"children",type:"(field) => React.ReactNode",description:"Render a godx-ui control bound to the field."},{name:"id",type:"string",description:"Optional DOM identity; defaults to a unique id even across sibling forms."},{name:"layout",type:"FormLayoutProp",description:"Per-field layout override."},{name:"labelWidth",type:"WidthProp",description:"Per-field label width."},{name:"controlWidth",type:"WidthProp",description:"Per-field control width."},{name:"labelAddon",type:"React.ReactNode",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:"colSpan",type:"number",description:"Grid column span."}],example:`import { FormFieldControl } from "@godxjp/ui/form";
11
11
  import { Input } from "@godxjp/ui/data-entry";
12
- <FormFieldControl name="email" label="Email" required>{field => <Input {...field} value={String(field.value ?? "")} />}</FormFieldControl>;`,docPath:"docs/data-entry/form-root.tsx",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"FormFieldArray",tagline:"Dynamic typed field collections with stable keys, nested validation and append/remove/reorder operations.",props:[{name:"name",type:"FieldArrayPath<T>",description:"Array field path in the surrounding RHF FormRoot."},{name:"children",type:"(collection) => React.ReactNode",description:"Receives fields with stable key/name/index, append/prepend/insert/remove/move/swap/replace, array error and disabled state."}],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>;',docPath:"docs/FORMS.md",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"useZodForm",tagline:"Create a typed React Hook Form instance with Zod validation; reuse reset, setValue, trigger, formState and field errors.",props:[{name:"schema",type:"z.ZodType<T>",description:"Zod schema, including async refinements."},{name:"options",type:"UseZodFormOptionsProp<T>",description:"RHF options such as defaultValues, mode, reValidateMode, criteriaMode and shouldUnregister."}],example:`import { useZodForm } from "@godxjp/ui/form";
12
+ <FormFieldControl name="email" label="Email" required>{field => <Input {...field} value={String(field.value ?? "")} />}</FormFieldControl>;`,docPath:"docs/data-entry/form-root.tsx",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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 \u2014 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={\u2026}` claims its bag key, so the 422 message renders once under the field \u2014 see FormRoot's canonical server-validation composition (FormRoot + FormErrors + AlertMutationFeedback renders a 422 exactly once)."],useCases:["Validated settings forms","Nested repeating data entry"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"FormFieldArray",tagline:"Dynamic typed field collections with stable keys, nested validation and append/remove/reorder operations.",props:[{name:"name",type:"FieldArrayPath<T>",description:"Array field path in the surrounding RHF FormRoot."},{name:"children",type:"(collection) => React.ReactNode",description:"Receives fields with stable key/name/index, append/prepend/insert/remove/move/swap/replace, array error and disabled state."}],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>;',docPath:"docs/FORMS.md",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"useZodForm",tagline:"Create a typed React Hook Form instance with Zod validation; reuse reset, setValue, trigger, formState and field errors.",props:[{name:"schema",type:"z.ZodType<T>",description:"Zod schema, including async refinements."},{name:"options",type:"UseZodFormOptionsProp<T>",description:"RHF options such as defaultValues, mode, reValidateMode, criteriaMode and shouldUnregister."}],example:`import { useZodForm } from "@godxjp/ui/form";
13
13
  import { z } from "zod";
14
14
  const form = useZodForm(z.object({email: z.string().email()}), {defaultValues: {email: ""}, mode: "onBlur"});`,docPath:"docs/FORMS.md",group:"data-entry",importPath:"@godxjp/ui/form",storyPath:"data-entry/Form.stories.tsx",rules:[23,31],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"],related:["Form","FormField","FormRoot","FormFieldControl"]},{name:"CardBar",group:"data-display",tagline:"Inline card toolbar with scoped inset and divider edges.",props:[{name:"pad",type:"PadProp",description:"Instance padding on the token scale."},{name:"padRaw",type:"PadRawProp",description:"Measured padding escape."},{name:"gap",type:"GapProp",description:"Spacing between main and extra slots."},{name:"surface",type:'"muted"',description:"Optional muted ground."},{name:"border",type:'"none" | "block-start" | "block-end" | "both"',description:"Override positional divider edges for stacked bars."}],storyPath:"data-display/Card.stories.tsx",docPath:"docs/data-display/card/index.tsx",rules:[9],usage:["Compose inside Card. Unset border follows its position; explicit border prevents double rules in stacked bars."],related:["Card","Flex"],useCases:["Composer tool strips and view tabs."],example:'<Card><CardBar pad={{block:2,inline:3}} border="block-start">Tools</CardBar></Card>'},{name:"TimeRangePicker",group:"data-entry",tagline:"Ordered time range with optional endpoints and canonical native fields.",props:[{name:"value",type:"[string,string]",description:"Controlled canonical times."},{name:"defaultValue",type:"[string,string]",description:"Uncontrolled initial range."},{name:"onValueChange",type:"(value: [string,string]) => void",description:"Reports edited range."},{name:"order",type:"boolean",description:"Sort times automatically; false allows overnight ranges."},{name:"allowEmpty",type:"[boolean,boolean]",description:"Permitted empty endpoints, default [true,true]."},{name:"name",type:"string",description:"Native names are name_from and name_to."},{name:"format",type:"string",description:"TimePicker display format."},{name:"disabledTime",type:"TimePickerDisabledTimeProp",description:"Shared time constraints."},{name:"showSeconds",type:"boolean",description:"Enable second precision."},{name:"minuteStep",type:"number",description:"Minute column step."},{name:"hourStep",type:"number",description:"Hour column step."},{name:"secondStep",type:"number",description:"Second column step."},{name:"allowClear",type:"boolean",description:"Permit clear only for an endpoint also allowed empty."}],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."],related:["TimePicker","DatePicker"],example:`import { TimeRangePicker } from "@godxjp/ui/data-entry";
15
15
  <TimeRangePicker aria-label="Shift" defaultValue={["09:00", "18:00"]} />`,storyPath:"data-entry/time-range-picker.tsx",docPath:"docs/data-entry/time-range-picker.tsx",rules:[9]},{name:"VisuallyHidden",group:"general",tagline:"Accessible supporting text without a visible layout box.",props:[{name:"children",type:"ReactNode",description:"Text available to assistive technology."}],example:"<VisuallyHidden>Unread</VisuallyHidden>",docPath:"docs/general/typography.tsx",storyPath:"general/typography.tsx",rules:[],usage:["Use for supplementary accessible labels. Do not hide controls that remain focusable."]},{name:"RangeTimeline",group:"data-display",tagline:"Horizontal intervals with token-owned geometry and optional endpoint editing.",props:[{name:"label",type:"string",required:!0,description:"Localized accessible name for the scrollable schedule."},{name:"columns",type:"{ label: string; units: number }[]",required:!0,description:"Positive unit counts determine proportional column widths. Column count determines the minimum canvas width, so coarse grouping zooms out."},{name:"bands",type:"{ label: string; units: number }[]",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:"rows",type:"RangeTimelineRow[]",required:!0,description:"Each row has id, label, inclusive start/end unit offsets, and localized startLabel/endLabel including current values."},{name:"today",type:"number | null",description:"Optional current unit marker."},{name:"onRangeChange",type:'(id: string, edge: "start" | "end", delta: number) => void',description:"Committed endpoint movement in units. Omit for read-only. Dates remain consumer data; do not replace true anchors with clipped positions."}],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" }]} />',docPath:"docs/data-display/timeline.tsx",storyPath:"data-display/timeline.tsx",rules:[],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.","Use TimelineGrid for time-of-day columns; RangeTimeline is a horizontal range axis."]},{name:"PageContainer",group:"layout",tagline:"Mandatory page shell \u2014 EVERY page wraps its content in PageContainer (title/subtitle/extra/footer/breadcrumb).",props:[{name:"footerPad",type:"PadProp",description:"Instance footer inset; omitted preserves the theme."},{name:"toolbarPad",type:"PadProp",description:"Instance toolbar inset; omitted preserves the theme."},{name:"breadcrumbLabel",type:"string",description:"T\xEAn kh\u1EA3 truy c\u1EADp c\u1EE7a landmark <nav> breadcrumb. M\u1EB7c \u0111\u1ECBnh l\xE0 chu\u1ED7i Breadcrumb \u0111\xE3 d\u1ECBch."},{name:"title",type:"string",required:!0,description:"Page heading rendered as <h1>."},{name:"subtitle",type:"string",description:"Secondary line beneath the title."},{name:"status",type:"ReactNode",description:'Status/meta band beside the title (StatusBadge, environment tag, "updated \u2026" 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 \u2014 never hand-lay a badge next to the <h1>.'},{name:"extra",type:"ReactNode",description:"Action buttons / controls rendered right of the title row."},{name:"toolbar",type:"ReactNode",description:'FIXED chrome band between the page header and the body \u2014 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 \u2014 no wrapper, no gap. It 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 its ONLY breathing room is its own inset.'},{name:"footer",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 \u2014 `measure="narrow" | "medium"` or a service-wide `--app-shell-page-max-width` inside AppShell \u2014 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:"children",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 \u2014 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:"breadcrumb",type:"BreadcrumbItemProp[]",description:"Ordered trail of { label, to? } segments above the title."},{name:"breadcrumbAriaLabel",type:"string",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 \u2014 two nav landmarks sharing one name/role fail landmark-unique.`},{name:"variant",type:'"default" | "narrow" | "flush" | "ghost"',defaultValue:'"default"',description:"Page shell layout; flush removes padding for full-bleed content."},{name:"density",type:'"compact" | "default" | "comfortable"',defaultValue:'"default"',description:"Spacing density across the page subtree."},{name:"preset",type:'"default" | "admin-collection"',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:"headerLayout",type:'"stack" | "responsive-inline"',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 \u2014 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:"headerScale",type:'"document" | "chrome"',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 \u2014 a chat channel name, a mail subject line, an IDE tab \u2014 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 \u2014 this moves the type step only, never the element, so the screen-reader outline is untouched."},{name:"measure",type:'"default" | "narrow" | "medium"',defaultValue:'"default"',description:'Bounded page MEASURE shared by the header AND the body \u2014 a third axis, ORTHOGONAL to `variant` (chrome) and `headerLayout` (arrangement). "narrow" (--page-measure-narrow, 42rem outer \u2192 624px visible surface) and "medium" (--page-measure-medium, 48rem outer \u2192 720px visible surface) cap BOTH bands, so a header `extra` action ends flush with the body surface instead of stranded at the page edge \u2014 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:"stickyFooter",type:"boolean",defaultValue:"false",description:'Pin footer to viewport bottom on scroll \u2014 pairs with variant="narrow".'},{name:"footerReveal",type:'"always" | "onScroll"',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 \u2014 the standard edit/create save bar. Stays mounted (no reflow \u2192 no jitter).'},{name:"fill",type:"boolean",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:"headerLoading",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 \u2014 they come from the route, not the record. This is not a page-wide loading flag; use DataState for the body."},{name:"linkComponent",type:"React.ElementType",description:"Link component used for breadcrumb / header links (e.g. an Inertia or React Router `Link`). Defaults to a native `<a>`."}],usage:["DO: Always wrap every page's content in PageContainer \u2014 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` \u2014 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 \u2014 e.g. `extra={<Button>\u65B0\u898F\u4F5C\u6210</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 \u2014 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 \u2014 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 \u2014 `--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 \u2014 `--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. It is the band's ONLY breathing room: the band sits FLUSH against the header and the body (chrome is attached \u2014 a ruled, painted band adrift in two 16px voids divides nothing), so there is no outside space to tune. The default is `0` and stays `0`: a transparent band is not a surface and has no inside for an inset to breathe, and under `fill` every pixel of band height comes straight off the scroll viewport the slot exists to protect. Once the band is painted or ruled it DOES have an inside, and `--page-toolbar-pad-block: var(--space-2)` is where that inset belongs. The CALL SITE never sets it \u2014 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 \u2014 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'>\u2026</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` \u2014 three theme declarations, zero call-site classes.","DO: Set `headerScale='chrome'` when the page's top row is CHROME rather than a document title \u2014 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 \u2014 ghost drops the header's bottom pad and lets no divider inherit in \u2014 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 \u2014 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) \u2014 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 \u2014 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 \u2014 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 \u2014 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`) \u2014 those are the canonical page-header names. If you see `description` / `actions` in old code, migrate them.","DO: Leave `fill` off (the default) for ordinary pages \u2014 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 \u2014 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) \u2014 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 \u2014 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 \u2014 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) \u2014 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 \u2014 `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\u2192body 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` \u2014 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 \u2014 use `variant='flush'` + `<PageContainer.Inset>` for the Toolbar above the table.","A detail / edit form page where the footer holds Save and Cancel buttons \u2014 use `footer={<Flex direction='row' justify='between' fill><Button variant='outline'>\u524A\u9664</Button><Button>\u4FDD\u5B58</Button></Flex>}` with `stickyFooter` + `footerReveal='onScroll'` so the save bar slides up only once the header (and its actions) scroll out of view \u2014 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 \u2014 use `variant='default'` with `children={<Flex direction='col' gap='lg'>\u2026</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 \u2014 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 \u2014 use `fill` so the body occupies the full shell height, with `footer={<Composer/>}` + `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 \u2014 `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 \u2014 `toolbar={<Toolbar>\u2026</Toolbar>}` with `fill` + `footer={<Composer/>}` + `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'`)."],related:["PageContainer.Inset \u2014 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 \u2014 always use PageContainer for new pages; it supports `children`, `toolbar`, `footer`, `variant`, `density`, `stickyFooter`, and `fill`. Legacy code using the old prop names (`description` \u2192 `subtitle`, `actions` \u2192 `extra`) should be migrated to PageContainer.","AppShell \u2014 the outer shell that owns the sidebar/topbar layout grid; PageContainer lives inside AppShell's `children` slot. Do not put AppShell inside PageContainer \u2014 the nesting order is AppShell \u2192 PageContainer.","SplitPane \u2014 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."],example:`import { PageContainer, Flex } from "@godxjp/ui/layout";
@@ -873,7 +873,7 @@ import remarkGfm from "remark-gfm";
873
873
 
874
874
  <FormField id="coupon-name" label="\u30AF\u30FC\u30DD\u30F3\u540D" required error={errors.name} helper="\u6700\u592750\u6587\u5B57">
875
875
  <Input id="coupon-name" placeholder="\u6625\u306E\u82B1\u7C89\u75C7\u5BFE\u7B5615%OFF" value={name} onValueChange={(e) => setName(e.target.value)} />
876
- </FormField>`,storyPath:"data-entry/FormField.stories.tsx",rules:[23]},{name:"FormErrors",subParts:["FormErrorsProvider"],group:"data-entry",tagline:"The 'no field to stand on' error summary \u2014 renders the entries of the surrounding Form's server error bag that no mounted FormField name='\u2026' 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.",props:[{name:"errors",type:"Partial<Record<string, string | string[]>>",description:"Explicit error bag \u2014 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:"title",type:"ReactNode",description:"Heading above the messages. Defaults to the localized 'please review your input' title (dataEntry.formErrors.title)."},{name:"className",type:"string",description:"Root class override."}],usage:["DO pass the WHOLE bag to `<Form errors={form.errors}>` and place `<FormErrors />` at the top of the form \u2014 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 \u2014 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 \u2014 that is `Alert.QueryError` / toast territory. FormErrors is scoped to the VALIDATION bag of the surrounding form.","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 \u2014 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`) \u2014 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."],related:["Form \u2014 provides the error bag (`errors`) and the claim registry FormErrors reads; FormErrors must sit inside it (or receive `errors` explicitly).","FormErrorsProvider \u2014 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 \u2014 `name` claims a bag key and self-binds its message; the claimed key never re-appears in FormErrors.","Alert \u2014 the underlying destructive banner; use Alert directly for non-validation notices."],example:`import { Form, FormErrors, FormField, Input } from "@godxjp/ui/data-entry";
876
+ </FormField>`,storyPath:"data-entry/FormField.stories.tsx",rules:[23]},{name:"FormErrors",subParts:["FormErrorsProvider"],group:"data-entry",tagline:"The 'no field to stand on' error summary \u2014 renders the entries of the surrounding Form's server error bag that no mounted FormField name='\u2026' 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.",props:[{name:"errors",type:"Partial<Record<string, string | string[]>>",description:"Explicit error bag \u2014 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:"title",type:"ReactNode",description:"Heading above the messages. Defaults to the localized 'please review your input' title (dataEntry.formErrors.title)."},{name:"className",type:"string",description:"Root class override."}],usage:["DO pass the WHOLE bag to `<Form errors={form.errors}>` and place `<FormErrors />` at the top of the form \u2014 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 \u2014 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 \u2014 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} />\u2026</FormRoot>` \u2014 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 \u2014 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`) \u2014 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."],related:["Form \u2014 provides the error bag (`errors`) and the claim registry FormErrors reads; FormErrors must sit inside it (or receive `errors` explicitly).","FormErrorsProvider \u2014 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 \u2014 `name` claims a bag key and self-binds its message; the claimed key never re-appears in FormErrors.","Alert \u2014 the underlying destructive banner; use Alert directly for non-validation notices."],example:`import { Form, FormErrors, FormField, Input } from "@godxjp/ui/data-entry";
877
877
  import { useForm } from "@inertiajs/react";
878
878
 
879
879
  const form = useForm({ customer_nm: "", action_mode: "regist" });
@@ -899,7 +899,7 @@ const form = useForm({ customer_nm: "", action_mode: "regist" });
899
899
  aria-label="\u6570\u91CF"
900
900
  />`},{name:"SearchInput",group:"data-entry",tagline:"Debounced search box with a clear button. Fires onSearch (NOT onChange) after the debounce. Controlled (value) or uncontrolled (defaultValue).",props:[{name:"status",type:'"error" | "warning"',description:"Validation state the field paints \u2014 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:"variant",type:'"outlined" | "filled" | "borderless"',defaultValue:'"outlined"',description:"Chrome level \u2014 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 \u2014 a single bottom rule is a Material convention and SmartHR, the JP authority here, draws every field as a full box."},{name:"onSearch",type:"(q: string) => void",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:"value",type:"string",description:"Controlled value."},{name:"defaultValue",type:"string",defaultValue:'""',description:"Initial uncontrolled value."},{name:"placeholder",type:"string",description:"Input placeholder."},{name:"debounce",type:"number",defaultValue:"250",description:"Debounce delay (ms)."},{name:"id",type:"string",description:"Input id; pair with `label` or an external `<label htmlFor>`."},{name:"label",type:"React.ReactNode",description:"Optional visible label rendered above the search box (falls back to an sr-only label)."},{name:"onValueChange",type:"(value: string) => void",description:"Fires on EVERY keystroke (immediate) \u2014 required to keep a controlled `value` responsive."},{name:"ariaLabel",type:"string",description:"Accessible search name when there is no visible label."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disable search input and clearing."}],usage:["DO: listen to `onSearch`, not `onChange`. The component debounces internally (default 250 ms) and fires `onSearch(q)` after the delay \u2014 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` \u2014 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>` \u2014 still accessible, but providing a context-specific string (e.g. `ariaLabel='\u8ACB\u6C42\u66F8\u3092\u691C\u7D22'`) 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 \u2014 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 (\xD7), search icon, and accessible label \u2014 recreating these with a raw `<Input>` adds code and misses the UX contract.","DON'T: place SearchInput inside a `ToolbarGroup` wrapper \u2014 `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 \u2014 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 \u2014 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."],related:["Input \u2014 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 \u2014 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 \u2014 compose them together.","Command \u2014 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) \u2014 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."],example:`import { SearchInput } from "@godxjp/ui/data-entry";
901
901
 
902
- <SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u30FBID\u3067\u691C\u7D22" value={search} onSearch={setSearch} />`,storyPath:"data-entry/SearchInput.stories.tsx",rules:[23]},{name:"Select",subParts:["SelectContent","SelectGroup","SelectItem","SelectLabel","SelectScrollDownButton","SelectScrollUpButton","SelectSeparator","SelectTrigger","SelectValue"],group:"data-entry",tagline:"Polymorphic single-select: pass options/loadOptions for the data-driven (Ant-style) API, or compose sub-parts manually \u2014 never use a raw <select>.",props:[{name:"mode",type:'"multiple" | "tags"',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."},{name:"maxCount",type:"number",description:"Maximum selected options; selected items remain removable."},{name:"maxTagCount",type:"number",description:"Collapse extra selected labels."},{name:"options",type:"(SearchSelectOptionProp | { label: string; options: SearchSelectOptionProp[] })[]",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:"loadOptions",type:"(params: SearchSelectLoadParamsProp) => Promise<SearchSelectLoadResultProp>",description:"Async remote fetcher. Receives { query, page } (1-based). Must return { options, hasMore? }. Implies showSearch=true automatically."},{name:"showSearch",type:'boolean | { filterOption?: boolean | ((input, option) => boolean); optionFilterProp?: "label" | "value" | "sublabel"; filterSort?; searchValue?: string; onSearch?: (value: string) => void; autoClearSearchValue?: boolean }',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:"value",type:"string",defaultValue:'""',description:"Controlled selected value (data-driven API). Pass an empty string to represent no selection."},{name:"defaultValue",type:"string",description:"Uncontrolled initial value (data-driven API). The trigger shows the matching option's label at rest \u2014 including in searchable (showSearch) mode \u2014 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:"onValueChange",type:"(value: string, option?: SearchSelectOptionProp) => void",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) => \u2026` 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:"renderOption",type:"(option: SearchSelectOptionProp) => React.ReactNode",description:"Custom per-option renderer for the dropdown ROWS. Defaults to label + optional sublabel. Does not change the trigger \u2014 use `labelRender` for that."},{name:"labelRender",type:"(selected: { value: string; label: React.ReactNode; option?: SearchSelectOptionProp }) => React.ReactNode",description:"Custom renderer for the SELECTED value shown on the TRIGGER (`labelRender`) \u2014 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:"selectedLabel",type:"string",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:"selectedIcon",type:"React.ReactNode",description:"Leading icon shown on the trigger for the current value when its option isn't loaded yet (async preset) \u2014 the trigger counterpart of `selectedLabel`, so an edit form pre-filled from the server shows the avatar/flag at rest."},{name:"placeholder",type:"string",description:"Placeholder shown in the trigger when no value is selected."},{name:"searchPlaceholder",type:"string",description:"Placeholder inside the search input (combobox mode only)."},{name:"emptyMessage",type:"string",description:"Message rendered when the filtered list is empty."},{name:"loadingMessage",type:"string",description:"Message rendered while loadOptions is resolving."},{name:"errorMessage",type:"string",description:"Message rendered when an async loadOptions REJECTS \u2014 a distinct state from empty/loading. Defaults to a localized 'Couldn\u2019t load options'. The panel shows this instead of a blank surface or a misleading 'no results'."},{name:"clearable",type:"boolean",defaultValue:"true",description:"Show a clear row when a value is selected (data-driven API). Set to false for required fields."},{name:"clearLabel",type:"string",description:"Label for the clear row (data-driven combobox mode)."},{name:"disabled",type:"boolean",description:"Disables the entire select."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Searchable mode only (showSearch/loadOptions). Value is shown (and the clear affordance hidden) but the popover cannot be opened \u2014 no new pick, no search. Mirrors the Input/NumberInput readOnly contract: stays focusable and still submits its value, unlike disabled."},{name:"size",type:'"xs" | "sm" | "md" | "lg"',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:"width",type:'"full" | "auto" | "bounded"',defaultValue:'"full"',description:"Trigger width on the data-driven API (options / loadOptions), searchable or not \u2014 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:"open",type:"boolean",description:"Searchable mode only. Controlled popover open state (uncontrolled by default). Pair with onOpenChange."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Searchable mode only. Fires on every open/close attempt \u2014 including ones ignored because open is externally pinned \u2014 so a controlled consumer stays in sync."},{name:"search",type:"string",description:"Searchable mode only. Controlled search-box query (uncontrolled by default). Pair with onSearchChange."},{name:"onSearchChange",type:"(query: string) => void",description:"Searchable mode only. Fires on every keystroke in the search box."},{name:"filterOption",type:"(option: SearchSelectOptionProp, query: string) => boolean",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:"renderError",type:"(params: { message: string; retry: () => void }) => React.ReactNode",description:"Searchable mode only. Custom error slot, overriding the default errorMessage row. retry() reloads from the first page."},{name:"renderLoadMore",type:"(params: { hasMore: boolean; loading: boolean; loadMore: () => void }) => React.ReactNode",description:"Searchable mode only. Custom 'load more' affordance appended below the list while another page is available \u2014 pairs with (does not replace) the built-in scroll-triggered pagination."},{name:"name",type:"string",description:"Form field name. Submits the selected value via a hidden input (data-driven API). Required for uncontrolled form submission."},{name:"id",type:"string",description:"HTML id for the trigger element. Wire to a <label htmlFor> for a11y."},{name:"className",type:"string",description:"Additional CSS classes applied to the trigger."},{name:"data-testid",type:"string",description:"Test id on the trigger. Option items get ${data-testid}-option-${value} automatically."},{name:"SelectTrigger size",type:'"sm" | "md"',defaultValue:'"md"',description:"Compound API only. Size variant on the SelectTrigger sub-component."},{name:"SelectTrigger width",type:'"full" | "auto"',defaultValue:'"full"',description:"Compound API only. `full` fills the field column (inside FormField). `auto` sizes the trigger to its label \u2014 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 showIndicator",type:"boolean",defaultValue:"true",description:"Compound API only. Set false to omit the built-in chevron disclosure indicator from the DOM entirely (not a CSS hide) \u2014 for specialized triggers (icon-only, etc.) that render their own affordance, so no consumer descendant CSS is needed."},{name:"status",type:'"error" | "warning"',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:"variant",type:'"outlined" | "filled" | "borderless"',description:"antd `variant` \u2014 the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once."},{name:"loading",type:"boolean",description:"antd `loading` \u2014 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:"defaultOpen",type:"boolean",description:"antd `defaultOpen` \u2014 uncontrolled initial popup state."},{name:"allowClear",type:"boolean | { clearIcon?: React.ReactNode; label?: string }",description:"antd `allowClear`. The object form replaces the \u2715 icon and/or its accessible label. Beats `clearable` when both are given."},{name:"onClear",type:"() => void",description:"antd `onClear` \u2014 fires after the value is cleared through the \u2715."},{name:"notFoundContent",type:"React.ReactNode",description:"antd `notFoundContent` \u2014 the node shown when the popup has nothing to list. Outranks the string-only emptyMessage."},{name:"autoClearSearchValue",type:"boolean",description:"antd `autoClearSearchValue` (default true) \u2014 clear the search box after a pick / on close. Set false to resume the same filtered list on the next open."},{name:"filterSort",type:"(a: SearchSelectOptionProp, b: SearchSelectOptionProp, info: { searchValue: string }) => number",description:"antd `filterSort` \u2014 orders what filterOption kept. Static options only; with loadOptions the server owns the order. Never mutates the caller's array."},{name:"optionRender",type:"(option: SearchSelectOptionProp, info: { index: number }) => React.ReactNode",description:"antd `optionRender` \u2014 per-option renderer in antd's own (option, { index }) shape. Outranks the older renderOption."},{name:"menuItemSelectedIcon",type:"React.ReactNode",description:"antd `menuItemSelectedIcon` \u2014 a decorative mark on the picked row. Off by default: the picked row is already marked by fill + weight, which costs no width."},{name:"popupMatchSelectWidth",type:"boolean | number",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:"fieldNames",type:"{ label?: string; value?: string; options?: string; groupLabel?: string; disabled?: string }",description:"antd `fieldNames` \u2014 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:"labelInValue",type:"boolean",description:"antd `labelInValue` \u2014 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:'\u6771\u4EAC\u672C\u793E'} renders the pick immediately, with no options page loaded and no flash of the raw id."},{name:"prefix",type:"React.ReactNode",description:"antd `prefix` \u2014 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:"suffixIcon",type:"React.ReactNode",description:"antd `suffixIcon` \u2014 replaces the trailing chevron; `null` removes the indicator entirely (antd's own replacement for the deprecated showArrow). While a value is clearable the \u2715 owns that seat, as in antd."},{name:"placement",type:'"bottomStart" | "bottomEnd" | "topStart" | "topEnd"',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 \u2014 what a picker wants."},{name:"popupRender",type:"(originNode: React.ReactNode) => React.ReactNode",description:"antd `popupRender` \u2014 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:"listHeight",type:"number",description:"antd `listHeight` \u2014 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:"onPopupScroll",type:"(event: React.UIEvent<HTMLElement>) => void",description:"antd `onPopupScroll` \u2014 fires on the option list's own scroll. It runs BESIDE the built-in infinite scroll (loadOptions paging), never instead of it."},{name:"optionFilterProp",type:'"label" | "value" | "sublabel"',description:"antd `optionFilterProp` \u2014 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:"tokenSeparators",type:"string[]",description:"antd `tokenSeparators` (multiple/tags) \u2014 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:"maxTagTextLength",type:"number",description:"antd `maxTagTextLength` (multiple/tags) \u2014 cut each chip's TEXT to this many characters (an ellipsis marks the cut). The value keeps its whole label."},{name:"tagRender",type:"(props: { value: string; label: React.ReactNode; onClose: () => void; index: number; disabled: boolean }) => React.ReactNode",description:"antd `tagRender` (multiple/tags) \u2014 render one chip yourself. Exactly the shape TagInput's tagRender takes. The onClose handed in is the same remover the built-in \u2715 calls, so a custom chip cannot end up unremovable; supplying tagRender withdraws the built-in \u2715."},{name:"onSelect / onDeselect",type:"(value: string, option: SearchSelectOptionProp) => void",description:"antd `onSelect` / `onDeselect` \u2014 fires as one option JOINS or LEAVES the selection, beside onValueChange (which reports the whole value)."},{name:"maxTagPlaceholder",type:"React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)",description:"antd `maxTagPlaceholder` \u2014 the node standing in for the values maxTagCount hid. A function receives the omitted values, so '+3 \u4EF6' or a tooltip listing them is possible."}],usage:["DO use the data-driven API (options/loadOptions) for straightforward selects \u2014 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.","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 \u2014 the trigger's visible text is the option LABEL (\u6771\u4EAC\u672C\u793E), 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.","DO name the control with FormField, aria-label, or a <label htmlFor> pointing at the trigger id \u2014 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 \u2014 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 \u2014 a Select with no label of any kind is anonymous. Wrapping in <FormField label=\u2026> 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 \u2014 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 \u2192 SelectTrigger (contains SelectValue) \u2192 SelectContent \u2192 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 \u2014 e.g. opening it programmatically after a validation error \u2014 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 \u2014 it differs from disabled: the control stays focusable and its value still submits. clearable is ignored while readOnly.","DO use filterOption (searchable mode, static options) when the default label/value substring match isn't right \u2014 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 \u2014 icon-only, or one with its own affordance \u2014 instead of hiding [data-slot=select-chevron] with consumer CSS."],useCases:["Status filter on an invoice list \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 pass clearable=false so the user cannot clear the field once set; pair with name='department_id' for Inertia useForm submission.","Async account picker whose API can fail \u2014 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."],related:["Segmented \u2014 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.","SearchSelect \u2014 the combobox engine Select delegates to when showSearch=true or loadOptions is set. Prefer Select with showSearch instead of reaching for SearchSelect directly (SearchSelect is now deprecated as a public API).","TreeSelect \u2014 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 \u2014 use Select (with the `showSearch` prop) for typeahead/autocomplete lookup patterns instead of the removed Autocomplete component.","RadioGroup \u2014 use instead of Select when there are 2-4 mutually exclusive choices that must all be visible at once without opening a popover.","Combobox (if present) \u2014 compound cmdk-powered combobox for free-text + suggestion; Select is for strict value lists only."],example:`import {
902
+ <SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u30FBID\u3067\u691C\u7D22" value={search} onSearch={setSearch} />`,storyPath:"data-entry/SearchInput.stories.tsx",rules:[23]},{name:"Select",subParts:["SelectContent","SelectGroup","SelectItem","SelectLabel","SelectScrollDownButton","SelectScrollUpButton","SelectSeparator","SelectTrigger","SelectValue"],group:"data-entry",tagline:"Polymorphic single-select: pass options/loadOptions for the data-driven (Ant-style) API, or compose sub-parts manually \u2014 never use a raw <select>.",props:[{name:"mode",type:'"multiple" | "tags"',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."},{name:"maxCount",type:"number",description:"Maximum selected options; selected items remain removable."},{name:"maxTagCount",type:"number",description:"Collapse extra selected labels."},{name:"options",type:"(SearchSelectOptionProp | { label: string; options: SearchSelectOptionProp[] })[]",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:"loadOptions",type:"(params: SearchSelectLoadParamsProp) => Promise<SearchSelectLoadResultProp>",description:"Async remote fetcher. Receives { query, page } (1-based). Must return { options, hasMore? }. Implies showSearch=true automatically."},{name:"showSearch",type:'boolean | { filterOption?: boolean | ((input, option) => boolean); optionFilterProp?: "label" | "value" | "sublabel"; filterSort?; searchValue?: string; onSearch?: (value: string) => void; autoClearSearchValue?: boolean }',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:"value",type:"string",defaultValue:'""',description:"Controlled selected value (data-driven API). Pass an empty string to represent no selection."},{name:"defaultValue",type:"string",description:"Uncontrolled initial value (data-driven API). The trigger shows the matching option's label at rest \u2014 including in searchable (showSearch) mode \u2014 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:"onValueChange",type:"(value: string, option?: SearchSelectOptionProp) => void",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) => \u2026` 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:"renderOption",type:"(option: SearchSelectOptionProp) => React.ReactNode",description:"Custom per-option renderer for the dropdown ROWS. Defaults to label + optional sublabel. Does not change the trigger \u2014 use `labelRender` for that."},{name:"labelRender",type:"(selected: { value: string; label: React.ReactNode; option?: SearchSelectOptionProp }) => React.ReactNode",description:"Custom renderer for the SELECTED value shown on the TRIGGER (`labelRender`) \u2014 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:"selectedLabel",type:"string",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:"selectedIcon",type:"React.ReactNode",description:"Leading icon shown on the trigger for the current value when its option isn't loaded yet (async preset) \u2014 the trigger counterpart of `selectedLabel`, so an edit form pre-filled from the server shows the avatar/flag at rest."},{name:"placeholder",type:"string",description:"Placeholder shown in the trigger when no value is selected."},{name:"searchPlaceholder",type:"string",description:"Placeholder inside the search input (combobox mode only)."},{name:"emptyMessage",type:"string",description:"Message rendered when the filtered list is empty."},{name:"loadingMessage",type:"string",description:"Message rendered while loadOptions is resolving."},{name:"errorMessage",type:"string",description:"Message rendered when an async loadOptions REJECTS \u2014 a distinct state from empty/loading. Defaults to a localized 'Couldn\u2019t load options'. The panel shows this instead of a blank surface or a misleading 'no results'."},{name:"clearable",type:"boolean",defaultValue:"false",description:"Show the clear \u2715 when a value is selected (data-driven API). Off by default, as antd `allowClear` \u2014 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:"clearLabel",type:"string",description:"Label for the clear row (data-driven combobox mode)."},{name:"disabled",type:"boolean",description:"Disables the entire select."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Searchable mode only (showSearch/loadOptions). Value is shown (and the clear affordance hidden) but the popover cannot be opened \u2014 no new pick, no search. Mirrors the Input/NumberInput readOnly contract: stays focusable and still submits its value, unlike disabled."},{name:"size",type:'"xs" | "sm" | "md" | "lg"',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:"width",type:'"full" | "auto" | "bounded"',defaultValue:'"full"',description:"Trigger width on the data-driven API (options / loadOptions), searchable or not \u2014 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:"open",type:"boolean",description:"Searchable mode only. Controlled popover open state (uncontrolled by default). Pair with onOpenChange."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Searchable mode only. Fires on every open/close attempt \u2014 including ones ignored because open is externally pinned \u2014 so a controlled consumer stays in sync."},{name:"search",type:"string",description:"Searchable mode only. Controlled search-box query (uncontrolled by default). Pair with onSearchChange."},{name:"onSearchChange",type:"(query: string) => void",description:"Searchable mode only. Fires on every keystroke in the search box."},{name:"filterOption",type:"(option: SearchSelectOptionProp, query: string) => boolean",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:"renderError",type:"(params: { message: string; retry: () => void }) => React.ReactNode",description:"Searchable mode only. Custom error slot, overriding the default errorMessage row. retry() reloads from the first page."},{name:"renderLoadMore",type:"(params: { hasMore: boolean; loading: boolean; loadMore: () => void }) => React.ReactNode",description:"Searchable mode only. Custom 'load more' affordance appended below the list while another page is available \u2014 pairs with (does not replace) the built-in scroll-triggered pagination."},{name:"name",type:"string",description:"Form field name. Submits the selected value via a hidden input (data-driven API). Required for uncontrolled form submission."},{name:"id",type:"string",description:"HTML id for the trigger element. Wire to a <label htmlFor> for a11y."},{name:"className",type:"string",description:"Additional CSS classes applied to the trigger."},{name:"data-testid",type:"string",description:"Test id on the trigger. Option items get ${data-testid}-option-${value} automatically."},{name:"SelectTrigger size",type:'"sm" | "md"',defaultValue:'"md"',description:"Compound API only. Size variant on the SelectTrigger sub-component."},{name:"SelectTrigger width",type:'"full" | "auto"',defaultValue:'"full"',description:"Compound API only. `full` fills the field column (inside FormField). `auto` sizes the trigger to its label \u2014 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 showIndicator",type:"boolean",defaultValue:"true",description:"Compound API only. Set false to omit the built-in chevron disclosure indicator from the DOM entirely (not a CSS hide) \u2014 for specialized triggers (icon-only, etc.) that render their own affordance, so no consumer descendant CSS is needed."},{name:"status",type:'"error" | "warning"',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:"variant",type:'"outlined" | "filled" | "borderless"',description:"antd `variant` \u2014 the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once."},{name:"loading",type:"boolean",description:"antd `loading` \u2014 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:"defaultOpen",type:"boolean",description:"antd `defaultOpen` \u2014 uncontrolled initial popup state."},{name:"allowClear",type:"boolean | { clearIcon?: React.ReactNode; label?: string }",defaultValue:"false",description:"antd `allowClear` \u2014 the clear \u2715 on the trigger while a value is selected. Default false, as in antd. The object form replaces the \u2715 icon and/or its accessible label. Beats `clearable` when both are given."},{name:"onClear",type:"() => void",description:"antd `onClear` \u2014 fires after the value is cleared through the \u2715."},{name:"notFoundContent",type:"React.ReactNode",description:"antd `notFoundContent` \u2014 the node shown when the popup has nothing to list. Outranks the string-only emptyMessage."},{name:"autoClearSearchValue",type:"boolean",description:"antd `autoClearSearchValue` (default true) \u2014 clear the search box after a pick / on close. Set false to resume the same filtered list on the next open."},{name:"filterSort",type:"(a: SearchSelectOptionProp, b: SearchSelectOptionProp, info: { searchValue: string }) => number",description:"antd `filterSort` \u2014 orders what filterOption kept. Static options only; with loadOptions the server owns the order. Never mutates the caller's array."},{name:"optionRender",type:"(option: SearchSelectOptionProp, info: { index: number }) => React.ReactNode",description:"antd `optionRender` \u2014 per-option renderer in antd's own (option, { index }) shape. Outranks the older renderOption."},{name:"menuItemSelectedIcon",type:"React.ReactNode",description:"antd `menuItemSelectedIcon` \u2014 a decorative mark on the picked row. Off by default: the picked row is already marked by fill + weight, which costs no width."},{name:"popupMatchSelectWidth",type:"boolean | number",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:"fieldNames",type:"{ label?: string; value?: string; options?: string; groupLabel?: string; disabled?: string }",description:"antd `fieldNames` \u2014 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:"labelInValue",type:"boolean",description:"antd `labelInValue` \u2014 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:'\u6771\u4EAC\u672C\u793E'} renders the pick immediately, with no options page loaded and no flash of the raw id."},{name:"prefix",type:"React.ReactNode",description:"antd `prefix` \u2014 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:"suffixIcon",type:"React.ReactNode",description:"antd `suffixIcon` \u2014 replaces the trailing chevron; `null` removes the indicator entirely (antd's own replacement for the deprecated showArrow). While a value is clearable the \u2715 owns that seat, as in antd."},{name:"placement",type:'"bottomStart" | "bottomEnd" | "topStart" | "topEnd"',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 \u2014 what a picker wants."},{name:"popupRender",type:"(originNode: React.ReactNode) => React.ReactNode",description:"antd `popupRender` \u2014 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:"listHeight",type:"number",description:"antd `listHeight` \u2014 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:"onPopupScroll",type:"(event: React.UIEvent<HTMLElement>) => void",description:"antd `onPopupScroll` \u2014 fires on the option list's own scroll. It runs BESIDE the built-in infinite scroll (loadOptions paging), never instead of it."},{name:"optionFilterProp",type:'"label" | "value" | "sublabel"',description:"antd `optionFilterProp` \u2014 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:"tokenSeparators",type:"string[]",description:"antd `tokenSeparators` (multiple/tags) \u2014 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:"maxTagTextLength",type:"number",description:"antd `maxTagTextLength` (multiple/tags) \u2014 cut each chip's TEXT to this many characters (an ellipsis marks the cut). The value keeps its whole label."},{name:"tagRender",type:"(props: { value: string; label: React.ReactNode; onClose: () => void; index: number; disabled: boolean }) => React.ReactNode",description:"antd `tagRender` (multiple/tags) \u2014 render one chip yourself. Exactly the shape TagInput's tagRender takes. The onClose handed in is the same remover the built-in \u2715 calls, so a custom chip cannot end up unremovable; supplying tagRender withdraws the built-in \u2715."},{name:"onSelect / onDeselect",type:"(value: string, option: SearchSelectOptionProp) => void",description:"antd `onSelect` / `onDeselect` \u2014 fires as one option JOINS or LEAVES the selection, beside onValueChange (which reports the whole value)."},{name:"maxTagPlaceholder",type:"React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)",description:"antd `maxTagPlaceholder` \u2014 the node standing in for the values maxTagCount hid. A function receives the omitted values, so '+3 \u4EF6' or a tooltip listing them is possible."}],usage:["DO use the data-driven API (options/loadOptions) for straightforward selects \u2014 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.","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 \u2014 the trigger's visible text is the option LABEL (\u6771\u4EAC\u672C\u793E), 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.","DO name the control with FormField, aria-label, or a <label htmlFor> pointing at the trigger id \u2014 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 \u2014 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 \u2014 a Select with no label of any kind is anonymous. Wrapping in <FormField label=\u2026> 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 \u2014 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 \u2192 SelectTrigger (contains SelectValue) \u2192 SelectContent \u2192 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 \u2014 e.g. opening it programmatically after a validation error \u2014 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 \u2014 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 \u2014 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 \u2014 icon-only, or one with its own affordance \u2014 instead of hiding [data-slot=select-chevron] with consumer CSS."],useCases:["Status filter on an invoice list \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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."],related:["Segmented \u2014 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.","SearchSelect \u2014 the combobox engine Select delegates to when showSearch=true or loadOptions is set. Prefer Select with showSearch instead of reaching for SearchSelect directly (SearchSelect is now deprecated as a public API).","TreeSelect \u2014 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 \u2014 use Select (with the `showSearch` prop) for typeahead/autocomplete lookup patterns instead of the removed Autocomplete component.","RadioGroup \u2014 use instead of Select when there are 2-4 mutually exclusive choices that must all be visible at once without opening a popover.","Combobox (if present) \u2014 compound cmdk-powered combobox for free-text + suggestion; Select is for strict value lists only."],example:`import {
903
903
  Select,
904
904
  SelectContent,
905
905
  SelectGroup,
@@ -947,7 +947,6 @@ export function CurrencySelect({ value, onChange }) {
947
947
  ]}
948
948
  placeholder="Select currency"
949
949
  searchPlaceholder="Search currencies\u2026"
950
- clearable={false}
951
950
  name="currency"
952
951
  />
953
952
  );
@@ -1061,7 +1060,7 @@ export function BillingFields() {
1061
1060
  </FormField>
1062
1061
  </Flex>
1063
1062
  );
1064
- }`,storyPath:"data-entry/DatePicker.stories.tsx",rules:[3,6,13,31]},{name:"Dialog",subParts:["DialogAction","DialogBody","DialogCancel","DialogClose","DialogContent","DialogDescription","DialogFooter","DialogHeader","DialogOverlay","DialogPortal","DialogRoot","DialogTitle","DialogTrigger"],group:"feedback",tagline:"Compound modal. Controlled via open + onOpenChange. Parts available flat (DialogTrigger/DialogContent/\u2026) or as Dialog.Trigger/Dialog.Content. Rendered with role=dialog.",props:[{name:"open",type:"boolean",description:"Controlled open state."},{name:"defaultOpen",type:"boolean",description:"Initial open state when uncontrolled."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Open-state change handler."},{name:"variant",type:'"default" | "destructive"',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 \u2715 off, because a \u2715 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 \u2014 gh#567.'},{name:"modal",type:"boolean",defaultValue:"true",description:"Kept from the Radix era. react-aria's Modal always locks scroll and hides the background from assistive tech, so `false` no longer turns that off."}],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 \u2014 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`. Radix requires 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 \u2014 mirror of SheetBody."],useCases:["Inline form dialog \u2014 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 \u2014 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.","Wizard / multi-step flow \u2014 step through entity setup (legal entity \u2192 fiscal year \u2192 opening balances) using a single Dialog whose `DialogContent` conditionally renders different step panels. Control which step is shown in local state."],related:['AlertDialog \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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."],example:`import { useState } from "react";
1063
+ }`,storyPath:"data-entry/DatePicker.stories.tsx",rules:[3,6,13,31]},{name:"Dialog",subParts:["DialogAction","DialogBody","DialogCancel","DialogClose","DialogContent","DialogDescription","DialogFooter","DialogHeader","DialogOverlay","DialogPortal","DialogRoot","DialogTitle","DialogTrigger"],group:"feedback",tagline:"Compound modal. Controlled via open + onOpenChange. Parts available flat (DialogTrigger/DialogContent/\u2026) or as Dialog.Trigger/Dialog.Content. Rendered with role=dialog.",props:[{name:"open",type:"boolean",description:"Controlled open state."},{name:"defaultOpen",type:"boolean",description:"Initial open state when uncontrolled."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Open-state change handler."},{name:"variant",type:'"default" | "destructive"',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 \u2715 off, because a \u2715 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 \u2014 gh#567.'},{name:"modal",type:"boolean",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"` \u2014 an alertdialog is always modal. gh#696.'}],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 \u2014 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`. Radix requires 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 \u2014 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."],useCases:["Inline form dialog \u2014 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 \u2014 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 \u2014 `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 \u2014 step through entity setup (legal entity \u2192 fiscal year \u2192 opening balances) using a single Dialog whose `DialogContent` conditionally renders different step panels. Control which step is shown in local state."],related:['AlertDialog \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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."],example:`import { useState } from "react";
1065
1064
  import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from "@godxjp/ui/feedback";
1066
1065
  import { Button } from "@godxjp/ui/general";
1067
1066
 
@@ -1126,7 +1125,7 @@ function ConfirmSettlement() {
1126
1125
  </AlertDialogPortal>
1127
1126
  </AlertDialogRoot>
1128
1127
  );
1129
- }`,storyPath:"feedback/AlertDialog.stories.tsx",rules:[23,3]},{name:"Sheet",subParts:["SheetBody","SheetClose","SheetContent","SheetDescription","SheetFooter","SheetHeader","SheetOverlay","SheetPortal","SheetTitle","SheetTrigger"],group:"feedback",tagline:"Side-panel drawer / responsive detail panel (Radix Dialog). Parts: Sheet/SheetTrigger/SheetContent(side=right|left|top|bottom, responsive=auto|side|bottom)/SheetHeader/SheetBody/SheetTitle/SheetFooter.",props:[{name:"open",type:"boolean",description:"Controlled open state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Open-state change handler."},{name:"width",type:"number | string",description:"On SheetContent (side left/right): desired panel width (number\u2192px). Default w-3/4 sm:max-w-md."},{name:"responsive",type:'"auto" | "side" | "bottom"',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:"title / subtitle / extra / tone",type:"ReactNode / ReactNode / ReactNode / ToneProp",description:"On SheetHeader (Ant-style): title (\u2192 SheetTitle, accessible name), subtitle (\u2192 SheetDescription), right-aligned extra actions, and a soft semantic `tone` background band. Children still supported."}],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 \u2014 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 \u2014 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\u2192Sheet 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 \u2014 maps to Radix DialogPrimitive.Title, 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 \u2014 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 \u2014 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 \u2014 they will not stick to the panel bottom.","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.","DON'T put a Sheet inside a Dialog (nested Radix portals 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 \u2014 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 \u2014 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.","Mobile-first navigation drawer: side='left' sheet acting as a slide-in nav menu on small viewports when the AppShell Sidebar is hidden \u2014 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."],related:["Dialog \u2014 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 \u2014 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 \u2014 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 \u2014 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."],example:`import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from "@godxjp/ui/feedback";
1128
+ }`,storyPath:"feedback/AlertDialog.stories.tsx",rules:[23,3]},{name:"Sheet",subParts:["SheetBody","SheetClose","SheetContent","SheetDescription","SheetFooter","SheetHeader","SheetOverlay","SheetPortal","SheetTitle","SheetTrigger"],group:"feedback",tagline:"Side-panel drawer / responsive detail panel (Radix Dialog). Parts: Sheet/SheetTrigger/SheetContent(side=right|left|top|bottom, responsive=auto|side|bottom)/SheetHeader/SheetBody/SheetTitle/SheetFooter.",props:[{name:"open",type:"boolean",description:"Controlled open state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Open-state change handler."},{name:"modal",type:"boolean",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:"width",type:"number | string",description:"On SheetContent (side left/right): desired panel width (number\u2192px). Default w-3/4 sm:max-w-md."},{name:"responsive",type:'"auto" | "side" | "bottom"',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:"title / subtitle / extra / tone",type:"ReactNode / ReactNode / ReactNode / ToneProp",description:"On SheetHeader (Ant-style): title (\u2192 SheetTitle, accessible name), subtitle (\u2192 SheetDescription), right-aligned extra actions, and a soft semantic `tone` background band. Children still supported."}],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 \u2014 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 \u2014 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\u2192Sheet 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 \u2014 maps to Radix DialogPrimitive.Title, 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 \u2014 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 \u2014 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 \u2014 they will not stick to the panel bottom.","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 \u2715 or a footer close action. Escape closes it only while focus is inside the panel.","DON'T put a Sheet inside a Dialog (nested Radix portals 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 \u2014 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 \u2014 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 \u2014 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."],related:["Dialog \u2014 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 \u2014 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 \u2014 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 \u2014 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."],example:`import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from "@godxjp/ui/feedback";
1130
1129
  import { Button } from "@godxjp/ui/general";
1131
1130
 
1132
1131
  <Sheet open={open} onOpenChange={setOpen}>
@@ -1257,7 +1256,7 @@ export function CutoffTimeForm() {
1257
1256
  <Button type="submit">Save</Button>
1258
1257
  </form>
1259
1258
  );
1260
- }`,storyPath:"data-entry/TimePicker.stories.tsx",rules:[3,6,13,23]},{name:"Cascader",group:"data-entry",tagline:"Multi-level hierarchical path picker (Popover + cascading columns); value is always a string[] path, never a flat ID \u2014 passing a bare string breaks it.",props:[{name:"name",type:"string",description:"Native form field name; repeated values for multiple paths."},{name:"readOnly",type:"boolean",description:"Prevent edits while preserving the displayed value."},{name:"options",type:"TreeOptionProp[]",required:!0,description:"The hierarchical option tree. Each node has { value: string; label: ReactNode; disabled?: boolean; isLeaf?: boolean; children?: TreeOptionProp[] }. Normalised internally via fieldNames."},{name:"value",type:"string[] | string[][]",description:"Controlled value. Single mode: string[] path (e.g. ['vn','hcm','q1']). Multiple mode: string[][] array of paths."},{name:"defaultValue",type:"string[] | string[][]",description:"Initial value for uncontrolled mode. Same shape as value."},{name:"onValueChange",type:"(value: string[] | string[][], selectedOptions?: TreeOptionProp[] | TreeOptionProp[][]) => void",description:"Fires when selection changes. First arg is the selected path(s); second is the matching node objects. On clear, called with []."},{name:"multiple",type:"boolean",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:"changeOnSelect",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:"showSearch",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:"placeholder",type:"string",description:"Trigger button placeholder text when no value is selected. Defaults to the i18n key dataEntry.cascader.placeholder."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disables the trigger button and prevents the popover from opening."},{name:"expandTrigger",type:'"click" | "hover"',defaultValue:'"click"',description:"How child columns are expanded. 'hover' expands on mouseenter and collapses back on mouseleave."},{name:"fieldNames",type:"TreeFieldNamesProp",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:"allowClear",type:"boolean",defaultValue:"true",description:"Shows an X icon on the trigger when a value is selected. Clicking it calls onChange([]) and resets to placeholder."},{name:"className",type:"string",description:"Extra Tailwind classes applied to the trigger button."},{name:"id",type:"string",description:"HTML id forwarded to the trigger button. Use to associate a <label htmlFor>."},{name:"status",type:'"error" | "warning"',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:"variant",type:'"outlined" | "filled" | "borderless"',description:"antd `variant` \u2014 the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once."},{name:"loading",type:"boolean",description:"antd `loading` \u2014 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:"size",type:'"xs" | "sm" | "md" | "lg"',description:"antd `size` \u2014 height tier on the shared --control-height ladder."},{name:"open",type:"boolean",description:"antd `open` \u2014 controlled panel state. The consumer is the authority: nothing internal closes a pinned panel."},{name:"defaultOpen",type:"boolean",description:"antd `defaultOpen` \u2014 uncontrolled initial panel state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"antd `onOpenChange` \u2014 fires for both controlled and uncontrolled panels."},{name:"maxTagCount",type:"number",description:'antd `maxTagCount` \u2014 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:"maxTagPlaceholder",type:"React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)",description:"antd `maxTagPlaceholder` \u2014 the node standing in for what maxTagCount hid. Defaults to a localized `+N`."},{name:"notFoundContent",type:"React.ReactNode",description:"antd `notFoundContent` \u2014 the node shown when the popup has nothing to list. Outranks the string-only emptyMessage."},{name:"autoClearSearchValue",type:"boolean",description:"antd `autoClearSearchValue` (default true) \u2014 clear the search box after a pick / on close. Set false to resume the same filtered list on the next open."},{name:"showCheckedStrategy",type:'"SHOW_CHILD" | "SHOW_PARENT"',description:"antd `showCheckedStrategy` (multiple only). SHOW_PARENT collapses a fully-checked branch into the branch itself; SHOW_CHILD (default) lists the leaves."},{name:"loadData",type:"(selectedOptions: TreeOptionProp[]) => void | Promise<void>",description:"antd `loadData` \u2014 lazy children. Called ONCE per branch that has no children and isLeaf !== true, the first time it is expanded; push the fetched children into options."},{name:"displayRender",type:"(labels: string[], selectedOptions?: TreeOptionProp[]) => React.ReactNode",description:"antd `displayRender` \u2014 owns the trigger label built from the selected path."},{name:"optionRender",type:"(option: TreeOptionProp) => React.ReactNode",description:"antd `optionRender` \u2014 owns a column row's body. The checkbox, check mark and chevron stay with the component."},{name:"search",type:"string",description:"Controlled search query (antd `showSearch.searchValue`)."},{name:"onSearchChange",type:"(query: string) => void",description:"Search query change (antd `showSearch.onSearch`)."}],usage:["DO pass a string[] path as value in single mode (e.g. ['country','region','city']). DON'T pass a flat string ID \u2014 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 \u2014 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} \u2014 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 \u2192 Prefecture \u2192 City) for address or branch-office pickers in accounting or logistics forms.","Expense category selection (e.g. Operating Expenses \u2192 Marketing \u2192 Digital Ads) where the full classification path is required for the general ledger.","Product taxonomy navigation (Department \u2192 Category \u2192 Sub-category) in inventory or invoice line-item entry.","Organisational unit picker (Company \u2192 Division \u2192 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 \u2014 not just the leaf value."],related:["TreeSelect \u2014 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 \u2014 use for a flat (non-hierarchical) list of options. Cascader is only needed when items have meaningful parent\u2013child levels.","Transfer \u2014 use when the user needs to shuttle multiple items between two panels; not for hierarchical path selection."],example:`{\`import { Cascader } from "@godxjp/ui/data-entry";
1259
+ }`,storyPath:"data-entry/TimePicker.stories.tsx",rules:[3,6,13,23]},{name:"Cascader",group:"data-entry",tagline:"Multi-level hierarchical path picker (Popover + cascading columns); value is always a string[] path, never a flat ID \u2014 passing a bare string breaks it.",props:[{name:"name",type:"string",description:"Native form field name; repeated values for multiple paths."},{name:"readOnly",type:"boolean",description:"Prevent edits while preserving the displayed value."},{name:"options",type:"TreeOptionProp[]",required:!0,description:"The hierarchical option tree. Each node has { value: string; label: ReactNode; disabled?: boolean; isLeaf?: boolean; children?: TreeOptionProp[] }. Normalised internally via fieldNames."},{name:"value",type:"string[] | string[][]",description:"Controlled value. Single mode: string[] path (e.g. ['vn','hcm','q1']). Multiple mode: string[][] array of paths."},{name:"defaultValue",type:"string[] | string[][]",description:"Initial value for uncontrolled mode. Same shape as value."},{name:"onValueChange",type:"(value: string[] | string[][], selectedOptions?: TreeOptionProp[] | TreeOptionProp[][]) => void",description:"Fires when selection changes. First arg is the selected path(s); second is the matching node objects. On clear, called with []."},{name:"multiple",type:"boolean",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:"changeOnSelect",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:"showSearch",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:"placeholder",type:"string",description:"Trigger button placeholder text when no value is selected. Defaults to the i18n key dataEntry.cascader.placeholder."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disables the trigger button and prevents the popover from opening."},{name:"expandTrigger",type:'"click" | "hover"',defaultValue:'"click"',description:"How child columns are expanded. 'hover' expands on mouseenter and collapses back on mouseleave."},{name:"fieldNames",type:"TreeFieldNamesProp",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:"allowClear",type:"boolean",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:"className",type:"string",description:"Extra Tailwind classes applied to the trigger button."},{name:"id",type:"string",description:"HTML id forwarded to the trigger button. Use to associate a <label htmlFor>."},{name:"status",type:'"error" | "warning"',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:"variant",type:'"outlined" | "filled" | "borderless"',description:"antd `variant` \u2014 the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once."},{name:"loading",type:"boolean",description:"antd `loading` \u2014 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:"size",type:'"xs" | "sm" | "md" | "lg"',description:"antd `size` \u2014 height tier on the shared --control-height ladder."},{name:"open",type:"boolean",description:"antd `open` \u2014 controlled panel state. The consumer is the authority: nothing internal closes a pinned panel."},{name:"defaultOpen",type:"boolean",description:"antd `defaultOpen` \u2014 uncontrolled initial panel state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"antd `onOpenChange` \u2014 fires for both controlled and uncontrolled panels."},{name:"maxTagCount",type:"number",description:'antd `maxTagCount` \u2014 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:"maxTagPlaceholder",type:"React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)",description:"antd `maxTagPlaceholder` \u2014 the node standing in for what maxTagCount hid. Defaults to a localized `+N`."},{name:"notFoundContent",type:"React.ReactNode",description:"antd `notFoundContent` \u2014 the node shown when the popup has nothing to list. Outranks the string-only emptyMessage."},{name:"autoClearSearchValue",type:"boolean",description:"antd `autoClearSearchValue` (default true) \u2014 clear the search box after a pick / on close. Set false to resume the same filtered list on the next open."},{name:"showCheckedStrategy",type:'"SHOW_CHILD" | "SHOW_PARENT"',description:"antd `showCheckedStrategy` (multiple only). SHOW_PARENT collapses a fully-checked branch into the branch itself; SHOW_CHILD (default) lists the leaves."},{name:"loadData",type:"(selectedOptions: TreeOptionProp[]) => void | Promise<void>",description:"antd `loadData` \u2014 lazy children. Called ONCE per branch that has no children and isLeaf !== true, the first time it is expanded; push the fetched children into options."},{name:"displayRender",type:"(labels: string[], selectedOptions?: TreeOptionProp[]) => React.ReactNode",description:"antd `displayRender` \u2014 owns the trigger label built from the selected path."},{name:"optionRender",type:"(option: TreeOptionProp) => React.ReactNode",description:"antd `optionRender` \u2014 owns a column row's body. The checkbox, check mark and chevron stay with the component."},{name:"search",type:"string",description:"Controlled search query (antd `showSearch.searchValue`)."},{name:"onSearchChange",type:"(query: string) => void",description:"Search query change (antd `showSearch.onSearch`)."}],usage:["DO pass a string[] path as value in single mode (e.g. ['country','region','city']). DON'T pass a flat string ID \u2014 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 \u2014 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} \u2014 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 \u2192 Prefecture \u2192 City) for address or branch-office pickers in accounting or logistics forms.","Expense category selection (e.g. Operating Expenses \u2192 Marketing \u2192 Digital Ads) where the full classification path is required for the general ledger.","Product taxonomy navigation (Department \u2192 Category \u2192 Sub-category) in inventory or invoice line-item entry.","Organisational unit picker (Company \u2192 Division \u2192 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 \u2014 not just the leaf value."],related:["TreeSelect \u2014 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 \u2014 use for a flat (non-hierarchical) list of options. Cascader is only needed when items have meaningful parent\u2013child levels.","Transfer \u2014 use when the user needs to shuttle multiple items between two panels; not for hierarchical path selection."],example:`{\`import { Cascader } from "@godxjp/ui/data-entry";
1261
1260
 
1262
1261
  const REGIONS = [
1263
1262
  {
@@ -1333,7 +1332,7 @@ function MultiRegionPicker() {
1333
1332
  changeOnSelect
1334
1333
  onValueChange={(v) => console.log("path", v)}
1335
1334
  />
1336
- \`}`,storyPath:"data-entry/Cascader.stories.tsx",rules:[3,6,23,31]},{name:"TreeSelect",group:"data-entry",tagline:"Hierarchical tree picker in a Popover (single or multi-select with checkboxes) \u2014 `onValueChange` receives `string` in single mode and `string[]` in multi/checkable mode; never use a raw `<select>` for tree-structured data.",props:[{name:"name",type:"string",description:"Native form field name; repeated values for multiple selections."},{name:"readOnly",type:"boolean",description:"Prevent edits while preserving value."},{name:"treeData",type:"TreeOptionProp[]",required:!0,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:"value",type:"string | string[]",description:"Controlled selected value(s). Pass `string` in single mode, `string[]` in multi/checkable mode. When undefined the component is uncontrolled."},{name:"defaultValue",type:"string | string[]",description:"Initial value for uncontrolled usage. Ignored once `value` is provided."},{name:"onValueChange",type:"(value: string | string[] | undefined) => void",description:"Called on selection change. Returns `string` in single mode, `string[]` in multi/checkable mode, or `undefined` when cleared."},{name:"multiple",type:"boolean",defaultValue:"false",description:"Enable multi-select without checkboxes. When true, `onValueChange` always fires with `string[]`."},{name:"treeCheckable",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:"treeCheckStrictly",type:"boolean",defaultValue:"false",description:"When true (only with `treeCheckable`), parent and child selections are independent \u2014 checking a parent does NOT auto-check its children."},{name:"showCheckedStrategy",type:'"SHOW_CHILD" | "SHOW_PARENT" | "SHOW_ALL"',defaultValue:'"SHOW_CHILD"',description:"Controls which values appear in the trigger label when checkboxes are used. `SHOW_CHILD` (default) \u2014 show only leaf nodes selected; `SHOW_PARENT` \u2014 show nearest ancestor when all children selected; `SHOW_ALL` \u2014 show every checked node. Use the exported constants `TreeSelect.SHOW_CHILD`, `TreeSelect.SHOW_PARENT`, `TreeSelect.SHOW_ALL` instead of raw strings."},{name:"showSearch",type:"boolean",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:"treeDefaultExpandAll",type:"boolean",defaultValue:"false",description:"Expand all nodes when the dropdown first opens. Initialised once; does not re-expand on re-render."},{name:"placeholder",type:"string",description:"Trigger button placeholder text when nothing is selected. Defaults to the i18n key `dataEntry.treeSelect.placeholder`."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disables the trigger button and all interactions."},{name:"allowClear",type:"boolean",defaultValue:"true",description:"Show an `X` icon in the trigger to clear the selection. Set to `false` to make selection mandatory."},{name:"className",type:"string",description:"Additional Tailwind classes applied to the trigger Button."},{name:"id",type:"string",description:"HTML `id` placed on the trigger Button \u2014 use this to associate a `<label htmlFor>` for accessibility."},{name:"aria-label",type:"string",description:"Accessible name for the combobox trigger when no visible label is available."},{name:"aria-errormessage",type:"string",description:"ID of the element containing the current validation error message."},{name:"aria-invalid",type:"boolean | 'true' | 'false'",description:"Marks the semantic combobox trigger invalid for assistive technology."},{name:"aria-required",type:"boolean | 'true' | 'false'",description:"Marks the semantic combobox trigger required for assistive technology."},{name:"fieldNames",type:"{ label?: string; value?: string; children?: string }",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:"status",type:'"error" | "warning"',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:"variant",type:'"outlined" | "filled" | "borderless"',description:"antd `variant` \u2014 the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once."},{name:"loading",type:"boolean",description:"antd `loading` \u2014 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:"size",type:'"xs" | "sm" | "md" | "lg"',description:"antd `size` \u2014 height tier on the shared --control-height ladder."},{name:"open",type:"boolean",description:"antd `open` \u2014 controlled panel state. The consumer is the authority: nothing internal closes a pinned panel."},{name:"defaultOpen",type:"boolean",description:"antd `defaultOpen` \u2014 uncontrolled initial panel state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"antd `onOpenChange` \u2014 fires for both controlled and uncontrolled panels."},{name:"maxTagCount",type:"number",description:'antd `maxTagCount` \u2014 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:"maxTagPlaceholder",type:"React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)",description:"antd `maxTagPlaceholder` \u2014 the node standing in for what maxTagCount hid. Defaults to a localized `+N`."},{name:"notFoundContent",type:"React.ReactNode",description:"antd `notFoundContent` \u2014 the node shown when the popup has nothing to list. Outranks the string-only emptyMessage."},{name:"autoClearSearchValue",type:"boolean",description:"antd `autoClearSearchValue` (default true) \u2014 clear the search box after a pick / on close. Set false to resume the same filtered list on the next open."},{name:"loadData",type:"(node: TreeOptionProp) => void | Promise<void>",description:"antd `loadData` \u2014 lazy children. Called ONCE per node that has no children and isLeaf !== true, the first time it is expanded; push the fetched children into treeData. Such a node still reads as expandable (aria-expanded + a working expander)."},{name:"treeTitleRender",type:"(node: TreeOptionProp) => React.ReactNode",description:"antd `treeTitleRender` \u2014 owns a node's title only; the checkbox, expander and row ARIA stay with the component."},{name:"search",type:"string",description:"Controlled search query (antd `showSearch.searchValue`)."},{name:"onSearchChange",type:"(query: string) => void",description:"Search query change (antd `showSearch.onSearch`)."}],usage:["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'>` \u2014 not a native `<select>` \u2014 so an explicit label is required.","DO use `treeCheckable` (+ optionally `showCheckedStrategy`) for selecting multiple nodes with parent\u2013child 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 \u2014 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 \u2014 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."],related:["Select \u2014 flat single/multi picker; use when data has no parent-child hierarchy. Pick TreeSelect as soon as items have `children`.","Cascader \u2014 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\u2192Region\u2192City). Pick TreeSelect when the user may select any node at any level or needs checkboxes.","Checkbox / CheckboxGroup \u2014 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 \u2014 low-level search primitive; TreeSelect already embeds this internally. Do NOT compose your own tree dropdown out of Command \u2014 use TreeSelect."],example:`import { useState } from "react";
1335
+ \`}`,storyPath:"data-entry/Cascader.stories.tsx",rules:[3,6,23,31]},{name:"TreeSelect",group:"data-entry",tagline:"Hierarchical tree picker in a Popover (single or multi-select with checkboxes) \u2014 `onValueChange` receives `string` in single mode and `string[]` in multi/checkable mode; never use a raw `<select>` for tree-structured data.",props:[{name:"name",type:"string",description:"Native form field name; repeated values for multiple selections."},{name:"readOnly",type:"boolean",description:"Prevent edits while preserving value."},{name:"treeData",type:"TreeOptionProp[]",required:!0,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:"value",type:"string | string[]",description:"Controlled selected value(s). Pass `string` in single mode, `string[]` in multi/checkable mode. When undefined the component is uncontrolled."},{name:"defaultValue",type:"string | string[]",description:"Initial value for uncontrolled usage. Ignored once `value` is provided."},{name:"onValueChange",type:"(value: string | string[] | undefined) => void",description:"Called on selection change. Returns `string` in single mode, `string[]` in multi/checkable mode, or `undefined` when cleared."},{name:"multiple",type:"boolean",defaultValue:"false",description:"Enable multi-select without checkboxes. When true, `onValueChange` always fires with `string[]`."},{name:"treeCheckable",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:"treeCheckStrictly",type:"boolean",defaultValue:"false",description:"When true (only with `treeCheckable`), parent and child selections are independent \u2014 checking a parent does NOT auto-check its children."},{name:"showCheckedStrategy",type:'"SHOW_CHILD" | "SHOW_PARENT" | "SHOW_ALL"',defaultValue:'"SHOW_CHILD"',description:"Controls which values appear in the trigger label when checkboxes are used. `SHOW_CHILD` (default) \u2014 show only leaf nodes selected; `SHOW_PARENT` \u2014 show nearest ancestor when all children selected; `SHOW_ALL` \u2014 show every checked node. Use the exported constants `TreeSelect.SHOW_CHILD`, `TreeSelect.SHOW_PARENT`, `TreeSelect.SHOW_ALL` instead of raw strings."},{name:"showSearch",type:"boolean",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:"treeDefaultExpandAll",type:"boolean",defaultValue:"false",description:"Expand all nodes when the dropdown first opens. Initialised once; does not re-expand on re-render."},{name:"placeholder",type:"string",description:"Trigger button placeholder text when nothing is selected. Defaults to the i18n key `dataEntry.treeSelect.placeholder`."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disables the trigger button and all interactions."},{name:"allowClear",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:"className",type:"string",description:"Additional Tailwind classes applied to the trigger Button."},{name:"id",type:"string",description:"HTML `id` placed on the trigger Button \u2014 use this to associate a `<label htmlFor>` for accessibility."},{name:"aria-label",type:"string",description:"Accessible name for the combobox trigger when no visible label is available."},{name:"aria-errormessage",type:"string",description:"ID of the element containing the current validation error message."},{name:"aria-invalid",type:"boolean | 'true' | 'false'",description:"Marks the semantic combobox trigger invalid for assistive technology."},{name:"aria-required",type:"boolean | 'true' | 'false'",description:"Marks the semantic combobox trigger required for assistive technology."},{name:"fieldNames",type:"{ label?: string; value?: string; children?: string }",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:"status",type:'"error" | "warning"',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:"variant",type:'"outlined" | "filled" | "borderless"',description:"antd `variant` \u2014 the control surface. Default `outlined`. Drawn from --control-{surface,filled,borderless}-* tokens, so a theme retunes all three at once."},{name:"loading",type:"boolean",description:"antd `loading` \u2014 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:"size",type:'"xs" | "sm" | "md" | "lg"',description:"antd `size` \u2014 height tier on the shared --control-height ladder."},{name:"open",type:"boolean",description:"antd `open` \u2014 controlled panel state. The consumer is the authority: nothing internal closes a pinned panel."},{name:"defaultOpen",type:"boolean",description:"antd `defaultOpen` \u2014 uncontrolled initial panel state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"antd `onOpenChange` \u2014 fires for both controlled and uncontrolled panels."},{name:"maxTagCount",type:"number",description:'antd `maxTagCount` \u2014 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:"maxTagPlaceholder",type:"React.ReactNode | ((omitted: { value: string; label: React.ReactNode }[]) => React.ReactNode)",description:"antd `maxTagPlaceholder` \u2014 the node standing in for what maxTagCount hid. Defaults to a localized `+N`."},{name:"notFoundContent",type:"React.ReactNode",description:"antd `notFoundContent` \u2014 the node shown when the popup has nothing to list. Outranks the string-only emptyMessage."},{name:"autoClearSearchValue",type:"boolean",description:"antd `autoClearSearchValue` (default true) \u2014 clear the search box after a pick / on close. Set false to resume the same filtered list on the next open."},{name:"loadData",type:"(node: TreeOptionProp) => void | Promise<void>",description:"antd `loadData` \u2014 lazy children. Called ONCE per node that has no children and isLeaf !== true, the first time it is expanded; push the fetched children into treeData. Such a node still reads as expandable (aria-expanded + a working expander)."},{name:"treeTitleRender",type:"(node: TreeOptionProp) => React.ReactNode",description:"antd `treeTitleRender` \u2014 owns a node's title only; the checkbox, expander and row ARIA stay with the component."},{name:"search",type:"string",description:"Controlled search query (antd `showSearch.searchValue`)."},{name:"onSearchChange",type:"(query: string) => void",description:"Search query change (antd `showSearch.onSearch`)."}],usage:["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'>` \u2014 not a native `<select>` \u2014 so an explicit label is required.","DO use `treeCheckable` (+ optionally `showCheckedStrategy`) for selecting multiple nodes with parent\u2013child 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 \u2014 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 \u2014 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."],related:["Select \u2014 flat single/multi picker; use when data has no parent-child hierarchy. Pick TreeSelect as soon as items have `children`.","Cascader \u2014 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\u2192Region\u2192City). Pick TreeSelect when the user may select any node at any level or needs checkboxes.","Checkbox / CheckboxGroup \u2014 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 \u2014 low-level search primitive; TreeSelect already embeds this internally. Do NOT compose your own tree dropdown out of Command \u2014 use TreeSelect."],example:`import { useState } from "react";
1337
1336
  import { FormField, TreeSelect } from "@godxjp/ui/data-entry";
1338
1337
 
1339
1338
  const accountTree = [
@@ -4430,7 +4429,7 @@ A block with no reason is IGNORED and the finding stands. An unclosed block runs
4430
4429
  The class-shaped rules (gap-*/p-*/m-*, bg-<palette>-*, w-[\u2026], pr-*, dark:*) only read class
4431
4430
  expressions \u2014 a className/class attribute, a class-named binding (\`baseClass\`, \`statusStyles\`,
4432
4431
  \`badgeVariants\`) or a cn()/clsx()/cva() call \u2014 so prose that merely spells a utility is not a
4433
- finding and needs no suppression.`,$=[{id:"dialog-needs-body",severity:"error",category:"composition",standard:null,fix:"Wrap a Dialog/Sheet's middle in <DialogBody>/<SheetBody>. The scroll lives on the body \u2014 the content box is `overflow: hidden` with no max-height \u2014 so an overlay without one clips long content at BOTH ends and takes the footer's buttons with it, leaving Escape as the only way out. It only shows on real data, never on demo data (gh#617)."},{id:"no-utility-spacing",severity:"error",category:"composition",standard:null,fix:"Remove gap-*/p-*/m-* from your own markup; space siblings with <Flex gap> / <ResponsiveGrid>. Page sections are spaced by <PageContainer> (docs/CONSUMER-RULES.md \xA73)."},{id:"no-utility-layout",severity:"error",category:"composition",standard:null,fix:'Replace className="flex \u2026" / "grid \u2026" with <Flex> (row), <Flex direction="col"> (stack) or <ResponsiveGrid columns>.'},{id:"no-hand-rolled-surface",severity:"warn",category:"composition",standard:null,fix:"A rounded+border/bg div is a fake surface \u2014 use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from tokens. A read-only sample of a colour a USER chose is Swatch, which takes that value as a prop (gh#527)."},{id:"sibling-cards-need-flex",severity:"warn",category:"composition",standard:null,fix:'Wrap adjacent <Card>s in <Flex direction="col" gap="lg"> or <ResponsiveGrid>; direct children of PageContainer are spaced by the page already.'},{id:"no-raw-palette-color",severity:"error",category:"tokens",standard:null,fix:"Use semantic tokens (bg-primary, text-muted-foreground), never raw palette (bg-blue-500)."},{id:"no-arbitrary-hex",severity:"error",category:"tokens",standard:null,fix:"No hardcoded hex in className; read design-system color tokens."},{id:"no-arbitrary-spacing",severity:"error",category:"tokens",standard:null,fix:"No p-[13px]/gap-[7px]; use the token scale / <Flex gap> / <PageContainer>."},{id:"no-arbitrary-size",severity:"error",category:"tokens",standard:null,fix:"No w-[37px]/h-[260px]; use token sizes or a sizing prop (min-w-[\u2026] allowed)."},{id:"no-arbitrary-typography",severity:"error",category:"tokens",standard:null,fix:"No text-[20px]/leading-[1.7]; use the golden-ratio type-scale tokens."},{id:"no-arbitrary-radius",severity:"error",category:"tokens",standard:null,fix:"No rounded-[6px]; use rounded-sm/md/lg radius tokens."},{id:"no-off-scale-token-value",severity:"warn",category:"tokens",standard:null,fix:"A design-system knob you override takes a step (style={{ '--card-space-inset': 'var(--space-4)' }}) or a calc() from one (calc(var(--space-4) + 2px)), not a raw 13px. Only axes that HAVE a scale count: space/padding/gap/margin, font-size, radius, icon-size (width/height/size/offset have none yet, so a number there is fine). A value that is genuinely off the grid keeps its literal and says why in place, with a /* scale-exempt: 6px status dot, below --space-1 */ comment on that line or the one above."},{id:"no-dark-color-override",severity:"warn",category:"tokens",standard:null,fix:"Drop dark: color overrides \u2014 semantic tokens already adapt."},{id:"raw-white-black",severity:"warn",category:"tokens",standard:null,fix:"Prefer semantic tokens (text-primary-foreground, bg-background) over raw white/black."},{id:"no-domain-tracking-token",severity:"error",category:"tokens",standard:null,fix:"No package-tracking/domain tokens; use semantic tokens or app theme overrides."},{id:"no-space-xy",severity:"error",category:"tokens",standard:null,fix:"Use <Flex gap> instead of space-x/y-*."},{id:"no-raw-select",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Select> from @godxjp/ui, not a raw <select>."},{id:"no-raw-table",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use the <Table>/<DataTable> family, not a raw <table>."},{id:"no-raw-input",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Input> from @godxjp/ui, not a raw <input>."},{id:"no-raw-textarea",severity:"warn",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Textarea> from @godxjp/ui, not a raw <textarea>."},{id:"no-raw-button",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Button> from @godxjp/ui, not a raw <button>."},{id:"card-manual-padding",severity:"error",category:"composition",standard:null,fix:"Wrap the body in <CardContent>; don't hand-roll padding on <Card>."},{id:"card-needs-content",severity:"error",category:"composition",standard:null,fix:"<Card> body must be in <CardContent> (no padding otherwise); flush only for a full-bleed table."},{id:"bare-control-needs-formfield",severity:"warn",category:"composition",standard:"WCAG 2.2 SC 1.3.1 \xB7 3.3.2 \xB7 @godxjp/ui FormField (cardinal rule 227)",fix:"Wrap a labelled control in <FormField label=\u2026> \u2014 it owns label\u2194control id wiring, aria/error, AND the field rhythm; never pair a bare <Label> with an <Input>."},{id:"manual-field-error",severity:"warn",category:"composition",standard:"WCAG 2.2 SC 3.3.1",fix:"Use <FormField error=\u2026>, not a hand-rolled <p class='text-destructive'>."},{id:"manual-field-helper",severity:"warn",category:"composition",standard:null,fix:"Use <FormField helper=\u2026>, not a hand-rolled helper <p>."},{id:"status-tone-not-variant",severity:"error",category:"api",standard:null,fix:"Badge/Tag/StatCard status uses tone, not variant (variant is structural)."},{id:"value-callback-on-value-change",severity:"error",category:"api",standard:null,fix:"Abstract value components use onValueChange, not onChange."},{id:"icon-button-needs-name",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 4.1.2 \xB7 1.1.1 \xB7 WAI-ARIA 1.2",fix:"Add aria-label={t('\u2026')} to <Button size='icon'>; the glyph is aria-hidden."},{id:"img-needs-alt",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 1.1.1 \xB7 HTML Living Standard",fix:"Add alt to every <img> (alt='' if decorative); prefer <Avatar>/<AspectRatio>."},{id:"no-positive-tabindex",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 2.4.3 \xB7 WAI-ARIA APG",fix:"Use tabIndex 0 or -1 only; never positive \u2014 it breaks focus order."},{id:"hand-rolled-close-glyph",severity:"warn",category:"a11y",standard:"WAI-ARIA 1.2 (dialog) \xB7 WCAG 2.2 SC 4.1.2",fix:"Pass onDismiss to <Alert>, or use <Dialog>/<Sheet>'s built-in labelled close \u2014 not a bare \u2715."},{id:"no-emoji-in-ui",severity:"warn",category:"i18n",standard:"Unicode UTS #51 \xB7 WCAG 2.2 SC 1.1.1",fix:"No emoji in product UI; quiet i18n copy + Lucide icon + Badge tone."},{id:"no-emoji-flag",severity:"warn",category:"i18n",standard:"ISO 3166-1 \xB7 ECMA-402 Intl.DisplayNames \xB7 Unicode UTS #51",fix:"Derive country names from Intl.DisplayNames; no emoji flags."},{id:"hardcoded-currency",severity:"warn",category:"i18n",standard:"ISO 4217 \xB7 ECMA-402 Intl.NumberFormat",fix:"Format money with Intl.NumberFormat({ style: 'currency', currency }), not \xA5{amount}."},{id:"raw-intl-date",severity:"warn",category:"i18n",standard:"ISO 8601 \xB7 IANA tz \xB7 ECMA-402 Intl.DateTimeFormat",fix:"Use formatDate from @godxjp/ui/datetime, not hand-built or locale-default dates."},{id:"no-physical-direction",severity:"warn",category:"rtl",standard:"W3C CSS Logical Properties L1 \xB7 WCAG 2.2 (1.3.2)",fix:"Use logical utilities (ms-/me-/ps-/pe-, start-/end-, text-start/end, border-s/e, rounded-s/e)."},{id:"no-em-dash-in-copy",severity:"warn",category:"copy",standard:"@godxjp/ui reference-design typography",fix:"No em-dash (\u2014) in copy; use a middot \xB7 or two calm sentences."}];function J(e){return e?$.filter(t=>t.category===e):$}var ee="node node_modules/@godxjp/ui/scripts/visual-audit.mjs <baseUrl> [route \u2026] (optional peers, TESTED range: playwright >=1.55 <2 [1.61.1] + @axe-core/playwright >=4.10 <5 [4.12.1] + axe-core >=4.10 <5 [4.12.1] + a chromium via `playwright install chromium`; --strict for a CI gate, --format json ALWAYS emits valid JSON with a status of ok|partial|error separating infra errors[] from product findings[], --rules to print this catalog)",Z=[{id:"css-layers-missing",severity:"error",category:"layout",standard:"@godxjp/ui styles contract (styles / styles/core are the only entries)",fix:"Import `@godxjp/ui/styles` (or `styles/core` without fonts); never cherry-pick *-layout.css \u2014 a missing layer renders naked menus and unsized Select rows."},{id:"control-height-mismatch",severity:"error",category:"layout",standard:"@godxjp/ui control tier (--control-height) \xB7 Nielsen consistency heuristic",fix:"Every control in one row must share --control-height; replace hand-rolled pills with Avatar/Button/Badge, never restyle a control's height."},{id:"sibling-card-gap",severity:"error",category:"layout",standard:"@godxjp/ui spacing scale (docs/SPACING.md)",fix:'Adjacent Cards need one space step between them \u2014 <Flex direction="col" gap>, <ResponsiveGrid>, or direct children of PageContainer.'},{id:"row-content-starved",severity:"warn",category:"layout",standard:"WCAG 2.2 SC 1.4.10 reflow",fix:`A sibling (a w-full SelectTrigger) takes the row's width and truncates its neighbours \u2014 give the Select width="auto" or move it out of the row.`},{id:"axe-violations",severity:"warn",category:"a11y",standard:"WCAG 2.2 A/AA \xB7 WAI-ARIA 1.2 (axe-core engine)",fix:"Fix each axe node \u2014 contrast (1.4.3), name/role/value (4.1.2), ARIA, landmarks. Runs on the REAL DOM, catching what static analysis cannot."},{id:"target-size-min",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 2.5.8 (24\xD724 AA) \xB7 2.5.5 (44\xD744 AAA)",fix:"Interactive targets must be \u226524\xD724 CSS px; size from the --control-height tier."},{id:"oversaturated-accent",severity:"warn",category:"color",standard:"@godxjp/ui reference-design \u6E0B\u307F (OKLCH chroma \u2264 0.18)",fix:"Desaturate brand/primary surfaces (OKLCH chroma \u2264 0.18); read --primary tokens, no raw vivid bars."},{id:"emoji-rendered",severity:"warn",category:"i18n",standard:"Unicode UTS #51 \xB7 WCAG 2.2 SC 1.1.1",fix:"Remove emoji from rendered product text; quiet i18n copy + Lucide icon + Badge tone."},{id:"alert-controls-misplaced",severity:"warn",category:"layout",standard:"@godxjp/ui Alert anatomy \xB7 WAI-ARIA 1.2 \xB7 WCAG 2.2 SC 4.1.2",fix:"Use <Alert>: one leading tone icon, <Alert.Actions> trailing-right normal width, onDismiss \xD7 top-right, one horizontal row."}];function te(e){return e?Z.filter(t=>t.category===e):Z}var p={name:"@godxjp/ui-mcp",version:"26.4.0",godxUiCompatibility:"26.4.x",description:"Model Context Protocol server for @godxjp/ui \u2014 gives Claude Code / Codex CLI / Cursor / any MCP-aware agent live access to the component catalog, prop vocabulary, design tokens, 45 cardinal rules, copy-paste-ready patterns, 12 design / taste skills synthesised from Leonxlnx/taste-skill, 20+ anti-AI-tell patterns, and a 50-check redesign audit \u2014 token-efficient (list \u2192 drill-down).",type:"module",main:"./dist/index.js",module:"./dist/index.js",types:"./dist/index.d.ts",bin:{"godx-ui-mcp":"./dist/index.js"},files:["dist","README.md"],publishConfig:{registry:"https://registry.npmjs.org/",access:"public"},repository:{type:"git",url:"git+https://github.com/godx-jp/godxjp-ui.git",directory:"mcp"},homepage:"https://github.com/godx-jp/godxjp-ui/tree/main/mcp#readme",license:"Apache-2.0",scripts:{build:"tsup",dev:"tsup --watch",start:"node dist/index.js",inspect:"npx @modelcontextprotocol/inspector node dist/index.js","type-check":"tsc --noEmit",test:"vitest run",prepublishOnly:"npm run build"},dependencies:{"@modelcontextprotocol/sdk":"^1.29.0",zod:"^4.4.3"},devDependencies:{"@types/node":"^22.10.0",tsup:"^8.5.1",typescript:"^6.0.3",vitest:"^4.1.6"},keywords:["mcp","model-context-protocol","godxjp","ui","design-system","react","claude","cursor"],author:"GoDX (https://godx.jp)",bugs:{url:"https://github.com/godx-jp/godxjp-ui/issues"}};var oe=[{name:"list_skills",description:"List every design/taste skill bundled by this MCP (id + name + whenToUse + section ids). Use FIRST to discover skills; then `get_skill_section` to drill in.",inputSchema:{type:"object",properties:{}}},{name:"list_primitives",description:"List every @godxjp/ui primitive/composite/shell (group + tagline per entry). Optionally filter by group. Then `get_component` for one's full API.",inputSchema:{type:"object",properties:{group:{type:"string",enum:["general","layout","data-display","data-entry","feedback","navigation","composites","shell","providers"]}}}},{name:"list_patterns",description:"List every canonical copy-paste code pattern (signup-form, settings-page, data-table-page, async-data-state, confirm-destructive, \u2026); common aliases resolve too. Use before `get_pattern`.",inputSchema:{type:"object",properties:{}}},{name:"list_anti_ai_tells",description:"List every AI-tell pattern to AVOID (optionally by category). Use to self-audit a design before shipping; then `get_anti_ai_tell` for the fix.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["visual","layout","copy","interaction","imagery","structure"]}}}},{name:"list_redesign_checks",description:"List the redesign audit checklist (50+ checks; optionally by category). Use when auditing an existing project; then `get_redesign_check` for a symptom's fix.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["typography","color-surface","layout","interactivity","content","components","iconography","code-quality","omissions"]}}}},{name:"list_audit_rules",description:"List the LOCAL static ui-audit rules (scripts/ui-audit.mjs) to run BEFORE any visual review \u2014 each cites the standard it enforces (WCAG/WAI-ARIA/Intl/ISO/IANA/CSS-Logical) + a fix + the run command. Optionally by category.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["tokens","composition","api","a11y","i18n","rtl","copy"]}}}},{name:"list_visual_checks",description:"List the RUNTIME visual-audit checks (scripts/visual-audit.mjs \u2014 Playwright + axe-core) to run against the RUNNING app: contrast/ARIA (axe), target size, rendered-accent chroma, DOM emoji, banner layout. Needs a browser (vs list_audit_rules, static). Optionally by category.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["a11y","color","i18n","layout"]}}}},{name:"get_anti_ai_tell",description:"Fetch ONE anti-AI-tell \u2014 full body + concrete fix. Use after `list_anti_ai_tells`.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Exact tell name from list_anti_ai_tells."}},required:["name"]}},{name:"get_redesign_check",description:"Fetch redesign check(s) matching a symptom snippet. Returns full fix + UI note. Use after `list_redesign_checks`.",inputSchema:{type:"object",properties:{symptom:{type:"string",description:"Fragment of the symptom text (e.g. 'Inter everywhere' / '100vh')."}},required:["symptom"]}},{name:"get_skill_section",description:"Fetch ONE section of ONE skill \u2014 token-efficient. E.g. `skill='soft', section='double-bezel'`. Use after `list_skills` narrowed the relevant skill + section.",inputSchema:{type:"object",properties:{skill:{type:"string",description:"Skill id (e.g. 'soft', 'minimalist', 'taste')."},section:{type:"string",description:"Section id within that skill."}},required:["skill","section"]}},{name:"get_component",description:"Full guide for one @godxjp/ui component \u2014 import path, props/types/defaults, HOW to use it (DO/DON'T), WHEN to reach for it (use cases), related components (don't reinvent/confuse), a copy-paste example, story path, and cardinal rules. Use this before hand-rolling anything. Design-token knobs are listed compactly (name+default); pass `verbose:true` for what each token controls.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Component name (e.g. 'Button', 'DataTable')."},verbose:{type:"boolean",description:"Include the full design-token table with a 'what it controls' description per token. Default false (compact token+default only) to save context."}},required:["name"]}},{name:"get_pattern",description:"Full code snippet for one canonical pattern \u2014 copy-paste-ready.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Pattern slug (use list_patterns first)."}},required:["name"]}},{name:"get_rule",description:"Read one cardinal rule from CLAUDE.md (by number) OR all if no number.",inputSchema:{type:"object",properties:{number:{type:"number",description:"Rule number (1-N)."}}}},{name:"get_vocab",description:"Read shared prop-vocabulary type (`SizeProp`, `StatusProp`, `ColorProp`, `LoadingProp`, etc.) OR all if no name.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Vocab type name."}}}},{name:"get_tokens",description:"Read design tokens, optionally filtered by tier category (primitive / semantic / component).",inputSchema:{type:"object",properties:{category:{type:"string",enum:["primitive","semantic","component"]}}}},{name:"list_consumer_skills",description:"List the design skills relevant to an app-dev BUILDING WITH @godxjp/ui (audience consumer/both). Hides core library-maintenance skills. START HERE if you import @godxjp/ui and want guidance (design-to-page, compose-a-screen, taste, \u2026). Returns id + name + whenToUse + section ids.",inputSchema:{type:"object",properties:{}}},{name:"get_consumer_skill",description:"Fetch ONE section of ONE consumer-facing skill. Same as get_skill_section but refuses core-only skills (steers app-devs away from library-maintenance material). Use after list_consumer_skills / route_consumer_task.",inputSchema:{type:"object",properties:{skill:{type:"string",description:"Consumer skill id (e.g. 'design-to-page', 'compose-a-screen')."},section:{type:"string",description:"Section id within that skill."}},required:["skill"]}},{name:"route_consumer_task",description:"Natural-language task \u2192 consumer skill+section pointer. Like route_task but only points to consumer-facing skills (never core library-maintenance). Use FIRST when you're building an app with @godxjp/ui.",inputSchema:{type:"object",properties:{task:{type:"string",description:"Describe what you want to build."}},required:["task"]}},{name:"draft_bug_report",description:"When @godxjp/ui ITSELF is at fault (missing token, a primitive lacking the controlled-vocabulary prop, a real a11y/behaviour bug, a wrong catalog example) and you cannot follow a rule \u2014 DON'T fake a workaround. This drafts a detailed GitHub issue body + a copy-paste `gh issue create` command so you can report it. Prints the command only; never runs gh.",inputSchema:{type:"object",properties:{summary:{type:"string",description:"One-line title of the bug / blocked rule."},repro:{type:"string",description:"Minimal steps or code to reproduce."},expected:{type:"string",description:"What SHOULD happen (per the rule/spec)."},actual:{type:"string",description:"What actually happens."},component:{type:"string",description:"Affected component name, if any (links to get_component)."},rule:{type:"number",description:"Cardinal rule number that can't be followed, if any."},version:{type:"string",description:"Installed @godxjp/ui version (e.g. '12.1.0')."},env:{type:"string",description:"Environment (browser/OS/framework), if relevant."}},required:["summary"]}},{name:"check_compatibility",description:"Report whether the @godxjp/ui version installed in the target project matches THIS catalog (which describes one release train). A mismatched minor means the props/tokens/patterns may describe a build they never installed (#140). Pass the installed version (`npm ls @godxjp/ui`); call it at the START of a consumer session.",inputSchema:{type:"object",properties:{version:{type:"string",description:"Installed @godxjp/ui version in the target project, e.g. '16.10.0'. Omit to just read the catalog's own version + compatible range."}}}},{name:"route_task",description:"Natural-language task \u2192 skill+section pointer (e.g. 'design a premium agency hero' \u2192 soft/vibe-archetypes). Use FIRST when you don't know which skill applies.",inputSchema:{type:"object",properties:{task:{type:"string",description:"Describe what you want to build."}},required:["task"]}},{name:"suggest_primitive",description:"Use case \u2192 primitive recommendation. E.g. 'confirm a destructive delete' \u2192 DangerZone pattern + Dialog suggestion.",inputSchema:{type:"object",properties:{use_case:{type:"string"}},required:["use_case"]}},{name:"search_components",description:"Fuzzy-search primitives by name / tagline / prop. Returns ranked matches.",inputSchema:{type:"object",properties:{query:{type:"string"}},required:["query"]}},{name:"get_frame_coverage",description:"Verified preview-contract coverage for a component (issue #163). Answers 'is this state actually PROVEN?' \u2014 returns, per contract dimension (variants, tones, sizes, shapes, density, controlled/uncontrolled ownership, disabled/read-only/loading/empty/error/success, async retry/cancel/offline, responsive viewport matrix, RTL, long/localized content, keyboard/focus, accessible name/description/error, reduced motion / coarse touch), whether an EXECUTED case proves it (covered), whether nothing proves it (UNTESTED), or whether it cannot exist (not-applicable, with a reason). UNTESTED IS NOT A PASS: never infer that a component supports a state because an example renders. Call this before telling a user a component 'supports' anything. Omit `name` for the repo-wide summary and the tracked known gaps.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Public export name (e.g. 'Button', 'DataTable', 'CardFooter'). Omit for the repo-wide coverage summary."}}}},{name:"lint_jsx",description:"Heuristic check of a JSX snippet for common violations \u2014 raw `<button>` / `<input>`, `color='error'` on Tag/Badge, missing aria-label, missing source.code override on stories with cell renderers (rule 34), etc.",inputSchema:{type:"object",properties:{jsx:{type:"string"}},required:["jsx"]}}];async function ne(e,t){switch(e){case"list_skills":return ve();case"list_primitives":return ce(t.group);case"list_patterns":return xe();case"list_anti_ai_tells":return Ce(t.category);case"list_redesign_checks":return De(t.category);case"list_audit_rules":return Te(t.category);case"list_visual_checks":return Se(t.category);case"get_anti_ai_tell":return Ae(String(t.name??""));case"get_redesign_check":return Oe(String(t.symptom??""));case"get_skill_section":return he(String(t.skill??""),String(t.section??""));case"get_component":return Re(String(t.name??""),t.verbose===!0);case"get_pattern":return Pe(String(t.name??""));case"get_rule":return Le(typeof t.number=="number"?t.number:void 0);case"get_vocab":return Fe(t.name==null?void 0:String(t.name));case"get_tokens":return Me(t.category);case"list_consumer_skills":return ye();case"get_consumer_skill":return we(String(t.skill??""),String(t.section??""));case"route_consumer_task":return ae(String(t.task??""),{consumerOnly:!0});case"draft_bug_report":return ke(t);case"check_compatibility":return de(t.version==null?void 0:String(t.version));case"route_task":return ae(String(t.task??""));case"suggest_primitive":return Be(String(t.use_case??""));case"search_components":return Ue(String(t.query??""));case"get_frame_coverage":return ze(t.name===void 0?void 0:String(t.name));case"lint_jsx":return He(String(t.jsx??""));default:return`Unknown tool: ${e}`}}function ve(){let e=`# Available skills (${S.length})
4432
+ finding and needs no suppression.`,$=[{id:"dialog-needs-body",severity:"error",category:"composition",standard:null,fix:"Wrap a Dialog/Sheet's middle in <DialogBody>/<SheetBody>. The scroll lives on the body \u2014 the content box is `overflow: hidden` with no max-height \u2014 so an overlay without one clips long content at BOTH ends and takes the footer's buttons with it, leaving Escape as the only way out. It only shows on real data, never on demo data (gh#617)."},{id:"no-utility-spacing",severity:"error",category:"composition",standard:null,fix:"Remove gap-*/p-*/m-* from your own markup; space siblings with <Flex gap> / <ResponsiveGrid>. Page sections are spaced by <PageContainer> (docs/CONSUMER-RULES.md \xA73)."},{id:"no-utility-layout",severity:"error",category:"composition",standard:null,fix:'Replace className="flex \u2026" / "grid \u2026" with <Flex> (row), <Flex direction="col"> (stack) or <ResponsiveGrid columns>.'},{id:"no-hand-rolled-surface",severity:"warn",category:"composition",standard:null,fix:"A rounded+border/bg div is a fake surface \u2014 use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from tokens. A read-only sample of a colour a USER chose is Swatch, which takes that value as a prop (gh#527)."},{id:"sibling-cards-need-flex",severity:"warn",category:"composition",standard:null,fix:'Wrap adjacent <Card>s in <Flex direction="col" gap="lg"> or <ResponsiveGrid>; direct children of PageContainer are spaced by the page already.'},{id:"no-raw-palette-color",severity:"error",category:"tokens",standard:null,fix:"Use semantic tokens (bg-primary, text-muted-foreground), never raw palette (bg-blue-500)."},{id:"no-arbitrary-hex",severity:"error",category:"tokens",standard:null,fix:"No hardcoded hex in className; read design-system color tokens."},{id:"no-arbitrary-spacing",severity:"error",category:"tokens",standard:null,fix:"No p-[13px]/gap-[7px]; use the token scale / <Flex gap> / <PageContainer>."},{id:"no-arbitrary-size",severity:"error",category:"tokens",standard:null,fix:"No w-[37px]/h-[260px]; use token sizes or a sizing prop (min-w-[\u2026] allowed)."},{id:"no-arbitrary-typography",severity:"error",category:"tokens",standard:null,fix:"No text-[20px]/leading-[1.7]; use the golden-ratio type-scale tokens."},{id:"no-arbitrary-radius",severity:"error",category:"tokens",standard:null,fix:"No rounded-[6px]; use rounded-sm/md/lg radius tokens."},{id:"no-off-scale-token-value",severity:"warn",category:"tokens",standard:null,fix:"A design-system knob you override takes a step (style={{ '--card-space-inset': 'var(--space-4)' }}) or a calc() from one (calc(var(--space-4) + 2px)), not a raw 13px. Only axes that HAVE a scale count: space/padding/gap/margin, font-size, radius, icon-size (width/height/size/offset have none yet, so a number there is fine). A value that is genuinely off the grid keeps its literal and says why in place, with a /* scale-exempt: 6px status dot, below --space-1 */ comment on that line or the one above."},{id:"no-dark-color-override",severity:"warn",category:"tokens",standard:null,fix:"Drop dark: color overrides \u2014 semantic tokens already adapt."},{id:"raw-white-black",severity:"warn",category:"tokens",standard:null,fix:"Prefer semantic tokens (text-primary-foreground, bg-background) over raw white/black."},{id:"no-domain-tracking-token",severity:"error",category:"tokens",standard:null,fix:"No package-tracking/domain tokens; use semantic tokens or app theme overrides."},{id:"no-space-xy",severity:"error",category:"tokens",standard:null,fix:"Use <Flex gap> instead of space-x/y-*."},{id:"no-raw-select",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Select> from @godxjp/ui, not a raw <select>."},{id:"no-raw-table",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use the <Table>/<DataTable> family, not a raw <table>."},{id:"no-raw-input",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Input> from @godxjp/ui, not a raw <input>."},{id:"no-raw-textarea",severity:"warn",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Textarea> from @godxjp/ui, not a raw <textarea>."},{id:"no-raw-button",severity:"error",category:"composition",standard:"HTML Living Standard (WHATWG)",fix:"Use <Button> from @godxjp/ui, not a raw <button>."},{id:"card-manual-padding",severity:"error",category:"composition",standard:null,fix:"Wrap the body in <CardContent>; don't hand-roll padding on <Card>."},{id:"card-needs-content",severity:"error",category:"composition",standard:null,fix:"<Card> body must be in <CardContent> (no padding otherwise); flush only for a full-bleed table."},{id:"bare-control-needs-formfield",severity:"warn",category:"composition",standard:"WCAG 2.2 SC 1.3.1 \xB7 3.3.2 \xB7 @godxjp/ui FormField (cardinal rule 227)",fix:"Wrap a labelled control in <FormField label=\u2026> \u2014 it owns label\u2194control id wiring, aria/error, AND the field rhythm; never pair a bare <Label> with an <Input>."},{id:"manual-field-error",severity:"warn",category:"composition",standard:"WCAG 2.2 SC 3.3.1",fix:"Use <FormField error=\u2026>, not a hand-rolled <p class='text-destructive'>."},{id:"manual-field-helper",severity:"warn",category:"composition",standard:null,fix:"Use <FormField helper=\u2026>, not a hand-rolled helper <p>."},{id:"status-tone-not-variant",severity:"error",category:"api",standard:null,fix:"Badge/Tag/StatCard status uses tone, not variant (variant is structural)."},{id:"value-callback-on-value-change",severity:"error",category:"api",standard:null,fix:"Abstract value components use onValueChange, not onChange."},{id:"icon-button-needs-name",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 4.1.2 \xB7 1.1.1 \xB7 WAI-ARIA 1.2",fix:"Add aria-label={t('\u2026')} to <Button size='icon'>; the glyph is aria-hidden."},{id:"img-needs-alt",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 1.1.1 \xB7 HTML Living Standard",fix:"Add alt to every <img> (alt='' if decorative); prefer <Avatar>/<AspectRatio>."},{id:"no-positive-tabindex",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 2.4.3 \xB7 WAI-ARIA APG",fix:"Use tabIndex 0 or -1 only; never positive \u2014 it breaks focus order."},{id:"hand-rolled-close-glyph",severity:"warn",category:"a11y",standard:"WAI-ARIA 1.2 (dialog) \xB7 WCAG 2.2 SC 4.1.2",fix:"Pass onDismiss to <Alert>, or use <Dialog>/<Sheet>'s built-in labelled close \u2014 not a bare \u2715."},{id:"no-emoji-in-ui",severity:"warn",category:"i18n",standard:"Unicode UTS #51 \xB7 WCAG 2.2 SC 1.1.1",fix:"No emoji in product UI; quiet i18n copy + Lucide icon + Badge tone."},{id:"no-emoji-flag",severity:"warn",category:"i18n",standard:"ISO 3166-1 \xB7 ECMA-402 Intl.DisplayNames \xB7 Unicode UTS #51",fix:"Derive country names from Intl.DisplayNames; no emoji flags."},{id:"hardcoded-currency",severity:"warn",category:"i18n",standard:"ISO 4217 \xB7 ECMA-402 Intl.NumberFormat",fix:"Format money with Intl.NumberFormat({ style: 'currency', currency }), not \xA5{amount}."},{id:"raw-intl-date",severity:"warn",category:"i18n",standard:"ISO 8601 \xB7 IANA tz \xB7 ECMA-402 Intl.DateTimeFormat",fix:"Use formatDate from @godxjp/ui/datetime, not hand-built or locale-default dates."},{id:"no-physical-direction",severity:"warn",category:"rtl",standard:"W3C CSS Logical Properties L1 \xB7 WCAG 2.2 (1.3.2)",fix:"Use logical utilities (ms-/me-/ps-/pe-, start-/end-, text-start/end, border-s/e, rounded-s/e)."},{id:"no-em-dash-in-copy",severity:"warn",category:"copy",standard:"@godxjp/ui reference-design typography",fix:"No em-dash (\u2014) in copy; use a middot \xB7 or two calm sentences."}];function J(e){return e?$.filter(t=>t.category===e):$}var ee="node node_modules/@godxjp/ui/scripts/visual-audit.mjs <baseUrl> [route \u2026] (optional peers, TESTED range: playwright >=1.55 <2 [1.61.1] + @axe-core/playwright >=4.10 <5 [4.12.1] + axe-core >=4.10 <5 [4.12.1] + a chromium via `playwright install chromium`; --strict for a CI gate, --format json ALWAYS emits valid JSON with a status of ok|partial|error separating infra errors[] from product findings[], --rules to print this catalog)",Z=[{id:"css-layers-missing",severity:"error",category:"layout",standard:"@godxjp/ui styles contract (styles / styles/core are the only entries)",fix:"Import `@godxjp/ui/styles` (or `styles/core` without fonts); never cherry-pick *-layout.css \u2014 a missing layer renders naked menus and unsized Select rows."},{id:"control-height-mismatch",severity:"error",category:"layout",standard:"@godxjp/ui control tier (--control-height) \xB7 Nielsen consistency heuristic",fix:"Every control in one row must share --control-height; replace hand-rolled pills with Avatar/Button/Badge, never restyle a control's height."},{id:"sibling-card-gap",severity:"error",category:"layout",standard:"@godxjp/ui spacing scale (docs/SPACING.md)",fix:'Adjacent Cards need one space step between them \u2014 <Flex direction="col" gap>, <ResponsiveGrid>, or direct children of PageContainer.'},{id:"row-content-starved",severity:"warn",category:"layout",standard:"WCAG 2.2 SC 1.4.10 reflow",fix:`A sibling (a w-full SelectTrigger) takes the row's width and truncates its neighbours \u2014 give the Select width="auto" or move it out of the row.`},{id:"axe-violations",severity:"warn",category:"a11y",standard:"WCAG 2.2 A/AA \xB7 WAI-ARIA 1.2 (axe-core engine)",fix:"Fix each axe node \u2014 contrast (1.4.3), name/role/value (4.1.2), ARIA, landmarks. Runs on the REAL DOM, catching what static analysis cannot."},{id:"target-size-min",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 2.5.8 (24\xD724 AA) \xB7 2.5.5 (44\xD744 AAA)",fix:"Interactive targets must be \u226524\xD724 CSS px; size from the --control-height tier."},{id:"oversaturated-accent",severity:"warn",category:"color",standard:"@godxjp/ui reference-design \u6E0B\u307F (OKLCH chroma \u2264 0.18)",fix:"Desaturate brand/primary surfaces (OKLCH chroma \u2264 0.18); read --primary tokens, no raw vivid bars."},{id:"emoji-rendered",severity:"warn",category:"i18n",standard:"Unicode UTS #51 \xB7 WCAG 2.2 SC 1.1.1",fix:"Remove emoji from rendered product text; quiet i18n copy + Lucide icon + Badge tone."},{id:"alert-controls-misplaced",severity:"warn",category:"layout",standard:"@godxjp/ui Alert anatomy \xB7 WAI-ARIA 1.2 \xB7 WCAG 2.2 SC 4.1.2",fix:"Use <Alert>: one leading tone icon, <Alert.Actions> trailing-right normal width, onDismiss \xD7 top-right, one horizontal row."}];function te(e){return e?Z.filter(t=>t.category===e):Z}var p={name:"@godxjp/ui-mcp",version:"27.1.0",godxUiCompatibility:"27.1.x",description:"Model Context Protocol server for @godxjp/ui \u2014 gives Claude Code / Codex CLI / Cursor / any MCP-aware agent live access to the component catalog, prop vocabulary, design tokens, 45 cardinal rules, copy-paste-ready patterns, 12 design / taste skills synthesised from Leonxlnx/taste-skill, 20+ anti-AI-tell patterns, and a 50-check redesign audit \u2014 token-efficient (list \u2192 drill-down).",type:"module",main:"./dist/index.js",module:"./dist/index.js",types:"./dist/index.d.ts",bin:{"godx-ui-mcp":"./dist/index.js"},files:["dist","README.md"],publishConfig:{registry:"https://registry.npmjs.org/",access:"public"},repository:{type:"git",url:"git+https://github.com/godx-jp/godxjp-ui.git",directory:"mcp"},homepage:"https://github.com/godx-jp/godxjp-ui/tree/main/mcp#readme",license:"Apache-2.0",scripts:{build:"tsup",dev:"tsup --watch",start:"node dist/index.js",inspect:"npx @modelcontextprotocol/inspector node dist/index.js","type-check":"tsc --noEmit",test:"vitest run",prepublishOnly:"npm run build"},dependencies:{"@modelcontextprotocol/sdk":"^1.29.0",zod:"^4.4.3"},devDependencies:{"@types/node":"^22.10.0",tsup:"^8.5.1",typescript:"^6.0.3",vitest:"^4.1.6"},keywords:["mcp","model-context-protocol","godxjp","ui","design-system","react","claude","cursor"],author:"GoDX (https://godx.jp)",bugs:{url:"https://github.com/godx-jp/godxjp-ui/issues"}};var oe=[{name:"list_skills",description:"List every design/taste skill bundled by this MCP (id + name + whenToUse + section ids). Use FIRST to discover skills; then `get_skill_section` to drill in.",inputSchema:{type:"object",properties:{}}},{name:"list_primitives",description:"List every @godxjp/ui primitive/composite/shell (group + tagline per entry). Optionally filter by group. Then `get_component` for one's full API.",inputSchema:{type:"object",properties:{group:{type:"string",enum:["general","layout","data-display","data-entry","feedback","navigation","composites","shell","providers"]}}}},{name:"list_patterns",description:"List every canonical copy-paste code pattern (signup-form, settings-page, data-table-page, async-data-state, confirm-destructive, \u2026); common aliases resolve too. Use before `get_pattern`.",inputSchema:{type:"object",properties:{}}},{name:"list_anti_ai_tells",description:"List every AI-tell pattern to AVOID (optionally by category). Use to self-audit a design before shipping; then `get_anti_ai_tell` for the fix.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["visual","layout","copy","interaction","imagery","structure"]}}}},{name:"list_redesign_checks",description:"List the redesign audit checklist (50+ checks; optionally by category). Use when auditing an existing project; then `get_redesign_check` for a symptom's fix.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["typography","color-surface","layout","interactivity","content","components","iconography","code-quality","omissions"]}}}},{name:"list_audit_rules",description:"List the LOCAL static ui-audit rules (scripts/ui-audit.mjs) to run BEFORE any visual review \u2014 each cites the standard it enforces (WCAG/WAI-ARIA/Intl/ISO/IANA/CSS-Logical) + a fix + the run command. Optionally by category.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["tokens","composition","api","a11y","i18n","rtl","copy"]}}}},{name:"list_visual_checks",description:"List the RUNTIME visual-audit checks (scripts/visual-audit.mjs \u2014 Playwright + axe-core) to run against the RUNNING app: contrast/ARIA (axe), target size, rendered-accent chroma, DOM emoji, banner layout. Needs a browser (vs list_audit_rules, static). Optionally by category.",inputSchema:{type:"object",properties:{category:{type:"string",enum:["a11y","color","i18n","layout"]}}}},{name:"get_anti_ai_tell",description:"Fetch ONE anti-AI-tell \u2014 full body + concrete fix. Use after `list_anti_ai_tells`.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Exact tell name from list_anti_ai_tells."}},required:["name"]}},{name:"get_redesign_check",description:"Fetch redesign check(s) matching a symptom snippet. Returns full fix + UI note. Use after `list_redesign_checks`.",inputSchema:{type:"object",properties:{symptom:{type:"string",description:"Fragment of the symptom text (e.g. 'Inter everywhere' / '100vh')."}},required:["symptom"]}},{name:"get_skill_section",description:"Fetch ONE section of ONE skill \u2014 token-efficient. E.g. `skill='soft', section='double-bezel'`. Use after `list_skills` narrowed the relevant skill + section.",inputSchema:{type:"object",properties:{skill:{type:"string",description:"Skill id (e.g. 'soft', 'minimalist', 'taste')."},section:{type:"string",description:"Section id within that skill."}},required:["skill","section"]}},{name:"get_component",description:"Full guide for one @godxjp/ui component \u2014 import path, props/types/defaults, HOW to use it (DO/DON'T), WHEN to reach for it (use cases), related components (don't reinvent/confuse), a copy-paste example, story path, and cardinal rules. Use this before hand-rolling anything. Design-token knobs are listed compactly (name+default); pass `verbose:true` for what each token controls.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Component name (e.g. 'Button', 'DataTable')."},verbose:{type:"boolean",description:"Include the full design-token table with a 'what it controls' description per token. Default false (compact token+default only) to save context."}},required:["name"]}},{name:"get_pattern",description:"Full code snippet for one canonical pattern \u2014 copy-paste-ready.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Pattern slug (use list_patterns first)."}},required:["name"]}},{name:"get_rule",description:"Read one cardinal rule from CLAUDE.md (by number) OR all if no number.",inputSchema:{type:"object",properties:{number:{type:"number",description:"Rule number (1-N)."}}}},{name:"get_vocab",description:"Read shared prop-vocabulary type (`SizeProp`, `StatusProp`, `ColorProp`, `LoadingProp`, etc.) OR all if no name.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Vocab type name."}}}},{name:"get_tokens",description:"Read design tokens, optionally filtered by tier category (primitive / semantic / component).",inputSchema:{type:"object",properties:{category:{type:"string",enum:["primitive","semantic","component"]}}}},{name:"list_consumer_skills",description:"List the design skills relevant to an app-dev BUILDING WITH @godxjp/ui (audience consumer/both). Hides core library-maintenance skills. START HERE if you import @godxjp/ui and want guidance (design-to-page, compose-a-screen, taste, \u2026). Returns id + name + whenToUse + section ids.",inputSchema:{type:"object",properties:{}}},{name:"get_consumer_skill",description:"Fetch ONE section of ONE consumer-facing skill. Same as get_skill_section but refuses core-only skills (steers app-devs away from library-maintenance material). Use after list_consumer_skills / route_consumer_task.",inputSchema:{type:"object",properties:{skill:{type:"string",description:"Consumer skill id (e.g. 'design-to-page', 'compose-a-screen')."},section:{type:"string",description:"Section id within that skill."}},required:["skill"]}},{name:"route_consumer_task",description:"Natural-language task \u2192 consumer skill+section pointer. Like route_task but only points to consumer-facing skills (never core library-maintenance). Use FIRST when you're building an app with @godxjp/ui.",inputSchema:{type:"object",properties:{task:{type:"string",description:"Describe what you want to build."}},required:["task"]}},{name:"draft_bug_report",description:"When @godxjp/ui ITSELF is at fault (missing token, a primitive lacking the controlled-vocabulary prop, a real a11y/behaviour bug, a wrong catalog example) and you cannot follow a rule \u2014 DON'T fake a workaround. This drafts a detailed GitHub issue body + a copy-paste `gh issue create` command so you can report it. Prints the command only; never runs gh.",inputSchema:{type:"object",properties:{summary:{type:"string",description:"One-line title of the bug / blocked rule."},repro:{type:"string",description:"Minimal steps or code to reproduce."},expected:{type:"string",description:"What SHOULD happen (per the rule/spec)."},actual:{type:"string",description:"What actually happens."},component:{type:"string",description:"Affected component name, if any (links to get_component)."},rule:{type:"number",description:"Cardinal rule number that can't be followed, if any."},version:{type:"string",description:"Installed @godxjp/ui version (e.g. '12.1.0')."},env:{type:"string",description:"Environment (browser/OS/framework), if relevant."}},required:["summary"]}},{name:"check_compatibility",description:"Report whether the @godxjp/ui version installed in the target project matches THIS catalog (which describes one release train). A mismatched minor means the props/tokens/patterns may describe a build they never installed (#140). Pass the installed version (`npm ls @godxjp/ui`); call it at the START of a consumer session.",inputSchema:{type:"object",properties:{version:{type:"string",description:"Installed @godxjp/ui version in the target project, e.g. '16.10.0'. Omit to just read the catalog's own version + compatible range."}}}},{name:"route_task",description:"Natural-language task \u2192 skill+section pointer (e.g. 'design a premium agency hero' \u2192 soft/vibe-archetypes). Use FIRST when you don't know which skill applies.",inputSchema:{type:"object",properties:{task:{type:"string",description:"Describe what you want to build."}},required:["task"]}},{name:"suggest_primitive",description:"Use case \u2192 primitive recommendation. E.g. 'confirm a destructive delete' \u2192 DangerZone pattern + Dialog suggestion.",inputSchema:{type:"object",properties:{use_case:{type:"string"}},required:["use_case"]}},{name:"search_components",description:"Fuzzy-search primitives by name / tagline / prop. Returns ranked matches.",inputSchema:{type:"object",properties:{query:{type:"string"}},required:["query"]}},{name:"get_frame_coverage",description:"Verified preview-contract coverage for a component (issue #163). Answers 'is this state actually PROVEN?' \u2014 returns, per contract dimension (variants, tones, sizes, shapes, density, controlled/uncontrolled ownership, disabled/read-only/loading/empty/error/success, async retry/cancel/offline, responsive viewport matrix, RTL, long/localized content, keyboard/focus, accessible name/description/error, reduced motion / coarse touch), whether an EXECUTED case proves it (covered), whether nothing proves it (UNTESTED), or whether it cannot exist (not-applicable, with a reason). UNTESTED IS NOT A PASS: never infer that a component supports a state because an example renders. Call this before telling a user a component 'supports' anything. Omit `name` for the repo-wide summary and the tracked known gaps.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Public export name (e.g. 'Button', 'DataTable', 'CardFooter'). Omit for the repo-wide coverage summary."}}}},{name:"lint_jsx",description:"Heuristic check of a JSX snippet for common violations \u2014 raw `<button>` / `<input>`, `color='error'` on Tag/Badge, missing aria-label, missing source.code override on stories with cell renderers (rule 34), etc.",inputSchema:{type:"object",properties:{jsx:{type:"string"}},required:["jsx"]}}];async function ne(e,t){switch(e){case"list_skills":return ve();case"list_primitives":return ce(t.group);case"list_patterns":return xe();case"list_anti_ai_tells":return Ce(t.category);case"list_redesign_checks":return De(t.category);case"list_audit_rules":return Te(t.category);case"list_visual_checks":return Se(t.category);case"get_anti_ai_tell":return Ae(String(t.name??""));case"get_redesign_check":return Oe(String(t.symptom??""));case"get_skill_section":return he(String(t.skill??""),String(t.section??""));case"get_component":return Re(String(t.name??""),t.verbose===!0);case"get_pattern":return Pe(String(t.name??""));case"get_rule":return Le(typeof t.number=="number"?t.number:void 0);case"get_vocab":return Fe(t.name==null?void 0:String(t.name));case"get_tokens":return Me(t.category);case"list_consumer_skills":return ye();case"get_consumer_skill":return we(String(t.skill??""),String(t.section??""));case"route_consumer_task":return ae(String(t.task??""),{consumerOnly:!0});case"draft_bug_report":return ke(t);case"check_compatibility":return de(t.version==null?void 0:String(t.version));case"route_task":return ae(String(t.task??""));case"suggest_primitive":return Be(String(t.use_case??""));case"search_components":return Ue(String(t.query??""));case"get_frame_coverage":return ze(t.name===void 0?void 0:String(t.name));case"lint_jsx":return He(String(t.jsx??""));default:return`Unknown tool: ${e}`}}function ve(){let e=`# Available skills (${S.length})
4434
4433
 
4435
4434
  `;e+="Each is tagged `[audience]` \u2014 `core` = building @godxjp/ui itself, `consumer` = building an app with it, `both`. App-devs: use `list_consumer_skills` to hide core material.\n\n",e+='Use `get_skill_section skill="..." section="..."` to drill in.\n\n';for(let t of S)e+=`## ${t.id} \u2014 ${t.name} \`[${t.audience}]\`
4436
4435
  `,e+=`**When to use:** ${t.whenToUse}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui-mcp",
3
- "version": "26.4.0",
4
- "godxUiCompatibility": "26.4.x",
3
+ "version": "27.1.0",
4
+ "godxUiCompatibility": "27.1.x",
5
5
  "description": "Model Context Protocol server for @godxjp/ui — gives Claude Code / Codex CLI / Cursor / any MCP-aware agent live access to the component catalog, prop vocabulary, design tokens, 45 cardinal rules, copy-paste-ready patterns, 12 design / taste skills synthesised from Leonxlnx/taste-skill, 20+ anti-AI-tell patterns, and a 50-check redesign audit — token-efficient (list → drill-down).",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",