@godxjp/ui-mcp 28.12.0 → 28.13.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.
- package/dist/index.js +16 -16
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const form = useZodForm(schema, { defaultValues: { email: "" } });
|
|
|
11
11
|
import { Input } from "@godxjp/ui/data-entry";
|
|
12
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).",'CANONICAL BOOLEAN FIELD (gh#709, antd `Form.Item valuePropName="checked"`): `<FormFieldControl name="is_shared" valuePropName="checked">{(field) => <Checkbox {...field}>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306B\u5171\u6709\u3059\u308B</Checkbox>}</FormFieldControl>` \u2014 box and label on ONE line, the label text toggles the box and names it, no label row above it, and a validation / 422 error still lands on the checkbox (`aria-invalid` + `aria-describedby`). The bag is `{ id, name, checked, onCheckedChange, onBlur, ref, disabled? }`; the stored value is always a boolean.','DON\'T put a boolean field\'s label ABOVE its checkbox \u2014 `<FormFieldControl name="is_shared" label="\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306B\u5171\u6709\u3059\u308B">{(field) => <Checkbox checked={field.value} onCheckedChange={(c) => field.onChange(c === true)} />}</FormFieldControl>` renders the label on one row and the box far below it. Use `valuePropName="checked"` with the label as the Checkbox\'s children.'],useCases:["Validated settings forms","Nested repeating data entry"],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
|
-
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";
|
|
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:"placeholder",type:"[string,string]",description:"A PAIR, one per endpoint \u2014 TimePicker's single-string `placeholder` is omitted from this type on purpose, because a range has two empty fields and one string would label both of them the same. Route both through t()."},{name:"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; muted?: boolean }[]",required:!0,description:"Positive unit counts determine proportional column widths. Column count determines the minimum canvas width, so coarse grouping zooms out. `muted: true` tints that column down the whole body (a weekend, a holiday, a closed period) with `--range-timeline-muted-column-background` (default `hsl(var(--muted))`, the header surface; body text keeps 14.25:1 / 12.4:1 and `--muted-foreground` 5.2:1 / 5.47:1 on it)."},{name:"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. Optional `depth` (0 = top level) nests rows: pass them flat and depth-first (a parent, then its descendants); a row is a parent when the row after it is deeper. The component indents the label cell by `--range-timeline-indent-width` per level (logical padding, so RTL indents from the right; the label column keeps its width) and gives each parent a disclosure button. With no `depth > 0` anywhere the markup is unchanged."},{name:"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."},{name:"bordered",type:"boolean",defaultValue:"true",description:"Rule the body as a Gantt grid: a line between every row (label column and track), a line under the header and between band and tick rows, and a vertical line per column down the whole body, exactly under its header column edge for unequal `units` too. ON BY DEFAULT, like `Calendar bordered`. The lines are one decorative layer (aria-hidden, pointer-events none) behind the bars. TWO TIERS, and they are different knobs: the INSIDE grid is `--range-timeline-grid-color`, default `hsl(var(--border))` (L* 93.80 / 1.149:1 on the card in light, L* 20.76 / 1.270:1 in dark) at `--range-timeline-grid-width` (hairline); the OUTER FRAME and the label-column divider are `--range-timeline-border-color`, default `var(--border)` \u2014 the same tier, so the grid can never read heavier than the box around it or than a DataTable row rule. It was `hsl(var(--input) / 0.5)` (L* 78.67 / 1.738:1) through 27.6.0 and was reported darker than every card and table in the system (gh#730); set `--range-timeline-grid-color: hsl(var(--input) / 0.5)` to get that weight back. `bordered={false}` restores header-only ruling; muted columns still paint."},{name:"density",type:'"compact" | "default" | "comfortable"',defaultValue:'"default"',description:"Width of ONE axis unit \u2014 the canonical density vocabulary, the same three steps `DataTable density` takes. `default` is the shipped 56px/day, so no existing Gantt moves; `compact` is 42px/day (a 31-day axis needs 1558px instead of 1992px \u2014 measured, the widest two-digit day label across ja/en/ar/fa/hi/th/bn numbering systems is 19.86px in a 24px content box, so the tick still fits); `comfortable` is 70px/day. It moves the COLUMN WIDTH ONLY \u2014 row height, bar height and the label column are identical at every step, so a compact Gantt is the same schedule with more days on screen, not a smaller one. Re-points `--range-timeline-unit-width` on the element; retune a step with `--range-timeline-unit-width-{compact,default,comfortable}`. Unrelated to `PageContainer density` / `AppProvider density`, which rescale the whole page through `--scaling`."},{name:"expandedValues",type:"readonly string[]",description:"Controlled ids of the EXPANDED parent rows (same spelling as `Tree expandedValues`). A folded parent removes every descendant row \u2014 label AND bar \u2014 from the DOM and the accessibility tree; the body grid, row rules and today marker stay aligned."},{name:"defaultExpandedValues",type:"readonly string[]",description:"Uncontrolled initial expanded parents. Omitted: every parent starts expanded (unlike `Tree`, whose branches start closed), so adding `depth` never hides a row."},{name:"onExpandedValuesChange",type:"(values: string[]) => void",description:"Fires with the next expanded parent ids when a disclosure is toggled, for controlled and uncontrolled timelines alike."}],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.",'An interval wholly outside the columns shows a localized direction indicator at that edge of its row: a chevron plus "before" at the inline-start edge, "after" plus a chevron at the inline-end edge; the chevrons mirror under RTL.',"Mark non-working columns with `columns[].muted` rather than tinting cells yourself; retint the grid through `--range-timeline-grid-color` / `--range-timeline-muted-column-background`, never page CSS.",'Need more days on one screen? Pass `density="compact"` \u2014 do NOT declare `--range-timeline-unit-width` in an app stylesheet. Tick and band labels are CENTRED on their column by the component; a band clipped by the range (a month the axis starts inside) centres its label on the VISIBLE part, since that is the box the band owns.',"Use TimelineGrid for time-of-day columns; RangeTimeline is a horizontal range axis.","Nested Gantt (parent/child work items): pass the server's depth-first rows with `depth` on each (`{ id, label, start, end, startLabel, endLabel, depth }`), and fold with `expandedValues` / `onExpandedValuesChange` (or `defaultExpandedValues`). Never fake an indent inside `label` \u2014 the component owns the indent, the disclosure button (named from `rangeTimeline.childRows` + the row label, `aria-expanded`) and the `list` / `listitem` + `aria-level` / `aria-setsize` / `aria-posinset` structure."]},{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 | { start?: ReactNode; end?: ReactNode }",description:'Action buttons / controls rendered at the END of the title row. A bare node is the whole slot and stays the shape every page already passes \u2014 it lands in `start`. `{ start, end }` splits it in two so a page can put something AFTER the actions: the record-screen convention is identity \u2192 actions \u2192 PAGER LAST, and with one slot the pager had to live in a second header band inside the body, so an app carried two header shapes (gh#734). Both sub-slots are direct children of the one header-extra box in source order, so DOM order IS reading order and the accessibility tree follows; unused, nothing renders at all. The union is `TabsExtraProp`\'s on purpose \u2014 `Tabs.extra` already accepts exactly this for exactly this reason, and the names are logical (`start`/`end`, never left/right), so they swap sides under dir="rtl".'},{name:"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: Pass `extra={{ start: <actions/>, end: <pager/> }}` when the header convention is identity \u2192 action cluster \u2192 PAGER LAST. The two sub-slots render in that order inside the one header-extra box, so the record-pager band that used to live inside the body moves up and the app stops carrying two header shapes (gh#734). The shape is `Tabs.extra`'s, logical (`start`/`end`), and a bare node still means exactly what it always did.",'DON\'T: Fake "after the actions" by putting the pager in a second header strip inside `children`, or by appending it to the same node you pass to `extra` and spacing it with a utility \u2014 the first gives the page two header bands to keep in sync, the second hand-lays the gap the header-extra box already owns.',"DO: Leave `fill` off (the default) for ordinary pages \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";
|
|
16
16
|
import { Button } from "@godxjp/ui/general";
|
|
17
17
|
|
|
@@ -784,7 +784,7 @@ import { Flex } from "@godxjp/ui/layout";
|
|
|
784
784
|
</Flex>
|
|
785
785
|
</CardContent>
|
|
786
786
|
</Card>
|
|
787
|
-
</Flex>`,storyPath:"data-display/ListRow.stories.tsx",rules:[42,44]},{name:"CredentialReveal",group:"data-display",tagline:"One-time secret surface \u2014 masked-by-default value with a show/hide toggle, a copy button that confirms the copy, optional download, and an optional acknowledge action to pair with Dialog. The GitHub/Stripe token-reveal pattern as a real primitive so consumers stop hand-rolling it.",props:[{name:"secret",type:"string",required:!0,description:"The one-time secret value."},{name:"label",type:"string",description:"Accessible name / caption for the secret (e.g. 'API key')."},{name:"warning",type:"React.ReactNode | null",description:"Caution banner copy; defaults to a localized 'shown only once' warning. Pass null to suppress the banner."},{name:"revealed",type:"boolean",description:"Controlled reveal state (with defaultRevealed / onRevealedChange)."},{name:"defaultRevealed",type:"boolean",defaultValue:"false",description:"Uncontrolled initial reveal state."},{name:"onRevealedChange",type:"(revealed: boolean) => void",description:"Reveal toggle handler."},{name:"onCopy",type:"(secret: string) => void",description:"Called after the secret is written to the clipboard."},{name:"onAcknowledge",type:"() => void",description:"Renders a confirm button; wire it to the Dialog's onOpenChange(false)."},{name:"downloadable",type:"boolean",defaultValue:"false",description:"Offer a download-as-file button."},{name:"size",type:'"xs" | "sm" | "md" | "lg"',defaultValue:'"md"',description:"Action button size tier."},{name:"tone",type:'"warning" | "destructive" | "info"',defaultValue:'"warning"',description:"Caution banner severity."}],usage:["DO use for a secret shown exactly once after creation (device credential, API key, service-account secret) \u2014 it masks by default and confirms the copy.","DO pair it inside a Dialog and reset via controlled `revealed`/`onRevealedChange` (or let it re-blur automatically when the `secret` prop changes) so a reopened dialog starts masked.","DO pass `onAcknowledge` to gate the dialog close behind an explicit 'I've saved it' confirmation.","DON'T use it for an editable password field \u2014 that's PasswordInput. CredentialReveal is read-only display of an issued secret.","DON'T hand-roll the copy button + copied-state + aria-live announcement; it's built in."],useCases:["Device credential issued after enrollment","API key / personal access token shown once on creation","Service-account secret / client secret reveal","Recovery code or one-time bootstrap password"],related:["PasswordInput \u2014 editable password/secret ENTRY with a show/hide toggle (data-entry); CredentialReveal is read-only DISPLAY of an issued secret.","Dialog \u2014 the modal CredentialReveal is designed to live inside.","Alert \u2014 the caution banner CredentialReveal composes internally."],example:`import { CredentialReveal } from "@godxjp/ui/data-display";
|
|
787
|
+
</Flex>`,storyPath:"data-display/ListRow.stories.tsx",rules:[42,44]},{name:"CredentialReveal",group:"data-display",tagline:"One-time secret surface \u2014 masked-by-default value with a show/hide toggle, a copy button that confirms the copy, optional download, and an optional acknowledge action to pair with Dialog. The GitHub/Stripe token-reveal pattern as a real primitive so consumers stop hand-rolling it.",props:[{name:"secret",type:"string",required:!0,description:"The one-time secret value."},{name:"label",type:"string",description:"Accessible name / caption for the secret (e.g. 'API key')."},{name:"warning",type:"React.ReactNode | null",description:"Caution banner copy; defaults to a localized 'shown only once' warning. Pass null to suppress the banner."},{name:"revealed",type:"boolean",description:"Controlled reveal state (with defaultRevealed / onRevealedChange)."},{name:"defaultRevealed",type:"boolean",defaultValue:"false",description:"Uncontrolled initial reveal state."},{name:"onRevealedChange",type:"(revealed: boolean) => void",description:"Reveal toggle handler."},{name:"onCopy",type:"(secret: string) => void",description:"Called after the secret is written to the clipboard."},{name:"onAcknowledge",type:"() => void",description:"Renders a confirm button; wire it to the Dialog's onOpenChange(false)."},{name:"acknowledgeLabel",type:"React.ReactNode",description:'Copy on the button `onAcknowledge` creates. Defaults to a localized "I\'ve saved it" \u2014 override it when the confirmation claims something more specific than having read the secret ("\u4FDD\u7BA1\u3057\u307E\u3057\u305F", "Stored in 1Password"). Consumer-owned wording: route it through t().'},{name:"downloadable",type:"boolean",defaultValue:"false",description:"Offer a download-as-file button."},{name:"downloadFileName",type:"string",defaultValue:'"credential.txt"',description:"Name of the file `downloadable` writes. The default is deliberately anonymous; set it when the user will hold several at once (`api-key-prod.txt`) so the saved files are still telling apart in a downloads folder."},{name:"size",type:'"xs" | "sm" | "md" | "lg"',defaultValue:'"md"',description:"Action button size tier."},{name:"tone",type:'"warning" | "destructive" | "info"',defaultValue:'"warning"',description:"Caution banner severity."},{name:"id",type:"string",description:"DOM id on the root. Useful here because the surface is usually inside a Dialog: it is what a `aria-describedby` on the dialog, or a deep link back to the issued credential, can point at."},{name:"aria-label",type:"string",description:"Accessible name for the credential region when `label` is not set or is a non-string node. `label` is the VISIBLE caption and already names the region when it is a plain string, so reach for this only when the caption is rich content or when the surrounding dialog title is the only thing saying which secret this is."}],usage:["DO use for a secret shown exactly once after creation (device credential, API key, service-account secret) \u2014 it masks by default and confirms the copy.","DO pair it inside a Dialog and reset via controlled `revealed`/`onRevealedChange` (or let it re-blur automatically when the `secret` prop changes) so a reopened dialog starts masked.","DO pass `onAcknowledge` to gate the dialog close behind an explicit 'I've saved it' confirmation.","DON'T use it for an editable password field \u2014 that's PasswordInput. CredentialReveal is read-only display of an issued secret.","DON'T hand-roll the copy button + copied-state + aria-live announcement; it's built in."],useCases:["Device credential issued after enrollment","API key / personal access token shown once on creation","Service-account secret / client secret reveal","Recovery code or one-time bootstrap password"],related:["PasswordInput \u2014 editable password/secret ENTRY with a show/hide toggle (data-entry); CredentialReveal is read-only DISPLAY of an issued secret.","Dialog \u2014 the modal CredentialReveal is designed to live inside.","Alert \u2014 the caution banner CredentialReveal composes internally."],example:`import { CredentialReveal } from "@godxjp/ui/data-display";
|
|
788
788
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@godxjp/ui/feedback";
|
|
789
789
|
|
|
790
790
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
@@ -931,7 +931,7 @@ const form = useForm({ customer_nm: "", action_mode: "regist" });
|
|
|
931
931
|
step={1}
|
|
932
932
|
prefix="\xA5"
|
|
933
933
|
aria-label="\u6570\u91CF"
|
|
934
|
-
/>`},{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";
|
|
934
|
+
/>`},{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."},{name:"inputClassName",type:"string",description:"Class on the `<input>` itself. SearchInput renders a WRAPPER (label, icon, clear button, input), so `className` lands on that wrapper and never reaches the field \u2014 this is the second handle, for the case where the field and its chrome need different treatment. Layout and colour still belong to tokens; use it for the rare geometry a token cannot reach."}],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";
|
|
935
935
|
|
|
936
936
|
<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",absorbed:["Combobox","Autocomplete","CountrySelect","SearchSelect","Typeahead","AsyncSelect"],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. The popup list of either mode renders as `Command split` (rows ruled, list padding 0, gh#699); retune it with --command-item-divider-color / --command-item-divider-width."},{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. The popup opens once every running ancestor animation has finished (a Popover sliding in), so inside a Popover the listbox is placed below its settled trigger exactly like a click-open (gh#708); with nothing animating it is open on the first render. onOpenChange is not called for this initial open."},{name:"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.","A Select is safe inside a draggable element (a Kanban card with draggable=true) and inside a `contain: paint` / `transform` app region: the aria-hidden native <select> fallback is held at its static position beside the trigger (position: absolute, 1px clipped), so the browser's drag image stays the card's own box instead of reaching to the region's corner (gh#708). No wrapper or consumer CSS is needed.","DO name the control with FormField, aria-label, or a <label htmlFor> pointing at the trigger id \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 {
|
|
937
937
|
Select,
|
|
@@ -1031,7 +1031,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1031
1031
|
</SelectContent>
|
|
1032
1032
|
</Select>
|
|
1033
1033
|
);
|
|
1034
|
-
}`,storyPath:"data-entry/Select.stories.tsx",rules:[3,6,23]},{name:"Switch",group:"data-entry",tagline:'Toggle switch (bare), on react-aria-components. For a labelled row use Field. `role="switch"` is the real `<input>`; the painted track is the `<label>` around it, and that is where `data-state`/`data-size` live.',props:[{name:"loading",type:"boolean",defaultValue:"false",description:"antd `loading` \u2014 the toggle is mid-flight: a spinner replaces the thumb glyph and the control refuses the change. It reports `aria-busy`/`aria-disabled` rather than `disabled`, so a keyboard user's focus is not thrown to the next field the instant they flip a switch that saves over the network."},{name:"checkedChildren",type:"React.ReactNode",description:"antd `checkedChildren` \u2014 content shown INSIDE the track while on (`\u6709\u52B9`, `ON`, a glyph). Rendered aria-hidden: `role=switch` + `aria-checked` already say on/off."},{name:"unCheckedChildren",type:"React.ReactNode",description:"antd `unCheckedChildren` \u2014 the same, shown while off."},{name:"checked",type:"boolean",description:"Controlled checked state."},{name:"defaultChecked",type:"boolean",defaultValue:"false",description:"Uncontrolled initial state. Switch keeps its own state from here, so an uncontrolled toggle works without Field."},{name:"onCheckedChange",type:"(checked: boolean) => void",description:"Fires when toggled."},{name:"size",type:'"sm" | "md"',defaultValue:'"md"',description:"Thumb size \u2014 'sm' for dense rows."},{name:"id",type:"string",description:"Links to a <Label htmlFor>."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disable the toggle."}],usage:["DO use Switch (bare) only when you are building a custom inline toggle without a visible label \u2014 e.g., a DataTable row action column. Always pair it with a <Label htmlFor={id}> placed adjacent in the DOM; never leave it label-less for screen readers.",'DO pass `name` when the toggle must submit: Switch itself renders `<input type="hidden" name value="1|0">` beside the control, so a native <form> carries the value with no extra wiring. (The old advice here \u2014 that a bare Switch drops `name` \u2014 was never true of this component.)',"DO use the `size` prop ('sm' | 'md') to control thumb size. 'sm' is appropriate in dense DataTable rows or filter bars; omit it (defaults to 'md') everywhere else.","DO wire controlled state: pass both `checked` and `onCheckedChange` together. For uncontrolled use pass `defaultChecked` (or neither) \u2014 Switch holds that state itself; Field is not required for it.","DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field \u2014 that is exactly what Field provides, including aria-describedby, aria-invalid, error/helper text, and the hidden input. Reach for Field instead.","DO link the switch to its label via matching `id` on Switch and `htmlFor` on Label. Without this pairing, clicking the label text does not toggle the switch and the a11y association is broken."],useCases:["Inline toggle in a DataTable action cell (e.g., 'Active' column) where the label is already provided by the column header and no form submission is involved.","Settings panel where a React state boolean is toggled immediately via an optimistic API call \u2014 no <form> submit, so Field's hidden input is unnecessary.","Custom compound component where you compose Switch + Label yourself and need to put your own aria-* or data-* attributes on the control.","Filter toolbar toggle (e.g., 'Show archived') rendered inline next to other filter controls, using size='sm' for density parity with adjacent inputs.","Preview/demo UI where the switch controls a local display state (dark-mode preview, feature flag preview) with no server persistence."],related:["Field \u2014 use this instead of a bare Switch whenever the toggle needs a visible label, a description, a pressable help affordance (`labelAddon`) or a validation message (`error`, which wires aria-invalid / aria-errormessage / aria-describedby for you, gh#812). Native submission is the Switch's own job: give it `name` and it renders the hidden input itself.","Checkbox \u2014 use Checkbox (or CheckboxGroup) when the user is selecting one or more items from a set, or when the binary choice semantically means 'agree/select' rather than 'enable/disable'. Switch implies an immediate, persistent state change; Checkbox implies a form choice.","Field \u2014 use for a binary or small-set choice rendered as radio-style cards with rich descriptions, when the visual weight of a toggle is insufficient for the decision importance.","RadioGroup \u2014 use when the user must choose exactly one option from 2\u20134 mutually exclusive values; Switch is only appropriate for a single on/off boolean."],example:`import { Field, Switch } from "@godxjp/ui/data-entry";
|
|
1034
|
+
}`,storyPath:"data-entry/Select.stories.tsx",rules:[3,6,23]},{name:"Switch",group:"data-entry",tagline:'Toggle switch (bare), on react-aria-components. For a labelled row use Field. `role="switch"` is the real `<input>`; the painted track is the `<label>` around it, and that is where `data-state`/`data-size` live.',props:[{name:"loading",type:"boolean",defaultValue:"false",description:"antd `loading` \u2014 the toggle is mid-flight: a spinner replaces the thumb glyph and the control refuses the change. It reports `aria-busy`/`aria-disabled` rather than `disabled`, so a keyboard user's focus is not thrown to the next field the instant they flip a switch that saves over the network."},{name:"checkedChildren",type:"React.ReactNode",description:"antd `checkedChildren` \u2014 content shown INSIDE the track while on (`\u6709\u52B9`, `ON`, a glyph). Rendered aria-hidden: `role=switch` + `aria-checked` already say on/off."},{name:"unCheckedChildren",type:"React.ReactNode",description:"antd `unCheckedChildren` \u2014 the same, shown while off."},{name:"checked",type:"boolean",description:"Controlled checked state."},{name:"defaultChecked",type:"boolean",defaultValue:"false",description:"Uncontrolled initial state. Switch keeps its own state from here, so an uncontrolled toggle works without Field."},{name:"onCheckedChange",type:"(checked: boolean) => void",description:"Fires when toggled."},{name:"size",type:'"sm" | "md"',defaultValue:'"md"',description:"Thumb size \u2014 'sm' for dense rows."},{name:"id",type:"string",description:"Links to a <Label htmlFor>."},{name:"required",type:"boolean",defaultValue:"false",description:'ANNOUNCES the requirement; it does not enforce it. react-aria\'s Switch omits `isRequired`, and a switch is never the target of native constraint validation in this library, so this writes `aria-required="true"` onto the real input and stops there. The form layer (FormField / your schema) still owns whether an unflipped switch blocks submit \u2014 pairing this with nothing that validates is how a screen reader ends up promising a check the form never makes.'},{name:"disabled",type:"boolean",defaultValue:"false",description:"Disable the toggle."}],usage:["DO use Switch (bare) only when you are building a custom inline toggle without a visible label \u2014 e.g., a DataTable row action column. Always pair it with a <Label htmlFor={id}> placed adjacent in the DOM; never leave it label-less for screen readers.",'DO pass `name` when the toggle must submit: Switch itself renders `<input type="hidden" name value="1|0">` beside the control, so a native <form> carries the value with no extra wiring. (The old advice here \u2014 that a bare Switch drops `name` \u2014 was never true of this component.)',"DO use the `size` prop ('sm' | 'md') to control thumb size. 'sm' is appropriate in dense DataTable rows or filter bars; omit it (defaults to 'md') everywhere else.","DO wire controlled state: pass both `checked` and `onCheckedChange` together. For uncontrolled use pass `defaultChecked` (or neither) \u2014 Switch holds that state itself; Field is not required for it.","DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field \u2014 that is exactly what Field provides, including aria-describedby, aria-invalid, error/helper text, and the hidden input. Reach for Field instead.","DO link the switch to its label via matching `id` on Switch and `htmlFor` on Label. Without this pairing, clicking the label text does not toggle the switch and the a11y association is broken."],useCases:["Inline toggle in a DataTable action cell (e.g., 'Active' column) where the label is already provided by the column header and no form submission is involved.","Settings panel where a React state boolean is toggled immediately via an optimistic API call \u2014 no <form> submit, so Field's hidden input is unnecessary.","Custom compound component where you compose Switch + Label yourself and need to put your own aria-* or data-* attributes on the control.","Filter toolbar toggle (e.g., 'Show archived') rendered inline next to other filter controls, using size='sm' for density parity with adjacent inputs.","Preview/demo UI where the switch controls a local display state (dark-mode preview, feature flag preview) with no server persistence."],related:["Field \u2014 use this instead of a bare Switch whenever the toggle needs a visible label, a description, a pressable help affordance (`labelAddon`) or a validation message (`error`, which wires aria-invalid / aria-errormessage / aria-describedby for you, gh#812). Native submission is the Switch's own job: give it `name` and it renders the hidden input itself.","Checkbox \u2014 use Checkbox (or CheckboxGroup) when the user is selecting one or more items from a set, or when the binary choice semantically means 'agree/select' rather than 'enable/disable'. Switch implies an immediate, persistent state change; Checkbox implies a form choice.","Field \u2014 use for a binary or small-set choice rendered as radio-style cards with rich descriptions, when the visual weight of a toggle is insufficient for the decision importance.","RadioGroup \u2014 use when the user must choose exactly one option from 2\u20134 mutually exclusive values; Switch is only appropriate for a single on/off boolean."],example:`import { Field, Switch } from "@godxjp/ui/data-entry";
|
|
1035
1035
|
|
|
1036
1036
|
// Field, not a hand-rolled row: it owns the label-to-control id wiring, the description slot and
|
|
1037
1037
|
// the row rhythm. A <div className="flex items-center gap-2"> around a bare <Label> loses all three.
|
|
@@ -1044,7 +1044,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1044
1044
|
// Chat / comment composer \u2014 grows with its content, scrolls past 8 rows, collapses on send.
|
|
1045
1045
|
<Textarea autoGrow minRows={1} maxRows={8} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u5165\u529B..." />`,storyPath:"data-entry/Textarea.stories.tsx",rules:[]},{name:"Label",group:"data-entry",tagline:"Styled Radix Label; use htmlFor to associate with a control.",props:[{name:"htmlFor",type:"string",description:"Id of the associated control."},{name:"children",type:"ReactNode",description:"Label content."}],usage:["DO: always pass `htmlFor` matching the `id` of the associated control \u2014 this is the entire purpose of the component. Without it, clicking the label text does NOT focus or toggle the control, breaking a11y and UX.",'DO: import from `@godxjp/ui/data-entry` (not shadcn or Radix directly). The godx-ui Label extends Radix\'s LabelPrimitive with `data-slot="label"`, `select-none`, and `group-data-[disabled]` opacity-50 \u2014 hand-rolling a `<label>` loses all of these.',"DON'T: use Label as a standalone visible heading or section title. It is a form-control association primitive. For page/section headings use semantic HTML (`<h2>`, etc.) or a typography class instead.","DON'T: wrap Label around a control that is already labelled internally. FormField, Field, and CheckboxGroup all render Label internally \u2014 adding a second Label creates a duplicate association and redundant screen-reader announcement.","DO: pair Label with Checkbox or Switch when NOT using the compound wrapper (Field). In that case generate the shared id with `React.useId()` and pass it to both `id` on the control and `htmlFor` on Label.","PREFER FormField over a bare Label + control pair whenever you also need helper text, error messages, or `required` asterisk. FormField injects `aria-describedby` and `aria-invalid` automatically; a bare Label does not."],useCases:["Pairing with a standalone Checkbox when Field's two-line layout is unnecessary \u2014 e.g. a single 'Remember me' option in a login form.","Labelling a bare Switch (not Field) in a settings row where the switch is controlled by parent state and no HTML form name attribute is needed.","Adding a visible label to a custom or third-party control that accepts an `id` prop but isn't wrapped by FormField or Field.","Labelling a Textarea in a free-text form field when FormField's helper/error slots aren't needed, keeping the markup minimal.","Rendering an accessible label inside a table row where a FormField's block layout would break the inline/grid structure.","Adding a label to a DatePicker, TimePicker, or ColorPicker inside a simple layout that doesn't need the full FormField wrapper."],related:["FormField \u2014 prefer this over a bare Label whenever the field needs helper text, an error message, or a required marker; FormField renders Label internally and wires aria-describedby/aria-invalid automatically.","Field \u2014 use for a Checkbox or Radio.Item that needs a visible label and optional description line; it renders Label internally \u2014 do NOT add a second Label around it.","Field \u2014 use instead of a bare Switch + Label pair when the control must submit a value via an HTML form name; Field owns the Label + hidden input composition.","Checkbox \u2014 the most common bare-Label partner; pair with Label via shared useId() id/htmlFor when Field's layout is too heavy."],example:`import { Label } from "@godxjp/ui/data-entry";
|
|
1046
1046
|
|
|
1047
|
-
<Label htmlFor="stackable">\u4F75\u7528\u3092\u8A31\u53EF</Label>`,storyPath:"data-entry/Label.stories.tsx",rules:[]},{name:"Checkbox",subParts:["CheckboxVisual"],group:"data-entry",tagline:'Checkbox on react-aria-components; standalone or via CheckboxGroup with an options array. `role="checkbox"` is the real `<input>`; the painted box is the `<label>` around it and carries `data-state`.',props:[{name:"indeterminate",type:"boolean",description:'antd `indeterminate` \u2014 paint the PARTIAL mark (a dash) and announce `mixed`, without changing `checked`. This component also accepts the same state as `checked="indeterminate"`; the flag is antd\'s spelling of it, and the box falls back to the underlying `checked` the moment the flag goes false.'},{name:"checked",type:"boolean | 'indeterminate'",description:"Controlled checked state."},{name:"defaultChecked",type:"boolean | 'indeterminate'",description:"Uncontrolled initial state. It takes the tri-state too, and the box keeps the dash until the first click rather than falling back to unchecked."},{name:"onCheckedChange",type:"(checked) => void",description:"Fires when checked state changes."},{name:"id",type:"string",description:"Links to a <Label htmlFor>."},{name:"children",type:"React.ReactNode",description:"antd `<Checkbox>label</Checkbox>` \u2014 the INLINE label: box first, label at inline-end on the same line, the same markup as a `Checkbox.Group` option. A real `<label for>`: clicking the text toggles the box and the text is its accessible name. `className` then styles the labelled row."}],usage:['DO give a single boolean its label as children (antd `<Checkbox>label</Checkbox>`, gh#709): `<Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)}>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>` \u2014 box first, label on the same line, the text toggles the box and is its accessible name. In a form: `<FormFieldControl name="agree" valuePropName="checked">{(field) => <Checkbox {...field}>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>}</FormFieldControl>`.','DON\'T render a boolean field\'s label ABOVE its checkbox \u2014 `<FormFieldControl label="\u2026">{(field) => <Checkbox checked={field.value} \u2026 />}</FormFieldControl>` puts the label on one row and the box far below it. Put the label in the Checkbox\'s children and use `valuePropName="checked"` on the field.',"DO pair every standalone Checkbox WITHOUT children with a `<Label htmlFor={id}>` (or an `aria-label`) \u2014 the id prop on Checkbox must match the htmlFor on Label so screen readers announce the label on focus. Without this pairing the control is inaccessible.","DO use the controlled pattern (`checked` + `onCheckedChange`) for any form-bound checkbox. `onCheckedChange` receives `boolean | 'indeterminate'` \u2014 always coerce with `!!v` or an explicit guard before storing in state.","DO use `Checkbox.Group` (alias for CheckboxGroup) with the `options` prop when you have \u22652 choices from an array \u2014 it renders each item inside a `Field` (label + optional description), generates stable ids automatically, and manages the `string[]` value array. NEVER hand-roll a loop of bare `<Checkbox>` elements for a multi-select list.","DO pass `name` on `Checkbox.Group` (not on individual checkboxes) when the group must submit as form fields \u2014 the group propagates the name to each internal checkbox so the browser serialises all checked values under that key.","DON'T use `checked='indeterminate'` on `Checkbox.Group` children \u2014 indeterminate is only meaningful on a parent 'select-all' control you wire manually; the group itself does not auto-compute it.","DON'T wrap a standalone Checkbox in `Field` manually \u2014 `Field` is the internal composition primitive that `Checkbox.Group` uses. For a single boolean with a label, pass the label as children (`<Checkbox \u2026>label</Checkbox>`, the catalog example) \u2014 it renders the same row. Use `<Field id='x' label='\u2026' description='\u2026'><Checkbox id='x' \u2026 /></Field>` only when the row needs a description line \u2014 Field owns the label-to-control id wiring, the description slot and the row rhythm, and a hand-rolled flex row owns none of them; for a full labelled-checkbox with description, use `Field` directly only if you need a one-off item outside a group."],useCases:["A 'Select all' / bulk-action row above a DataTable \u2014 standalone Checkbox with `checked='indeterminate'` when some (not all) rows are selected, toggling between all-selected and none-selected.","A multi-step filter panel (e.g. filter invoices by payment status: Paid, Unpaid, Overdue) \u2014 `Checkbox.Group` with `options` prop and `orientation='vertical'`, controlled value wired to Toolbar state.","Confirmation or consent acknowledgement before a destructive action in a Dialog \u2014 standalone Checkbox with controlled state used to enable/disable the confirm Button.","Settings panel where each feature flag is a boolean toggle with a description line \u2014 `Checkbox.Group` with options carrying a `description` field so each row renders label + subtext via Field.","Bulk-edit form row in an accounting ledger (e.g. 'Apply to all selected entries') \u2014 standalone Checkbox with name + value inside a `<form>` for native HTML form submission.","Onboarding checklist (e.g. 'I have read the terms', 'I consent to data processing') with multiple distinct items whose values are independent \u2014 two separate standalone Checkboxes, each with their own id/state, not a Checkbox.Group (since each item maps to a different boolean field)."],related:["CheckboxGroup \u2014 use instead of bare Checkbox when you have a list of 2+ options from an array; it handles id generation, Field wrapping, value array management, and the `name` prop for form submission. Checkbox is for a single boolean; CheckboxGroup is for multi-select.","Switch / Field \u2014 use Switch when the action takes immediate effect (enable/disable a feature in settings) rather than selecting an option to be submitted later. Checkbox implies 'will be submitted as part of a form'; Switch implies 'applies now'. Field adds a hidden input for HTML form compatibility.","RadioGroup \u2014 use when only one option in a group may be selected at a time (mutually exclusive). CheckboxGroup = multiple selections allowed; RadioGroup = single selection only.","Field \u2014 the internal layout primitive (control slot + Label + description) that Checkbox.Group renders per item. Use it directly only when you need a one-off labelled checkbox or radio item outside of a group, and you want the consistent indent/description layout without the group's value-management overhead."],example:`import { Checkbox } from "@godxjp/ui/data-entry";
|
|
1047
|
+
<Label htmlFor="stackable">\u4F75\u7528\u3092\u8A31\u53EF</Label>`,storyPath:"data-entry/Label.stories.tsx",rules:[]},{name:"Checkbox",subParts:["CheckboxVisual"],group:"data-entry",tagline:'Checkbox on react-aria-components; standalone or via CheckboxGroup with an options array. `role="checkbox"` is the real `<input>`; the painted box is the `<label>` around it and carries `data-state`.',props:[{name:"indeterminate",type:"boolean",description:'antd `indeterminate` \u2014 paint the PARTIAL mark (a dash) and announce `mixed`, without changing `checked`. This component also accepts the same state as `checked="indeterminate"`; the flag is antd\'s spelling of it, and the box falls back to the underlying `checked` the moment the flag goes false.'},{name:"checked",type:"boolean | 'indeterminate'",description:"Controlled checked state."},{name:"defaultChecked",type:"boolean | 'indeterminate'",description:"Uncontrolled initial state. It takes the tri-state too, and the box keeps the dash until the first click rather than falling back to unchecked."},{name:"onCheckedChange",type:"(checked) => void",description:"Fires when checked state changes."},{name:"id",type:"string",description:"Links to a <Label htmlFor>."},{name:"required",type:"boolean",defaultValue:"false",description:"Keeps its HTML spelling here and becomes react-aria's `isRequired`, so unlike Switch \u2014 where the same prop only ANNOUNCES the requirement \u2014 this is real constraint validation on the underlying input: an unchecked box blocks native form submission. The consent checkbox is the case it exists for. It does not render an asterisk; the required MARK belongs to FormField's label."},{name:"children",type:"React.ReactNode",description:"antd `<Checkbox>label</Checkbox>` \u2014 the INLINE label: box first, label at inline-end on the same line, the same markup as a `Checkbox.Group` option. A real `<label for>`: clicking the text toggles the box and the text is its accessible name. `className` then styles the labelled row."}],usage:['DO give a single boolean its label as children (antd `<Checkbox>label</Checkbox>`, gh#709): `<Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)}>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>` \u2014 box first, label on the same line, the text toggles the box and is its accessible name. In a form: `<FormFieldControl name="agree" valuePropName="checked">{(field) => <Checkbox {...field}>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>}</FormFieldControl>`.','DON\'T render a boolean field\'s label ABOVE its checkbox \u2014 `<FormFieldControl label="\u2026">{(field) => <Checkbox checked={field.value} \u2026 />}</FormFieldControl>` puts the label on one row and the box far below it. Put the label in the Checkbox\'s children and use `valuePropName="checked"` on the field.',"DO pair every standalone Checkbox WITHOUT children with a `<Label htmlFor={id}>` (or an `aria-label`) \u2014 the id prop on Checkbox must match the htmlFor on Label so screen readers announce the label on focus. Without this pairing the control is inaccessible.","DO use the controlled pattern (`checked` + `onCheckedChange`) for any form-bound checkbox. `onCheckedChange` receives `boolean | 'indeterminate'` \u2014 always coerce with `!!v` or an explicit guard before storing in state.","DO use `Checkbox.Group` (alias for CheckboxGroup) with the `options` prop when you have \u22652 choices from an array \u2014 it renders each item inside a `Field` (label + optional description), generates stable ids automatically, and manages the `string[]` value array. NEVER hand-roll a loop of bare `<Checkbox>` elements for a multi-select list.","DO pass `name` on `Checkbox.Group` (not on individual checkboxes) when the group must submit as form fields \u2014 the group propagates the name to each internal checkbox so the browser serialises all checked values under that key.","DON'T use `checked='indeterminate'` on `Checkbox.Group` children \u2014 indeterminate is only meaningful on a parent 'select-all' control you wire manually; the group itself does not auto-compute it.","DON'T wrap a standalone Checkbox in `Field` manually \u2014 `Field` is the internal composition primitive that `Checkbox.Group` uses. For a single boolean with a label, pass the label as children (`<Checkbox \u2026>label</Checkbox>`, the catalog example) \u2014 it renders the same row. Use `<Field id='x' label='\u2026' description='\u2026'><Checkbox id='x' \u2026 /></Field>` only when the row needs a description line \u2014 Field owns the label-to-control id wiring, the description slot and the row rhythm, and a hand-rolled flex row owns none of them; for a full labelled-checkbox with description, use `Field` directly only if you need a one-off item outside a group."],useCases:["A 'Select all' / bulk-action row above a DataTable \u2014 standalone Checkbox with `checked='indeterminate'` when some (not all) rows are selected, toggling between all-selected and none-selected.","A multi-step filter panel (e.g. filter invoices by payment status: Paid, Unpaid, Overdue) \u2014 `Checkbox.Group` with `options` prop and `orientation='vertical'`, controlled value wired to Toolbar state.","Confirmation or consent acknowledgement before a destructive action in a Dialog \u2014 standalone Checkbox with controlled state used to enable/disable the confirm Button.","Settings panel where each feature flag is a boolean toggle with a description line \u2014 `Checkbox.Group` with options carrying a `description` field so each row renders label + subtext via Field.","Bulk-edit form row in an accounting ledger (e.g. 'Apply to all selected entries') \u2014 standalone Checkbox with name + value inside a `<form>` for native HTML form submission.","Onboarding checklist (e.g. 'I have read the terms', 'I consent to data processing') with multiple distinct items whose values are independent \u2014 two separate standalone Checkboxes, each with their own id/state, not a Checkbox.Group (since each item maps to a different boolean field)."],related:["CheckboxGroup \u2014 use instead of bare Checkbox when you have a list of 2+ options from an array; it handles id generation, Field wrapping, value array management, and the `name` prop for form submission. Checkbox is for a single boolean; CheckboxGroup is for multi-select.","Switch / Field \u2014 use Switch when the action takes immediate effect (enable/disable a feature in settings) rather than selecting an option to be submitted later. Checkbox implies 'will be submitted as part of a form'; Switch implies 'applies now'. Field adds a hidden input for HTML form compatibility.","RadioGroup \u2014 use when only one option in a group may be selected at a time (mutually exclusive). CheckboxGroup = multiple selections allowed; RadioGroup = single selection only.","Field \u2014 the internal layout primitive (control slot + Label + description) that Checkbox.Group renders per item. Use it directly only when you need a one-off labelled checkbox or radio item outside of a group, and you want the consistent indent/description layout without the group's value-management overhead."],example:`import { Checkbox } from "@godxjp/ui/data-entry";
|
|
1048
1048
|
|
|
1049
1049
|
// children is the inline label (antd): box first, label on the same line, the text toggles the box.
|
|
1050
1050
|
// Never a hand-rolled <div className="flex items-center gap-2"> around a bare <Label>.
|
|
@@ -1201,7 +1201,7 @@ import { Toaster } from "@godxjp/ui/feedback";
|
|
|
1201
1201
|
// anywhere \u2014 import toast from "sonner"
|
|
1202
1202
|
import { toast } from "sonner";
|
|
1203
1203
|
toast.success("\u30AF\u30FC\u30DD\u30F3\u3092\u516C\u958B\u3057\u307E\u3057\u305F");
|
|
1204
|
-
toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,storyPath:"feedback/Toaster.stories.tsx",rules:[]},{name:"Tabs",subParts:["TabsContent","TabsList","TabsTrigger"],group:"navigation",tagline:"Radix tab container with optional Ant-style `items` API. Pass items for the common full TabsList/TabsContent set, or compose TabsList/TabsTrigger/TabsContent manually when you need per-panel control.",props:[{name:"items",type:"{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]",description:'Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` \u2014 never a disabled one \u2014 and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant="editable-card"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` \u2014 antd\'s per-item `destroyOnHidden` is deliberately not offered, because on this component "kept mounted" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label \u2014 \u300C\u672A\u5BFE\u5FDC 12\u300D \u2014 in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces "\u672A\u5BFE\u5FDC, 12 \u4EF6\u306E\u8AB2\u984C" and never the concatenated "\u672A\u5BFE\u5FDC12". Prefer this over antd\'s answer, which is to put a Badge inside `label` \u2014 with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently.'},{name:"value",type:"string",description:"Controlled active tab key."},{name:"defaultValue",type:"string",description:"Uncontrolled initial tab key. Ignored (falls back to the first enabled item) when it names a disabled item or an unknown key."},{name:"onValueChange",type:"(value: string) => void",description:"Active-tab change handler."},{name:"variant",type:'"default" | "line" | "card" | "editable-card"',defaultValue:'"default"',description:"Trigger-strip appearance \u2014 this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all \u2014 only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) \u2014 so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`."},{name:"tabPlacement",type:'"top" | "bottom" | "start" | "end"',defaultValue:'"top"',description:'Which edge the trigger strip parks on \u2014 Ant Design 6.6.2\'s `tabPlacement` (its `tabPosition` is deprecated there), so the inline values are already RTL-logical. `start`/`end` also flip the tablist to vertical roving focus, which is what `orientation="vertical"` did on its own before; either prop still works and the other is derived from it. The strip stays FIRST in the DOM at every placement \u2014 `bottom`/`end` are a flex reversal, not a re-ordered tree. NARROW FOLD: `start`/`end` become `top`/`bottom` (arrow keys included) at or below `--tabs-placement-responsive-breakpoint-width` (48rem) \u2014 a vertical strip and its panel share one inline axis and a phone has room for one of them; Ant Design folds the same pair the same way. Set that token to `0px` to keep the strip vertical at every width.'},{name:"size",type:'"sm" | "md" | "lg"',defaultValue:'"md"',description:"Control tier of the triggers (Ant Design `size`). Expressed as the library's own control bands (--tabs-trigger-height-*/--tabs-trigger-font-size-*), so a tab strip and the Buttons beside it stay on one rhythm; `md` reproduces the previous trigger exactly."},{name:"centered",type:"boolean",description:"Ant Design `centered` \u2014 centre the strip on its own inline axis. Keeps the `safe` centring rule the strip depends on, so a strip that overflows still falls back to start alignment instead of stranding the leading tab outside the scrollport."},{name:"bodied",type:"boolean",defaultValue:"false",description:'Draw the PANEL BODY the card strip opens into, so the strip and the panel are ONE object: a surface, a border continuous with the rail, and the radius only on the two corners AWAY from the strip. `variant="card"` already repaints the active tab\'s joined edge in the surface colour (antd `genCardStyle`), but the package shipped no surface for it to merge into \u2014 measured at 1440px, `<Card>` under a card strip left an 8px gap AND a second 1px border at a 9.708px radius (two stacked boxes), `<Card variant="borderless">` and no box at all left the same 8px gap. With `bodied`: gap 0 (the body is pulled back exactly one border width so its edge and the tabs\' sit on one device row), one continuous line around the whole object, and no line at all across the active tab. HONOURED ONLY BY `card` / `editable-card` \u2014 the pill strip floats by design and the `line` strip\'s body is the `Card` it lives in (`<Card tabList>`), which is already joined; on any other variant it emits no attribute and changes no pixel. Retune it from `--tabs-panel-{background,border-width,radius,space-inset}`; `--tabs-panel-background` is read by the body AND by the merged tab edge, so the two can never disagree.'},{name:"extra",type:"React.ReactNode | { start?: React.ReactNode; end?: React.ReactNode }",description:"Ant Design `tabBarExtraContent`, renamed to the library's `extra` slot and made logical: antd's `left`/`right` keys are `start`/`end` here. A bare node goes to `end` (antd's own default). Renders a bar row beside the strip; without it \u2014 and without an add button \u2014 no extra wrapper is emitted at all. Under `bodied` the bar row aligns its contents to the JOINED edge instead of centring them, so the add button and the extra sit on the rail rather than hanging across it."},{name:"destroyOnHidden",type:"boolean",defaultValue:"true",description:"Ant Design `destroyOnHidden`. `true` (the default here, and Radix's own behaviour) unmounts a panel the moment it stops being selected. `false` keeps EVERY panel mounted and only hides the inactive ones, so a live chart, a scroll position or an unsent form draft survives a tab switch. The default is deliberately the opposite of antd's, which keeps panels mounted."},{name:"onEdit",type:'(target: string | React.MouseEvent<HTMLButtonElement>, action: "add" | "remove") => void',description:'Ant Design `onEdit`. Required for `variant="editable-card"` to grow its controls. `remove` passes the item\'s own `value`; `add` passes the click event. Tabs never mutates `items` itself \u2014 the consumer owns the list.'},{name:"addIcon",type:"React.ReactNode",description:"Ant Design `addIcon` \u2014 replaces the default + on the editable-card add button."},{name:"hideAdd",type:"boolean",description:"Ant Design `hideAdd` \u2014 keep editable-card's remove shortcuts but drop the add button."},{name:"closeIcon",type:"React.ReactNode",description:"Ant Design `removeIcon` \u2014 the strip-wide default glyph on `editable-card`'s remove shortcut. An item's own `closeIcon` still wins over it, which is antd's precedence. It replaces the glyph only: the \xD7 stays an `aria-hidden` pointer shortcut inside the tab and the announced route stays Delete/Backspace, so a custom icon never becomes a second focusable control inside a `role=\"tab\"`."},{name:"overflow",type:'"scroll" | "menu"',defaultValue:'"scroll"',description:"What the trigger strip does when there are more tabs than fit \u2014 Ant Design's `more`, mapped onto the `overflow` vocabulary Toolbar already uses for the same question rather than re-spelled. `scroll` (default, and what every strip does today) keeps one bounded row that scrolls its own inline overflow, with the active (or, under manual activation, focused) trigger re-pinned into view. `menu` keeps all of that AND puts a real named button beside the strip listing the tabs currently outside the scrollport; choosing one selects it. DIVERGES FROM ANTD DELIBERATELY: antd REMOVES the overflowing tabs from the bar, but the WAI-ARIA APG tab pattern requires the tablist to own every tab and a `display: none` tab cannot take roving focus \u2014 so here every tab stays in the strip and the menu is an ADDITIONAL pointer route, not a relocation. The default is not `menu` because switching it would change the rendered bar for every existing consumer at once."},{name:"onTabClick",type:"(value: string, event: React.MouseEvent<HTMLButtonElement>) => void",description:'Ant Design `onTabClick`. POINTER activation of a trigger, carrying the DOM event \u2014 that is what makes it a different prop from `onValueChange` rather than a second spelling of it. Keyboard activation is NOT routed here: under `activationMode="manual"` the arrow keys move focus without activating, so a key-driven "click" would be a fiction. Use `onValueChange` for the selection, whatever moved it. Note that `onValueChange` also fires when the ALREADY SELECTED tab is clicked, so `onTabClick` is not the way to detect a re-click.'},{name:"animated",type:"boolean | { inkBar?: boolean; tabPane?: boolean }",defaultValue:"{ inkBar: true, tabPane: false }",description:"Ant Design `animated`, ported from antd's own `useAnimateConfig`: `false` turns both switches off, `true` turns both ON, an object merges over `{ inkBar: true }`. `inkBar` is the `line` variant's active bar cross-fading between triggers (on by default, and the default is byte for byte what the strip already painted). `tabPane` fades the panel in when the selection moves \u2014 antd's motion really is an opacity fade and nothing else, so this is a port, not an invention; it reads `--tabs-pane-motion-duration`. Both switches are additionally off under `prefers-reduced-motion`, which antd's are not. What is NOT ported is antd's fade-OUT of the leaving pane (it parks the old node `position: absolute; inset: 0`): this component destroys a hidden panel by default, so there is usually no leaving node."},{name:"indicator",type:'{ size?: "full" | "label"; align?: "start" | "center" | "end" }',defaultValue:'{ size: "full", align: "center" }',description:"Ant Design `indicator`, governing the `line` variant's active bar only (no other variant has one). `align` keeps antd's name AND its values, which are already logical, so it reads the shared TextAlignProp vocabulary. `size` keeps antd's name with this library's values: antd takes `number | (origin) => number` \u2014 a px length or a function of the measured tab width \u2014 and neither can enter this API (a literal is what `no-arbitrary-spacing` stops; an origin function is the free-form escape hatch docs/DESIGN-AUTHORITY.md refuses). `full` is the whole trigger (antd's default, today's bar) and `label` is the trigger's content box, i.e. minus its own inline padding \u2014 which is what `size: (origin) => origin - 2 * padding` is written to produce. `align` only has anything to place once `size` is shorter than the trigger, exactly as upstream."},{name:"moreIcon",type:"React.ReactNode",description:'Ant Design `moreIcon` (its `more.icon` in 6.x; the flat prop is still published there) \u2014 the glyph on the `overflow="menu"` button. Flat here for the same reason `addIcon` and `closeIcon` are: `overflow` names the BEHAVIOUR, the icon is a slot. The button keeps its own `aria-label`, so a custom glyph never costs the control its accessible name.'},{name:"onTabScroll",type:'(info: { direction: "start" | "end" }) => void',description:"Ant Design `onTabScroll`, fired whenever the trigger strip's own scrollport moves \u2014 a swipe, a wheel, or the component re-pinning the active trigger (antd reports its own re-pins too). LOGICAL VALUES instead of antd's `left | right | top | bottom`: two of those four are just the other axis of the same event, and upstream's pair is read off the sign of an inner transform, so in an RTL strip its `left` means the opposite of what it means in LTR. `start`/`end` say the same thing on whichever axis and in whichever direction the strip is written. Only fires for the `items` API, which is the path that owns the strip element."}],usage:["DO pass `items` when all tab content is known up front \u2014 each item needs a unique `value`, trigger `label`, and panel `content`.",'When not using `items`, compose the full four-part tree \u2014 `<Tabs>` root, `<TabsList>` trigger bar, one `<TabsTrigger value="\u2026">` per tab, one `<TabsContent value="\u2026">` per matching trigger.',"DO: use `defaultValue` (uncontrolled) for simple local state; use `value` + `onValueChange` together (controlled) when the active tab is driven by URL query params, router state, or parent state. NEVER set both simultaneously.","DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",'DO: pass `orientation="vertical"` to `<Tabs>` (not to `TabsList`) for a side-rail layout \u2014 the CSS group classes on root and triggers respond automatically, so no extra className gymnastics are needed.',"DON'T: hand-roll the active-indicator underline or selected-state ring \u2014 `TabsTrigger` already applies `data-[state=active]` styles, including the token-owned indicator bar for the `line` variant. Adding your own `border-b-2 border-primary` (or a page-local `ring-0` override to remove one) breaks the design; retune --tabs-indicator-{background,size,offset} in the service theme instead.","DO trust the horizontal `TabsList` to scroll its own overflow (hidden scrollbar, swipeable) instead of clipping when tab labels \u2014 especially long localized ones (Japanese, German) \u2014 don't fit a narrow container. Don't wrap it in your own `overflow-x-auto` div or truncate labels to work around clipping; that is now the framework's job.","DON'T assume the first item is ever auto-selected when it is `disabled` \u2014 Tabs always resolves the fallback to the first ENABLED item (or none, if all are disabled). A `disabled: true` first item is safe to author without also setting `defaultValue`.",'DON\'T write your own resize/scroll-into-view effect to keep the selected tab on screen \u2014 `TabsList` observes its own size and its triggers\' `data-state` and re-pins the active (or focused, under `activationMode="manual"`) trigger with `scrollIntoView({ block: "nearest", inline: "nearest" })`, honoring `prefers-reduced-motion` and leaving a deliberate manual scroll alone. Before that, a 1440 \u2192 1024 \u2192 390 resize could strand the ACTIVE FIRST tab entirely outside the strip while it still reported `aria-selected="true"`.','DO reach for `variant="editable-card"` + `onEdit` instead of hand-rolling a closable tab bar. The \xD7 inside a tab is an `aria-hidden` pointer shortcut and the announced keyboard route is Delete/Backspace on the focused tab (`aria-keyshortcuts`) \u2014 a real <button> there is an axe failure twice over (`aria-required-children`, because a tablist may own nothing but tabs, and `nested-interactive`). The ADD button is a real button because it sits outside the tablist.',"DO use `destroyOnHidden={false}` when a hidden panel must keep state \u2014 a mounted chart, a scroll position, an unsent draft. Note it is the opposite default from Ant Design: here panels are destroyed unless you say otherwise.","DO add `bodied` whenever a `card`/`editable-card` strip stands on its own (a saved-views ribbon over a list). DON'T wrap the panel in your own `<Card>` to give it a surface \u2014 that is the measured defect `bodied` exists for: it adds a SECOND border at the Card radius over the strip\u2194panel gap and the pair reads as two stacked boxes. The one place you do not need it is inside `<Card tabList>`, where the card's own border already wraps strip and body.","DO put a tab's count in `count` (+ `countLabel`), not inside `label`. A number concatenated into the label loses the pill's size, its tone as the tab is selected/deselected, and \u2014 measured on Button in gh#734 \u2014 the accessible name: the digits run straight onto the label (\xAB\u672A\u5BFE\u5FDC12\xBB). With the slot the tab announces \"\u672A\u5BFE\u5FDC, 12 \u4EF6\u306E\u8AB2\u984C\". DON'T drop a `<Badge>` into `label` to get the same look; there is one counting API and Button, Toggle and Tabs all draw it.","DON'T go looking for antd's `tabBarGutter`, `tabBarStyle`, `renderTabBar`, `classNames`/`styles` or `more.popupRender` \u2014 each is declined on the record, not missing. The gutter between triggers is a theme knob (`--tabs-list-line-space-gap`, `--tabs-card-list-space-gap`; the pill strip has no gutter by design) because a px number is a constant, not a semantic axis; the other four exist upstream to let a consumer replace the rendered markup, and this library answers that layer with tokens (docs/DESIGN-AUTHORITY.md refuses them by name).","DON'T re-centre the strip with a `justify-center` utility. `TabsList` aligns with `safe center` on purpose: plain centring splits the overflow across BOTH edges while `scrollLeft` only ever covers the trailing one, so the leading tab ends up permanently outside the scrollport and no gesture reaches it. `safe` keeps the centred look while the tabs fit and falls back to start alignment the moment they don't."],useCases:["Detail drawers or pages that need full per-panel control \u2014 e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `Tabs` cannot pass.","Controlled tabs driven by URL search params (e.g. `?tab=history`) where the parent reads/writes the active key and passes it to `value` / `onValueChange`.",'Vertical side-rail navigation inside a `SplitPane` or settings layout where `orientation="vertical"` on the root and `variant="line"` on `TabsList` combine to produce a sidebar-style tab strip.',"Lightweight widget tabs on a dashboard card \u2014 e.g. switching a `DataTable` between 'Pending' and 'Paid' invoice views \u2014 where an uncontrolled `defaultValue` is sufficient and no URL state is needed.","Admin entity profile pages (company, partner, employee) where each `TabsContent` wraps an Inertia deferred prop panel, lazy-loading expensive data only when the tab is first activated."],related:["Steps (@godxjp/ui/navigation) \u2014 sequential wizard/progress indicator. Use Steps when order and completion state matter (multi-step forms, onboarding flows); use Tabs when panels are non-sequential and any tab can be visited freely.","Toolbar / ToolbarGroup (@godxjp/ui/navigation) \u2014 horizontal filter chip row. Visually resembles `line`-variant tabs but is semantically different: Toolbar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.","DropdownMenu (@godxjp/ui/navigation) \u2014 use for space-constrained contexts where showing all tab triggers at once is impractical (e.g. mobile overflow menu). If only 2-3 options exist and screen space is tight, a DropdownSidebar is a lighter alternative to a full tab strip."],example:`import { Tabs } from "@godxjp/ui/navigation";
|
|
1204
|
+
toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,storyPath:"feedback/Toaster.stories.tsx",rules:[]},{name:"Tabs",subParts:["TabsContent","TabsList","TabsTrigger"],group:"navigation",tagline:"Radix tab container with optional Ant-style `items` API. Pass items for the common full TabsList/TabsContent set, or compose TabsList/TabsTrigger/TabsContent manually when you need per-panel control.",props:[{name:"items",type:"{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]",description:'Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` \u2014 never a disabled one \u2014 and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant="editable-card"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` \u2014 antd\'s per-item `destroyOnHidden` is deliberately not offered, because on this component "kept mounted" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label \u2014 \u300C\u672A\u5BFE\u5FDC 12\u300D \u2014 in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces "\u672A\u5BFE\u5FDC, 12 \u4EF6\u306E\u8AB2\u984C" and never the concatenated "\u672A\u5BFE\u5FDC12". Prefer this over antd\'s answer, which is to put a Badge inside `label` \u2014 with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently.'},{name:"value",type:"string",description:"Controlled active tab key."},{name:"defaultValue",type:"string",description:"Uncontrolled initial tab key. Ignored (falls back to the first enabled item) when it names a disabled item or an unknown key."},{name:"onValueChange",type:"(value: string) => void",description:"Active-tab change handler."},{name:"variant",type:'"default" | "line" | "card" | "editable-card"',defaultValue:'"default"',description:"Trigger-strip appearance \u2014 this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all \u2014 only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) \u2014 so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`."},{name:"tabPlacement",type:'"top" | "bottom" | "start" | "end"',defaultValue:'"top"',description:'Which edge the trigger strip parks on \u2014 Ant Design 6.6.2\'s `tabPlacement` (its `tabPosition` is deprecated there), so the inline values are already RTL-logical. `start`/`end` also flip the tablist to vertical roving focus, which is what `orientation="vertical"` did on its own before; either prop still works and the other is derived from it. The strip stays FIRST in the DOM at every placement \u2014 `bottom`/`end` are a flex reversal, not a re-ordered tree. NARROW FOLD: `start`/`end` become `top`/`bottom` (arrow keys included) at or below `--tabs-placement-responsive-breakpoint-width` (48rem) \u2014 a vertical strip and its panel share one inline axis and a phone has room for one of them; Ant Design folds the same pair the same way. Set that token to `0px` to keep the strip vertical at every width.'},{name:"size",type:'"sm" | "md" | "lg"',defaultValue:'"md"',description:"Control tier of the triggers (Ant Design `size`). Expressed as the library's own control bands (--tabs-trigger-height-*/--tabs-trigger-font-size-*), so a tab strip and the Buttons beside it stay on one rhythm; `md` reproduces the previous trigger exactly."},{name:"centered",type:"boolean",description:"Ant Design `centered` \u2014 centre the strip on its own inline axis. Keeps the `safe` centring rule the strip depends on, so a strip that overflows still falls back to start alignment instead of stranding the leading tab outside the scrollport."},{name:"bodied",type:"boolean",defaultValue:"false",description:'Draw the PANEL BODY the card strip opens into, so the strip and the panel are ONE object: a surface, a border continuous with the rail, and the radius only on the two corners AWAY from the strip. `variant="card"` already repaints the active tab\'s joined edge in the surface colour (antd `genCardStyle`), but the package shipped no surface for it to merge into \u2014 measured at 1440px, `<Card>` under a card strip left an 8px gap AND a second 1px border at a 9.708px radius (two stacked boxes), `<Card variant="borderless">` and no box at all left the same 8px gap. With `bodied`: gap 0 (the body is pulled back exactly one border width so its edge and the tabs\' sit on one device row), one continuous line around the whole object, and no line at all across the active tab. HONOURED ONLY BY `card` / `editable-card` \u2014 the pill strip floats by design and the `line` strip\'s body is the `Card` it lives in (`<Card tabList>`), which is already joined; on any other variant it emits no attribute and changes no pixel. Retune it from `--tabs-panel-{background,border-width,radius,space-inset}`; `--tabs-panel-background` is read by the body AND by the merged tab edge, so the two can never disagree.'},{name:"extra",type:"React.ReactNode | { start?: React.ReactNode; end?: React.ReactNode }",description:"Ant Design `tabBarExtraContent`, renamed to the library's `extra` slot and made logical: antd's `left`/`right` keys are `start`/`end` here. A bare node goes to `end` (antd's own default). Renders a bar row beside the strip; without it \u2014 and without an add button \u2014 no extra wrapper is emitted at all. Under `bodied` the bar row aligns its contents to the JOINED edge instead of centring them, so the add button and the extra sit on the rail rather than hanging across it."},{name:"destroyOnHidden",type:"boolean",defaultValue:"true",description:"Ant Design `destroyOnHidden`. `true` (the default here, and Radix's own behaviour) unmounts a panel the moment it stops being selected. `false` keeps EVERY panel mounted and only hides the inactive ones, so a live chart, a scroll position or an unsent form draft survives a tab switch. The default is deliberately the opposite of antd's, which keeps panels mounted."},{name:"onEdit",type:'(target: string | React.MouseEvent<HTMLButtonElement>, action: "add" | "remove") => void',description:'Ant Design `onEdit`. Required for `variant="editable-card"` to grow its controls. `remove` passes the item\'s own `value`; `add` passes the click event. Tabs never mutates `items` itself \u2014 the consumer owns the list.'},{name:"addIcon",type:"React.ReactNode",description:"Ant Design `addIcon` \u2014 replaces the default + on the editable-card add button."},{name:"hideAdd",type:"boolean",description:"Ant Design `hideAdd` \u2014 keep editable-card's remove shortcuts but drop the add button."},{name:"closeIcon",type:"React.ReactNode",description:"Ant Design `removeIcon` \u2014 the strip-wide default glyph on `editable-card`'s remove shortcut. An item's own `closeIcon` still wins over it, which is antd's precedence. It replaces the glyph only: the \xD7 stays an `aria-hidden` pointer shortcut inside the tab and the announced route stays Delete/Backspace, so a custom icon never becomes a second focusable control inside a `role=\"tab\"`."},{name:"overflow",type:'"scroll" | "menu"',defaultValue:'"scroll"',description:"What the trigger strip does when there are more tabs than fit \u2014 Ant Design's `more`, mapped onto the `overflow` vocabulary Toolbar already uses for the same question rather than re-spelled. `scroll` (default, and what every strip does today) keeps one bounded row that scrolls its own inline overflow, with the active (or, under manual activation, focused) trigger re-pinned into view. `menu` keeps all of that AND puts a real named button beside the strip listing the tabs currently outside the scrollport; choosing one selects it. DIVERGES FROM ANTD DELIBERATELY: antd REMOVES the overflowing tabs from the bar, but the WAI-ARIA APG tab pattern requires the tablist to own every tab and a `display: none` tab cannot take roving focus \u2014 so here every tab stays in the strip and the menu is an ADDITIONAL pointer route, not a relocation. The default is not `menu` because switching it would change the rendered bar for every existing consumer at once."},{name:"onTabClick",type:"(value: string, event: React.MouseEvent<HTMLButtonElement>) => void",description:'Ant Design `onTabClick`. POINTER activation of a trigger, carrying the DOM event \u2014 that is what makes it a different prop from `onValueChange` rather than a second spelling of it. Keyboard activation is NOT routed here: under `activationMode="manual"` the arrow keys move focus without activating, so a key-driven "click" would be a fiction. Use `onValueChange` for the selection, whatever moved it. Note that `onValueChange` also fires when the ALREADY SELECTED tab is clicked, so `onTabClick` is not the way to detect a re-click.'},{name:"animated",type:"boolean | { inkBar?: boolean; tabPane?: boolean }",defaultValue:"{ inkBar: true, tabPane: false }",description:"Ant Design `animated`, ported from antd's own `useAnimateConfig`: `false` turns both switches off, `true` turns both ON, an object merges over `{ inkBar: true }`. `inkBar` is the `line` variant's active bar cross-fading between triggers (on by default, and the default is byte for byte what the strip already painted). `tabPane` fades the panel in when the selection moves \u2014 antd's motion really is an opacity fade and nothing else, so this is a port, not an invention; it reads `--tabs-pane-motion-duration`. Both switches are additionally off under `prefers-reduced-motion`, which antd's are not. What is NOT ported is antd's fade-OUT of the leaving pane (it parks the old node `position: absolute; inset: 0`): this component destroys a hidden panel by default, so there is usually no leaving node."},{name:"indicator",type:'{ size?: "full" | "label"; align?: "start" | "center" | "end" }',defaultValue:'{ size: "full", align: "center" }',description:"Ant Design `indicator`, governing the `line` variant's active bar only (no other variant has one). `align` keeps antd's name AND its values, which are already logical, so it reads the shared TextAlignProp vocabulary. `size` keeps antd's name with this library's values: antd takes `number | (origin) => number` \u2014 a px length or a function of the measured tab width \u2014 and neither can enter this API (a literal is what `no-arbitrary-spacing` stops; an origin function is the free-form escape hatch docs/DESIGN-AUTHORITY.md refuses). `full` is the whole trigger (antd's default, today's bar) and `label` is the trigger's content box, i.e. minus its own inline padding \u2014 which is what `size: (origin) => origin - 2 * padding` is written to produce. `align` only has anything to place once `size` is shorter than the trigger, exactly as upstream."},{name:"moreIcon",type:"React.ReactNode",description:'Ant Design `moreIcon` (its `more.icon` in 6.x; the flat prop is still published there) \u2014 the glyph on the `overflow="menu"` button. Flat here for the same reason `addIcon` and `closeIcon` are: `overflow` names the BEHAVIOUR, the icon is a slot. The button keeps its own `aria-label`, so a custom glyph never costs the control its accessible name.'},{name:"onTabScroll",type:'(info: { direction: "start" | "end" }) => void',description:"Ant Design `onTabScroll`, fired whenever the trigger strip's own scrollport moves \u2014 a swipe, a wheel, or the component re-pinning the active trigger (antd reports its own re-pins too). LOGICAL VALUES instead of antd's `left | right | top | bottom`: two of those four are just the other axis of the same event, and upstream's pair is read off the sign of an inner transform, so in an RTL strip its `left` means the opposite of what it means in LTR. `start`/`end` say the same thing on whichever axis and in whichever direction the strip is written. Only fires for the `items` API, which is the path that owns the strip element."},{name:"listClassName",type:"string",description:"Class on the TRIGGER STRIP (`TabsList`) under the `items` API \u2014 the handle that composing the tree manually gives you as `<TabsList className>`. `className` reaches only the root, which holds the strip AND the panels, so anything meant for the bar alone belongs here. Almost always unnecessary: placement, size, centring and the card rail are props and `--tabs-*` tokens."},{name:"contentClassName",type:"string",description:"Class on EVERY panel (`TabsContent`) under the `items` API. It is written so it can WIN: the joined card body travels to CSS as `data-bodied` on the root rather than as a class, precisely so a consumer class on the panel is not fighting a utility the component already claimed (gh#762). Reach for the `bodied` prop and the `--tabs-panel-*` tokens first \u2014 this is for the geometry no token exposes."}],usage:["DO pass `items` when all tab content is known up front \u2014 each item needs a unique `value`, trigger `label`, and panel `content`.",'When not using `items`, compose the full four-part tree \u2014 `<Tabs>` root, `<TabsList>` trigger bar, one `<TabsTrigger value="\u2026">` per tab, one `<TabsContent value="\u2026">` per matching trigger.',"DO: use `defaultValue` (uncontrolled) for simple local state; use `value` + `onValueChange` together (controlled) when the active tab is driven by URL query params, router state, or parent state. NEVER set both simultaneously.","DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",'DO: pass `orientation="vertical"` to `<Tabs>` (not to `TabsList`) for a side-rail layout \u2014 the CSS group classes on root and triggers respond automatically, so no extra className gymnastics are needed.',"DON'T: hand-roll the active-indicator underline or selected-state ring \u2014 `TabsTrigger` already applies `data-[state=active]` styles, including the token-owned indicator bar for the `line` variant. Adding your own `border-b-2 border-primary` (or a page-local `ring-0` override to remove one) breaks the design; retune --tabs-indicator-{background,size,offset} in the service theme instead.","DO trust the horizontal `TabsList` to scroll its own overflow (hidden scrollbar, swipeable) instead of clipping when tab labels \u2014 especially long localized ones (Japanese, German) \u2014 don't fit a narrow container. Don't wrap it in your own `overflow-x-auto` div or truncate labels to work around clipping; that is now the framework's job.","DON'T assume the first item is ever auto-selected when it is `disabled` \u2014 Tabs always resolves the fallback to the first ENABLED item (or none, if all are disabled). A `disabled: true` first item is safe to author without also setting `defaultValue`.",'DON\'T write your own resize/scroll-into-view effect to keep the selected tab on screen \u2014 `TabsList` observes its own size and its triggers\' `data-state` and re-pins the active (or focused, under `activationMode="manual"`) trigger with `scrollIntoView({ block: "nearest", inline: "nearest" })`, honoring `prefers-reduced-motion` and leaving a deliberate manual scroll alone. Before that, a 1440 \u2192 1024 \u2192 390 resize could strand the ACTIVE FIRST tab entirely outside the strip while it still reported `aria-selected="true"`.','DO reach for `variant="editable-card"` + `onEdit` instead of hand-rolling a closable tab bar. The \xD7 inside a tab is an `aria-hidden` pointer shortcut and the announced keyboard route is Delete/Backspace on the focused tab (`aria-keyshortcuts`) \u2014 a real <button> there is an axe failure twice over (`aria-required-children`, because a tablist may own nothing but tabs, and `nested-interactive`). The ADD button is a real button because it sits outside the tablist.',"DO use `destroyOnHidden={false}` when a hidden panel must keep state \u2014 a mounted chart, a scroll position, an unsent draft. Note it is the opposite default from Ant Design: here panels are destroyed unless you say otherwise.","DO add `bodied` whenever a `card`/`editable-card` strip stands on its own (a saved-views ribbon over a list). DON'T wrap the panel in your own `<Card>` to give it a surface \u2014 that is the measured defect `bodied` exists for: it adds a SECOND border at the Card radius over the strip\u2194panel gap and the pair reads as two stacked boxes. The one place you do not need it is inside `<Card tabList>`, where the card's own border already wraps strip and body.","DO put a tab's count in `count` (+ `countLabel`), not inside `label`. A number concatenated into the label loses the pill's size, its tone as the tab is selected/deselected, and \u2014 measured on Button in gh#734 \u2014 the accessible name: the digits run straight onto the label (\xAB\u672A\u5BFE\u5FDC12\xBB). With the slot the tab announces \"\u672A\u5BFE\u5FDC, 12 \u4EF6\u306E\u8AB2\u984C\". DON'T drop a `<Badge>` into `label` to get the same look; there is one counting API and Button, Toggle and Tabs all draw it.","DON'T go looking for antd's `tabBarGutter`, `tabBarStyle`, `renderTabBar`, `classNames`/`styles` or `more.popupRender` \u2014 each is declined on the record, not missing. The gutter between triggers is a theme knob (`--tabs-list-line-space-gap`, `--tabs-card-list-space-gap`; the pill strip has no gutter by design) because a px number is a constant, not a semantic axis; the other four exist upstream to let a consumer replace the rendered markup, and this library answers that layer with tokens (docs/DESIGN-AUTHORITY.md refuses them by name).","DON'T re-centre the strip with a `justify-center` utility. `TabsList` aligns with `safe center` on purpose: plain centring splits the overflow across BOTH edges while `scrollLeft` only ever covers the trailing one, so the leading tab ends up permanently outside the scrollport and no gesture reaches it. `safe` keeps the centred look while the tabs fit and falls back to start alignment the moment they don't."],useCases:["Detail drawers or pages that need full per-panel control \u2014 e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `Tabs` cannot pass.","Controlled tabs driven by URL search params (e.g. `?tab=history`) where the parent reads/writes the active key and passes it to `value` / `onValueChange`.",'Vertical side-rail navigation inside a `SplitPane` or settings layout where `orientation="vertical"` on the root and `variant="line"` on `TabsList` combine to produce a sidebar-style tab strip.',"Lightweight widget tabs on a dashboard card \u2014 e.g. switching a `DataTable` between 'Pending' and 'Paid' invoice views \u2014 where an uncontrolled `defaultValue` is sufficient and no URL state is needed.","Admin entity profile pages (company, partner, employee) where each `TabsContent` wraps an Inertia deferred prop panel, lazy-loading expensive data only when the tab is first activated."],related:["Steps (@godxjp/ui/navigation) \u2014 sequential wizard/progress indicator. Use Steps when order and completion state matter (multi-step forms, onboarding flows); use Tabs when panels are non-sequential and any tab can be visited freely.","Toolbar / ToolbarGroup (@godxjp/ui/navigation) \u2014 horizontal filter chip row. Visually resembles `line`-variant tabs but is semantically different: Toolbar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.","DropdownMenu (@godxjp/ui/navigation) \u2014 use for space-constrained contexts where showing all tab triggers at once is impractical (e.g. mobile overflow menu). If only 2-3 options exist and screen space is tight, a DropdownSidebar is a lighter alternative to a full tab strip."],example:`import { Tabs } from "@godxjp/ui/navigation";
|
|
1205
1205
|
|
|
1206
1206
|
<Tabs
|
|
1207
1207
|
defaultValue="overview"
|
|
@@ -1296,7 +1296,7 @@ export function CutoffTimeForm() {
|
|
|
1296
1296
|
<Button type="submit">Save</Button>
|
|
1297
1297
|
</form>
|
|
1298
1298
|
);
|
|
1299
|
-
}`,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";
|
|
1299
|
+
}`,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`)."},{name:"aria-label",type:"string",description:"Accessible name for the COMBOBOX TRIGGER \u2014 the element a keyboard user lands on, not the panel. Inside a FormField it is injected for you and you do not pass it; on a bare Cascader (a toolbar scope filter, a compact drilldown with no label row) it is the only name the control has, and cardinal rule 227 requires one. Route it through t(). The rest of the field-a11y contract \u2014 aria-labelledby / describedby / errormessage / invalid / required \u2014 is accepted on every form-capable component here and is FormField's to wire."}],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";
|
|
1300
1300
|
|
|
1301
1301
|
const REGIONS = [
|
|
1302
1302
|
{
|
|
@@ -1434,7 +1434,7 @@ export function DepartmentFilter() {
|
|
|
1434
1434
|
placeholder="Filter by department\u2026"
|
|
1435
1435
|
/>
|
|
1436
1436
|
);
|
|
1437
|
-
}`,storyPath:"data-entry/TreeSelect.stories.tsx",rules:[3,6,13,23]},{name:"Transfer",group:"data-entry",tagline:"Dual-list shuttle that moves items between source and target via Checkbox selection \u2014 you own targetKeys state; never hand-roll a two-panel picker.",props:[{name:"value",type:"string[]",description:"Canonical controlled target keys; takes precedence over targetKeys."},{name:"defaultValue",type:"string[]",description:"Initial uncontrolled target keys."},{name:"defaultTargetKeys",type:"string[]",description:"Compatibility name for uncontrolled initial target keys."},{name:"name",type:"string",description:"Native form name; repeats once per target key."},{name:"readOnly",type:"boolean",description:"Preserve assignment while preventing edits."},{name:"pagination",type:"boolean | { pageSize?: number }",description:"Independent pane pages; select-all affects visible enabled rows."},{name:"showSelectAll",type:"boolean",description:"Show each pane select-all control; defaults true."},{name:"filterOption",type:"(query: string, item: TransferItemProp) => boolean",description:"Custom search predicate."},{name:"render",type:"(item: TransferItemProp) => ReactNode",description:"Custom non-interactive row content; checkbox labels remain associated."},{name:"dataSource",type:"TransferItemProp[]",required:!0,description:"Full flat list of all items (both source and target). Each item needs a unique `key` string, a `title` (ReactNode rendered in the list row), an optional `description` (shown as a secondary line), and an optional `disabled` boolean to lock individual items."},{name:"targetKeys",type:"string[]",required:!0,description:"Keys of items currently in the right (target) panel. Items whose key is NOT in this array appear in the left (source) panel. This is the primary controlled state \u2014 you must update it inside `onChange`."},{name:"onChange",type:"(targetKeys: string[], direction: 'left' | 'right', moveKeys: string[]) => void",description:"Called after the user clicks a move button. Receives the new full targetKeys array, the direction of movement ('right' = source\u2192target, 'left' = target\u2192source), and the keys that were actually moved. Update your targetKeys state here."},{name:"titles",type:"[React.ReactNode, React.ReactNode]",description:"Panel header labels. Index 0 = left/source panel, index 1 = right/target panel. Defaults to i18n strings (dataEntry.transfer.source / dataEntry.transfer.target)."},{name:"showSearch",type:"boolean",defaultValue:"false",description:"When true, renders a SearchInput inside each panel that filters items by title and description text (debounce=0). Does not affect the underlying data; purely a client-side filter."},{name:"oneWay",type:"boolean",defaultValue:"false",description:"When true, hides the left-pointing move button so items can only flow source \u2192 target. Useful for append-only assignment flows."},{name:"disabled",type:"DisabledProp (boolean)",defaultValue:"false",description:"Disables the entire component: all checkboxes, the search input (pointer-events-none), and both move buttons."},{name:"className",type:"string",description:"Extra Tailwind classes applied to the outer flex wrapper. Use to constrain width or add margin."},{name:"selectedKeys",type:"[string[], string[]]",description:"Controlled selection state as a tuple: index 0 = keys checked in the source panel, index 1 = keys checked in the target panel. Omit to use internal (uncontrolled) selection state. Must be paired with `onSelectChange` when provided."},{name:"onSelectChange",type:"(sourceSelectedKeys: string[], targetSelectedKeys: string[]) => void",description:"Called whenever the checked selection in either panel changes. Provides updated arrays for source and target selections. Required when `selectedKeys` is controlled."},{name:"onValueChange",type:'(targetKeys: string[], direction: "left" | "right", moveKeys: string[]) => void',description:"Fires when items move between panels; you own `targetKeys` state."}],usage:["DO own `targetKeys` in state and update it inside `onChange`: `const [targetKeys, setTargetKeys] = useState<string[]>([]); onValueChange={(next) => setTargetKeys(next)}`.","DO NOT hand-roll a two-panel checkbox picker \u2014 Transfer ships the full shuttle UX (select-all header, indeterminate state, search, move buttons, empty state) out of the box.","DO enable `showSearch` for lists longer than ~10 items; the built-in SearchInput filters by both `title` and `description` text content, including ReactNode content via `reactNodeText`.","DO use `oneWay={true}` for append-only flows (e.g. adding permissions to a role) where items must never be moved back.","DO control `selectedKeys` / `onSelectChange` only when you need to read which items are currently checked (e.g. for a bulk-action toolbar outside the component). For most cases, leave both props out and let Transfer manage selection internally.","AVOID using Transfer for simple single-select or toggle scenarios \u2014 use a Checkbox list or Select multiple instead. Transfer is specifically for shuttle/dual-panel assignment flows."],useCases:["Assigning roles or permissions to a user: source panel shows available roles, target panel shows assigned roles; `oneWay={false}` allows removal.","Building a report column picker: source = all available columns, target = columns included in the report, user orders and moves them across.","Account mapping in an accounting app: map external chart-of-accounts entries (source) to canonical internal accounts (target) in a bulk import wizard.","Tag / label assignment in a CMS: move content tags from an available pool into a 'selected' set for a document.","Feature-flag targeting: move user segments from an 'all segments' list into the 'targeted segments' panel for a flag.","Permission set builder in an admin UI: shuttle individual API scopes from 'available' to 'granted' for an API key or OAuth client."],related:["MultiSelect \u2014 picks multiple values from a dropdown; prefer when the option set is large and a panel layout is not needed.","Checkbox (list) \u2014 use for a simple flat multi-select without a shuttle/move metaphor.","Select (compound) \u2014 single or multi-value dropdown; not a dual-panel component.","Tree \u2014 hierarchical item display; combine with Transfer's dataSource if items have a tree structure but the shuttle UX is still needed."],example:`import { useState } from "react";
|
|
1437
|
+
}`,storyPath:"data-entry/TreeSelect.stories.tsx",rules:[3,6,13,23]},{name:"Transfer",group:"data-entry",tagline:"Dual-list shuttle that moves items between source and target via Checkbox selection \u2014 you own targetKeys state; never hand-roll a two-panel picker.",props:[{name:"value",type:"string[]",description:"Canonical controlled target keys; takes precedence over targetKeys."},{name:"defaultValue",type:"string[]",description:"Initial uncontrolled target keys."},{name:"defaultTargetKeys",type:"string[]",description:"Compatibility name for uncontrolled initial target keys."},{name:"name",type:"string",description:"Native form name; repeats once per target key."},{name:"readOnly",type:"boolean",description:"Preserve assignment while preventing edits."},{name:"pagination",type:"boolean | { pageSize?: number }",description:"Independent pane pages; select-all affects visible enabled rows."},{name:"showSelectAll",type:"boolean",description:"Show each pane select-all control; defaults true."},{name:"filterOption",type:"(query: string, item: TransferItemProp) => boolean",description:"Custom search predicate."},{name:"render",type:"(item: TransferItemProp) => ReactNode",description:"Custom non-interactive row content; checkbox labels remain associated."},{name:"dataSource",type:"TransferItemProp[]",required:!0,description:"Full flat list of all items (both source and target). Each item needs a unique `key` string, a `title` (ReactNode rendered in the list row), an optional `description` (shown as a secondary line), and an optional `disabled` boolean to lock individual items."},{name:"targetKeys",type:"string[]",required:!0,description:"Keys of items currently in the right (target) panel. Items whose key is NOT in this array appear in the left (source) panel. This is the primary controlled state \u2014 you must update it inside `onChange`."},{name:"onChange",type:"(targetKeys: string[], direction: 'left' | 'right', moveKeys: string[]) => void",description:"Called after the user clicks a move button. Receives the new full targetKeys array, the direction of movement ('right' = source\u2192target, 'left' = target\u2192source), and the keys that were actually moved. Update your targetKeys state here."},{name:"titles",type:"[React.ReactNode, React.ReactNode]",description:"Panel header labels. Index 0 = left/source panel, index 1 = right/target panel. Defaults to i18n strings (dataEntry.transfer.source / dataEntry.transfer.target)."},{name:"showSearch",type:"boolean",defaultValue:"false",description:"When true, renders a SearchInput inside each panel that filters items by title and description text (debounce=0). Does not affect the underlying data; purely a client-side filter."},{name:"oneWay",type:"boolean",defaultValue:"false",description:"When true, hides the left-pointing move button so items can only flow source \u2192 target. Useful for append-only assignment flows."},{name:"disabled",type:"DisabledProp (boolean)",defaultValue:"false",description:"Disables the entire component: all checkboxes, the search input (pointer-events-none), and both move buttons."},{name:"className",type:"string",description:"Extra Tailwind classes applied to the outer flex wrapper. Use to constrain width or add margin."},{name:"id",type:"string",description:'Lands on the `role="group"` shuttle container, not on any one input \u2014 a two-pane shuttle has no single labelable control, so this is what a FormField label points at. FormField injects it; pass it yourself only for a bare Transfer.'},{name:"selectedKeys",type:"[string[], string[]]",description:"Controlled selection state as a tuple: index 0 = keys checked in the source panel, index 1 = keys checked in the target panel. Omit to use internal (uncontrolled) selection state. Must be paired with `onSelectChange` when provided."},{name:"onSelectChange",type:"(sourceSelectedKeys: string[], targetSelectedKeys: string[]) => void",description:"Called whenever the checked selection in either panel changes. Provides updated arrays for source and target selections. Required when `selectedKeys` is controlled."},{name:"onValueChange",type:'(targetKeys: string[], direction: "left" | "right", moveKeys: string[]) => void',description:"Fires when items move between panels; you own `targetKeys` state."}],usage:["DO own `targetKeys` in state and update it inside `onChange`: `const [targetKeys, setTargetKeys] = useState<string[]>([]); onValueChange={(next) => setTargetKeys(next)}`.","DO NOT hand-roll a two-panel checkbox picker \u2014 Transfer ships the full shuttle UX (select-all header, indeterminate state, search, move buttons, empty state) out of the box.","DO enable `showSearch` for lists longer than ~10 items; the built-in SearchInput filters by both `title` and `description` text content, including ReactNode content via `reactNodeText`.","DO use `oneWay={true}` for append-only flows (e.g. adding permissions to a role) where items must never be moved back.","DO control `selectedKeys` / `onSelectChange` only when you need to read which items are currently checked (e.g. for a bulk-action toolbar outside the component). For most cases, leave both props out and let Transfer manage selection internally.","AVOID using Transfer for simple single-select or toggle scenarios \u2014 use a Checkbox list or Select multiple instead. Transfer is specifically for shuttle/dual-panel assignment flows."],useCases:["Assigning roles or permissions to a user: source panel shows available roles, target panel shows assigned roles; `oneWay={false}` allows removal.","Building a report column picker: source = all available columns, target = columns included in the report, user orders and moves them across.","Account mapping in an accounting app: map external chart-of-accounts entries (source) to canonical internal accounts (target) in a bulk import wizard.","Tag / label assignment in a CMS: move content tags from an available pool into a 'selected' set for a document.","Feature-flag targeting: move user segments from an 'all segments' list into the 'targeted segments' panel for a flag.","Permission set builder in an admin UI: shuttle individual API scopes from 'available' to 'granted' for an API key or OAuth client."],related:["MultiSelect \u2014 picks multiple values from a dropdown; prefer when the option set is large and a panel layout is not needed.","Checkbox (list) \u2014 use for a simple flat multi-select without a shuttle/move metaphor.","Select (compound) \u2014 single or multi-value dropdown; not a dual-panel component.","Tree \u2014 hierarchical item display; combine with Transfer's dataSource if items have a tree structure but the shuttle UX is still needed."],example:`import { useState } from "react";
|
|
1438
1438
|
import { Transfer } from "@godxjp/ui/data-entry";
|
|
1439
1439
|
|
|
1440
1440
|
const ALL_ACCOUNTS = [
|
|
@@ -1457,7 +1457,7 @@ export function AccountMapping() {
|
|
|
1457
1457
|
showSearch
|
|
1458
1458
|
/>
|
|
1459
1459
|
);
|
|
1460
|
-
}`,storyPath:"data-entry/Transfer.stories.tsx",rules:[23,31]},{name:"Upload",group:"data-entry",tagline:"Drag-and-drop / button / avatar / picture file uploader in six variants \u2014 wire onUpload to your media-service and call collectUploadCommitActions on form submit; supports multipart forms and custom media storage.",props:[{name:"variant",type:'"dropzone" | "button" | "picture-card" | "picture" | "avatar" | "avatar-crop"',defaultValue:'"dropzone"',description:"Controls the visual rendering mode. dropzone = large dashed drop area + file list; button = compact outline button + file list; picture-card = grid of 96\xD796 image thumbnails; picture = single image preview with change/remove actions; avatar = circular single-image picker; avatar-crop = avatar with an in-dialog crop step before the item is staged."},{name:"listType",type:'"text" | "picture"',defaultValue:'"picture" for variant="picture", otherwise "text"',description:"HOW THE CHOSEN FILES ARE LISTED \u2014 antd's listType, and an axis of its own: variant decides how files are PICKED, listType decides how the picked ones are DRAWN. text = name, size and actions (the classic dropzone/button row). picture = a leading box on every row: the thumbnail when the item has a previewUrl, otherwise the glyph for its file kind (image / pdf / archive / text / generic), both boxes the same size so a mixed list keeps one row height. The glyph is decorative (aria-hidden) and carries its kind as data-file-kind for theming. antd's picture-card is variant='picture-card' here \u2014 the tile grid IS the picker there, so it is not offered as a listType."},{name:"value",type:"UploadFileItem[]",description:"Controlled list of file items. When provided the component is controlled \u2014 you own the state. Omit to run uncontrolled."},{name:"defaultValue",type:"UploadFileItem[]",description:"Initial list of file items for uncontrolled usage. Ignored once value is provided."},{name:"accept",type:"string",description:'MIME / extension accept string passed to the hidden <input type="file">. avatar/avatar-crop/picture/picture-card default to "image/*"; dropzone and button default to unrestricted.'},{name:"multiple",type:"boolean",description:"Allow multi-file selection. Auto-derived: false when maxCount is 1 (or when variant is avatar/avatar-crop/picture); otherwise true."},{name:"maxCount",type:"number",description:"Hard upper bound on the number of items. avatar/avatar-crop/picture auto-default to 1. Once the limit is reached the add button is hidden (picture-card) or additions are rejected; maxCount=1 replaces the current item."},{name:"maxSizeBytes",type:"number",description:"Files larger than this limit are rejected with localized feedback and onReject."},{name:"disabled",type:"boolean",description:"Disables all interactive surfaces (drop zone, buttons). Visual opacity + pointer-events-none applied."},{name:"removable",type:"boolean",defaultValue:"true",description:"Show the remove/delete control on each item. Set false to make uploads permanent within the session."},{name:"onUpload",type:"(file: File, item: UploadFileItem, context: UploadRequestContext) => Promise<UploadResult>",description:"Called immediately after a file is picked (before form submit). Transitions the item to status='uploading', then 'done' on resolve or 'error' on reject. Wire this to your media-service issue/PUT/complete cycle. If omitted files stay in status='idle' and the raw File object remains in item.file."},{name:"className",type:"string",description:"Extra CSS class applied to the outer wrapper div."},{name:"children",type:"React.ReactNode",description:"Custom button label for variant='button'. Falls back to the i18n 'Upload file' string."},{name:"onValueChange",type:"(items: UploadFileItemProp[]) => void",description:"Fires with the current file list."},{name:"triggerSize",type:'"default" | "md" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg"',description:'`variant="button"` only \u2014 size of the visible trigger, forwarded to Button. An icon size renders it icon-only and moves the label to `aria-label`: a 32px square beside other icon buttons rather than a 147px labelled one that outweighs them.'},{name:"triggerVariant",type:'"default" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | "link" | "bare"',defaultValue:'"outline"',description:'`variant="button"` only \u2014 visual weight of the visible trigger, forwarded to Button. Default `outline` suits a standalone form field. Pass `ghost` when the trigger sits in a toolbar row beside other icon buttons \u2014 inside a chat composer, say \u2014 where a bordered square reads as the odd one out.'},{name:"triggerIcon",type:"React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>",defaultValue:"the lucide upload arrow",description:'`variant="button"` only \u2014 the glyph the trigger draws. Pass the COMPONENT (`triggerIcon={Plus}`), not an element, exactly as `Icon`\'s `as` takes it; every lucide icon qualifies. It exists because the glyph was hard-coded and `triggerVariant` only moves emphasis, so a "create new" action that HAPPENS to upload could not carry a plus and had to announce itself as an upload (gh#734). The library still owns the class, the label spacing and the `aria-hidden`, so a swapped glyph renders at the same `--upload-row-icon-size` and never reaches the accessible name \u2014 the trigger\'s metrics cannot drift with the icon.'},{name:"readOnly",type:"boolean",description:"Displays existing files, blocks changes, preserves staged form data."},{name:"directory",type:"boolean",description:"Select a folder; each item preserves relativePath."},{name:"pastable",type:"boolean",description:"Paste clipboard files while focus is inside this Upload; text paste is untouched."},{name:"openFileDialogOnClick",type:"boolean",description:"Defaults true. Disable native dialog activation for drop-only surfaces."},{name:"name",type:"string",description:"Multipart file field name; named staged files also join native FormData."},{name:"action",type:"string | ((file: File) => string | Promise<string>)",description:"Multipart upload URL; onUpload takes precedence."},{name:"method",type:'"POST" | "PUT" | "PATCH"',description:"Request method, default POST."},{name:"headers",type:"Record<string, string>",description:"Request headers such as CSRF tokens; multipart boundaries remain browser-owned."},{name:"data",type:"Record<string, string | Blob> | ((file: File) => Record<string, string | Blob> | Promise<Record<string, string | Blob>>)",description:"Additional multipart fields, optionally resolved per file."},{name:"withCredentials",type:"boolean",description:"Send credentials with the default transport."},{name:"beforeUpload",type:"(file: File, files: File[]) => boolean | File | Blob | typeof UPLOAD_LIST_IGNORE | Promise<boolean | File | Blob | typeof UPLOAD_LIST_IGNORE>",description:"Validate or transform before upload; false stages manually; UPLOAD_LIST_IGNORE excludes. Rejections are reported."},{name:"onReject",type:"(rejection: UploadRejection) => void",description:"Reports accept, size, count, or preflight rejection."},{name:"onRemove",type:"(item: UploadFileItem) => boolean | void | Promise<boolean | void>",description:"Returning false or rejecting vetoes removal. Accepted removal aborts active upload."},{name:"onPreview",type:"(item: UploadFileItem) => void",description:"Preview action callback."},{name:"onDownload",type:"(item: UploadFileItem) => void",description:"Download action callback."},{name:"previewFile",type:"(file: File) => Promise<string>",description:"Asynchronously generate a custom thumbnail."},{name:"onDrop",type:"React.DragEventHandler<HTMLElement>",description:"Observe drop events."},{name:"showUploadList",type:"boolean",description:"Show the default file list, default true."},{name:"itemRender",type:"(node: React.ReactElement, item: UploadFileItem, items: UploadFileItem[], actions: UploadItemActions) => React.ReactNode",description:"Customize a file row while preserving its default actions."}],usage:["DO provide onUpload to auto-upload on pick. The callback must return { mediaId, previewUrl? } \u2014 the component transitions item.status through uploading \u2192 done/error automatically. Without onUpload the File object sits in item.file until you manually process it.","DO call collectUploadCommitActions(items) on form submit to get { deleteMediaIds, promoteMediaIds } for your media-service. For multipart endpoints submit File objects via FormData; never submit blob URLs as persisted media.","DO use createUploadItem(file) to build UploadFileItem objects when pre-populating value from server data (e.g. edit forms). Set status='done' and mediaId on existing server media so the draft/undo machinery tracks them correctly.","For native multipart or Inertia Form submissions, set name: staged local files are appended during the formdata event. Completed media uploads use collectUploadCommitActions instead.","Avatar/picture variants (maxCount=1) use internal soft-delete draft logic: removing an item marks it pendingDelete so the user can undo before committing. On form submit, collectUploadCommitActions converts pendingDelete \u2192 deleteMediaIds and done mediaIds \u2192 promoteMediaIds.","For avatar-crop: a crop dialog opens after pick. The cropped Blob is staged as a new UploadFileItem. The original file never enters the list \u2014 only the cropped version is passed to onUpload.","For a drawer or panel listing MIXED attachments (.png beside .json and .txt), keep variant='dropzone' and set listType='picture': the image rows draw their previewUrl as a thumbnail and every other row draws the glyph for its kind, on one row height. Do NOT switch to variant='picture' to get thumbnails \u2014 that variant also changes the picker, the default accept to image/* and maxCount to 1."],useCases:["Profile / user avatar editor: use variant='avatar-crop' so users can crop the image before upload; wire onUpload to your media-service; call collectUploadCommitActions on profile form submit to promote or delete.","Invoice / document attachment list: use variant='dropzone' with accept='.pdf,.xlsx' and maxSizeBytes to let accountants drag-drop supporting documents; show the file list with status indicators below the drop zone.","Product gallery (multiple images): use variant='picture-card' with maxCount to display a grid of thumbnails; each item gets an individual remove \u2715 button; collectUploadCommitActions on product save.","Single cover-image picker on a content form: use variant='picture' with maxCount=1 to show a preview rectangle with change/remove controls and undo-delete support.","CSV / bulk-import button in an admin table header: use variant='button' with accept='.csv' and custom children label ('Import CSV') to keep the UI compact; process item.file in the onChange handler.","Inline document replacement on an accounting record (replace, not append): use variant='avatar' (single-slot logic) or picture; onUpload returns the new mediaId; collectUploadCommitActions delivers replacesMediaId \u2192 deleteMediaIds."],related:["Input (type='file') \u2014 never hand-roll a raw file input; use Upload instead. Upload provides drag-drop, preview, upload lifecycle, and soft-delete draft.","Avatar (display-only) \u2014 the godx-ui Avatar component renders a user's existing image; use Upload variant='avatar' or 'avatar-crop' when you need the user to change it.","DataTable \u2014 unrelated to Upload but both appear together in bulk-import flows: Upload (button variant) triggers the import, DataTable shows the result."],example:`import { useState } from "react";
|
|
1460
|
+
}`,storyPath:"data-entry/Transfer.stories.tsx",rules:[23,31]},{name:"Upload",group:"data-entry",tagline:"Drag-and-drop / button / avatar / picture file uploader in six variants \u2014 wire onUpload to your media-service and call collectUploadCommitActions on form submit; supports multipart forms and custom media storage.",props:[{name:"variant",type:'"dropzone" | "button" | "picture-card" | "picture" | "avatar" | "avatar-crop"',defaultValue:'"dropzone"',description:"Controls the visual rendering mode. dropzone = large dashed drop area + file list; button = compact outline button + file list; picture-card = grid of 96\xD796 image thumbnails; picture = single image preview with change/remove actions; avatar = circular single-image picker; avatar-crop = avatar with an in-dialog crop step before the item is staged."},{name:"listType",type:'"text" | "picture"',defaultValue:'"picture" for variant="picture", otherwise "text"',description:"HOW THE CHOSEN FILES ARE LISTED \u2014 antd's listType, and an axis of its own: variant decides how files are PICKED, listType decides how the picked ones are DRAWN. text = name, size and actions (the classic dropzone/button row). picture = a leading box on every row: the thumbnail when the item has a previewUrl, otherwise the glyph for its file kind (image / pdf / archive / text / generic), both boxes the same size so a mixed list keeps one row height. The glyph is decorative (aria-hidden) and carries its kind as data-file-kind for theming. antd's picture-card is variant='picture-card' here \u2014 the tile grid IS the picker there, so it is not offered as a listType."},{name:"value",type:"UploadFileItem[]",description:"Controlled list of file items. When provided the component is controlled \u2014 you own the state. Omit to run uncontrolled."},{name:"defaultValue",type:"UploadFileItem[]",description:"Initial list of file items for uncontrolled usage. Ignored once value is provided."},{name:"accept",type:"string",description:'MIME / extension accept string passed to the hidden <input type="file">. avatar/avatar-crop/picture/picture-card default to "image/*"; dropzone and button default to unrestricted.'},{name:"multiple",type:"boolean",description:"Allow multi-file selection. Auto-derived: false when maxCount is 1 (or when variant is avatar/avatar-crop/picture); otherwise true."},{name:"maxCount",type:"number",description:"Hard upper bound on the number of items. avatar/avatar-crop/picture auto-default to 1. Once the limit is reached the add button is hidden (picture-card) or additions are rejected; maxCount=1 replaces the current item."},{name:"maxSizeBytes",type:"number",description:"Files larger than this limit are rejected with localized feedback and onReject."},{name:"disabled",type:"boolean",description:"Disables all interactive surfaces (drop zone, buttons). Visual opacity + pointer-events-none applied."},{name:"removable",type:"boolean",defaultValue:"true",description:"Show the remove/delete control on each item. Set false to make uploads permanent within the session."},{name:"onUpload",type:"(file: File, item: UploadFileItem, context: UploadRequestContext) => Promise<UploadResult>",description:"Called immediately after a file is picked (before form submit). Transitions the item to status='uploading', then 'done' on resolve or 'error' on reject. Wire this to your media-service issue/PUT/complete cycle. If omitted files stay in status='idle' and the raw File object remains in item.file."},{name:"className",type:"string",description:"Extra CSS class applied to the outer wrapper div."},{name:"id",type:"string",description:'Lands on the native `<input type="file">`, NOT on the wrapper \u2014 the hidden input is the semantic focus target, so this is what makes a `<label htmlFor>` (or FormField, which injects it) actually focus the picker. Putting it on the visible dropzone instead is the usual reason a label click does nothing.'},{name:"children",type:"React.ReactNode",description:"Custom button label for variant='button'. Falls back to the i18n 'Upload file' string."},{name:"onValueChange",type:"(items: UploadFileItemProp[]) => void",description:"Fires with the current file list."},{name:"triggerSize",type:'"default" | "md" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg"',description:'`variant="button"` only \u2014 size of the visible trigger, forwarded to Button. An icon size renders it icon-only and moves the label to `aria-label`: a 32px square beside other icon buttons rather than a 147px labelled one that outweighs them.'},{name:"triggerVariant",type:'"default" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | "link" | "bare"',defaultValue:'"outline"',description:'`variant="button"` only \u2014 visual weight of the visible trigger, forwarded to Button. Default `outline` suits a standalone form field. Pass `ghost` when the trigger sits in a toolbar row beside other icon buttons \u2014 inside a chat composer, say \u2014 where a bordered square reads as the odd one out.'},{name:"triggerIcon",type:"React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>",defaultValue:"the lucide upload arrow",description:'`variant="button"` only \u2014 the glyph the trigger draws. Pass the COMPONENT (`triggerIcon={Plus}`), not an element, exactly as `Icon`\'s `as` takes it; every lucide icon qualifies. It exists because the glyph was hard-coded and `triggerVariant` only moves emphasis, so a "create new" action that HAPPENS to upload could not carry a plus and had to announce itself as an upload (gh#734). The library still owns the class, the label spacing and the `aria-hidden`, so a swapped glyph renders at the same `--upload-row-icon-size` and never reaches the accessible name \u2014 the trigger\'s metrics cannot drift with the icon.'},{name:"readOnly",type:"boolean",description:"Displays existing files, blocks changes, preserves staged form data."},{name:"directory",type:"boolean",description:"Select a folder; each item preserves relativePath."},{name:"pastable",type:"boolean",description:"Paste clipboard files while focus is inside this Upload; text paste is untouched."},{name:"openFileDialogOnClick",type:"boolean",description:"Defaults true. Disable native dialog activation for drop-only surfaces."},{name:"name",type:"string",description:"Multipart file field name; named staged files also join native FormData."},{name:"action",type:"string | ((file: File) => string | Promise<string>)",description:"Multipart upload URL; onUpload takes precedence."},{name:"method",type:'"POST" | "PUT" | "PATCH"',description:"Request method, default POST."},{name:"headers",type:"Record<string, string>",description:"Request headers such as CSRF tokens; multipart boundaries remain browser-owned."},{name:"data",type:"Record<string, string | Blob> | ((file: File) => Record<string, string | Blob> | Promise<Record<string, string | Blob>>)",description:"Additional multipart fields, optionally resolved per file."},{name:"withCredentials",type:"boolean",description:"Send credentials with the default transport."},{name:"beforeUpload",type:"(file: File, files: File[]) => boolean | File | Blob | typeof UPLOAD_LIST_IGNORE | Promise<boolean | File | Blob | typeof UPLOAD_LIST_IGNORE>",description:"Validate or transform before upload; false stages manually; UPLOAD_LIST_IGNORE excludes. Rejections are reported."},{name:"onReject",type:"(rejection: UploadRejection) => void",description:"Reports accept, size, count, or preflight rejection."},{name:"onRemove",type:"(item: UploadFileItem) => boolean | void | Promise<boolean | void>",description:"Returning false or rejecting vetoes removal. Accepted removal aborts active upload."},{name:"onPreview",type:"(item: UploadFileItem) => void",description:"Preview action callback."},{name:"onDownload",type:"(item: UploadFileItem) => void",description:"Download action callback."},{name:"previewFile",type:"(file: File) => Promise<string>",description:"Asynchronously generate a custom thumbnail."},{name:"onDrop",type:"React.DragEventHandler<HTMLElement>",description:"Observe drop events."},{name:"showUploadList",type:"boolean",description:"Show the default file list, default true."},{name:"itemRender",type:"(node: React.ReactElement, item: UploadFileItem, items: UploadFileItem[], actions: UploadItemActions) => React.ReactNode",description:"Customize a file row while preserving its default actions."}],usage:["DO provide onUpload to auto-upload on pick. The callback must return { mediaId, previewUrl? } \u2014 the component transitions item.status through uploading \u2192 done/error automatically. Without onUpload the File object sits in item.file until you manually process it.","DO call collectUploadCommitActions(items) on form submit to get { deleteMediaIds, promoteMediaIds } for your media-service. For multipart endpoints submit File objects via FormData; never submit blob URLs as persisted media.","DO use createUploadItem(file) to build UploadFileItem objects when pre-populating value from server data (e.g. edit forms). Set status='done' and mediaId on existing server media so the draft/undo machinery tracks them correctly.","For native multipart or Inertia Form submissions, set name: staged local files are appended during the formdata event. Completed media uploads use collectUploadCommitActions instead.","Avatar/picture variants (maxCount=1) use internal soft-delete draft logic: removing an item marks it pendingDelete so the user can undo before committing. On form submit, collectUploadCommitActions converts pendingDelete \u2192 deleteMediaIds and done mediaIds \u2192 promoteMediaIds.","For avatar-crop: a crop dialog opens after pick. The cropped Blob is staged as a new UploadFileItem. The original file never enters the list \u2014 only the cropped version is passed to onUpload.","For a drawer or panel listing MIXED attachments (.png beside .json and .txt), keep variant='dropzone' and set listType='picture': the image rows draw their previewUrl as a thumbnail and every other row draws the glyph for its kind, on one row height. Do NOT switch to variant='picture' to get thumbnails \u2014 that variant also changes the picker, the default accept to image/* and maxCount to 1."],useCases:["Profile / user avatar editor: use variant='avatar-crop' so users can crop the image before upload; wire onUpload to your media-service; call collectUploadCommitActions on profile form submit to promote or delete.","Invoice / document attachment list: use variant='dropzone' with accept='.pdf,.xlsx' and maxSizeBytes to let accountants drag-drop supporting documents; show the file list with status indicators below the drop zone.","Product gallery (multiple images): use variant='picture-card' with maxCount to display a grid of thumbnails; each item gets an individual remove \u2715 button; collectUploadCommitActions on product save.","Single cover-image picker on a content form: use variant='picture' with maxCount=1 to show a preview rectangle with change/remove controls and undo-delete support.","CSV / bulk-import button in an admin table header: use variant='button' with accept='.csv' and custom children label ('Import CSV') to keep the UI compact; process item.file in the onChange handler.","Inline document replacement on an accounting record (replace, not append): use variant='avatar' (single-slot logic) or picture; onUpload returns the new mediaId; collectUploadCommitActions delivers replacesMediaId \u2192 deleteMediaIds."],related:["Input (type='file') \u2014 never hand-roll a raw file input; use Upload instead. Upload provides drag-drop, preview, upload lifecycle, and soft-delete draft.","Avatar (display-only) \u2014 the godx-ui Avatar component renders a user's existing image; use Upload variant='avatar' or 'avatar-crop' when you need the user to change it.","DataTable \u2014 unrelated to Upload but both appear together in bulk-import flows: Upload (button variant) triggers the import, DataTable shows the result."],example:`import { useState } from "react";
|
|
1461
1461
|
import { Upload, type UploadFileItem, collectUploadCommitActions } from "@godxjp/ui/data-entry";
|
|
1462
1462
|
|
|
1463
1463
|
// Example: avatar picker with server upload
|
|
@@ -2137,7 +2137,7 @@ export default function PasswordBlock() {
|
|
|
2137
2137
|
<PasswordStrength value={value} rules={rules} />
|
|
2138
2138
|
</div>
|
|
2139
2139
|
);
|
|
2140
|
-
}`,storyPath:"data-entry/PasswordStrength.stories.tsx",rules:[3,6]},{name:"InputOTP",subParts:["InputOTPGroup","InputOTPSeparator","InputOTPSlot"],group:"data-entry",tagline:"One-time-code / 2FA input (input-otp) \u2014 N single-character slots that behave as one field. Compose InputOTP > InputOTPGroup > InputOTPSlot.",props:[{name:"defaultValue",type:"string",description:"Initial uncontrolled code."},{name:"onValueChange",type:"(value: string) => void",description:"Canonical value callback, compatible with FormFieldControl."},{name:"mask",type:"boolean | string",description:"Mask filled visual slots without changing the submitted code."},{name:"formatter",type:"(value: string) => string",description:"Normalize typed and pasted codes."},{name:"size",type:'"xs" | "sm" | "md" | "lg"',description:"Shared control height tier."},{name:"status",type:'"error" | "warning"',description:"Validation appearance."},{name:"variant",type:'"outlined" | "filled" | "borderless" | "underlined"',description:"Shared field chrome."},{name:"readOnly",type:"boolean",description:"Refuse edits including paste."},{name:"maxLength",type:"number",required:!0,description:"Number of slots (e.g. 6)."},{name:"value",type:"string",description:"Controlled value."},{name:"onChange",type:"(value: string) => void",description:"Value callback (this is a true text input \u2014 onChange is the DOM-style value handler here)."},{name:"pattern",type:"string",description:"Allowed-char regex (e.g. digits only)."},{name:"align",type:'"start" | "center" | "end"',defaultValue:'"start"',description:"Main-axis alignment of the whole code row (groups + separators) inside the field. `center` is the canonical auth challenge. Before this existed, every consumer wrapped the OTP in their own flex-centring div \u2014 do not. A service that wants all code fields centred sets `--otp-container-align` once instead."}],usage:["DO set `maxLength` to the code length and render that many InputOTPSlot with sequential `index`.",'DO centre a challenge with `align="center"` on InputOTP \u2014 never with a wrapper `<div className="flex justify-center">`. The container is owned by input-otp, so a wrapper is the only thing a consumer CAN reach, which is exactly why the prop exists.',"DO wrap slots in InputOTPGroup; use InputOTPSeparator between groups (e.g. 3 + 3).","For device codes, set `appearance='grouped'` on each InputOTPGroup to render one outline per group while preserving the single hidden input, paste, caret, keyboard and screen-reader behavior.","DON'T build N separate Inputs \u2014 this is ONE field with paste, arrow-key, and caret handling built in.","DO widen the slots with `--otp-slot-size` when a challenge row must fill a wide auth panel \u2014 it defaults to the live `--control-height` tier, so re-scoping `--control-height` on the card instead would also resize the submit button and every other input in it. Set a NAMED tier (`var(--control-height-lg)`), never an ad-hoc calc offset.",'DO use `--otp-slot-inline-size` / `--otp-slot-block-size` when the code field is NOT square \u2014 a device-grant slot is taller than it is wide. They win over the `--otp-slot-size` shorthand and fall back to it, so setting neither keeps the square control tier. You rarely set them by hand inside `AuthShell preset="device-authorization"`: that preset already owns its code-field measure.',"DO drive the sign-in MFA challenge from FormField: `error` wires aria-invalid + aria-errormessage + a role=alert message onto the single field, and the slot borders turn destructive. State is never colour-only."],useCases:["2FA / OTP verification code","Email / SMS confirmation code","PIN entry","Invite / redemption code"],related:["Input (a normal single text field)","PasswordInput (masked secret field)"],example:`import { InputOTP, InputOTPGroup, InputOTPSlot } from "@godxjp/ui/data-entry";
|
|
2140
|
+
}`,storyPath:"data-entry/PasswordStrength.stories.tsx",rules:[3,6]},{name:"InputOTP",subParts:["InputOTPGroup","InputOTPSeparator","InputOTPSlot"],group:"data-entry",tagline:"One-time-code / 2FA input (input-otp) \u2014 N single-character slots that behave as one field. Compose InputOTP > InputOTPGroup > InputOTPSlot.",props:[{name:"defaultValue",type:"string",description:"Initial uncontrolled code."},{name:"onValueChange",type:"(value: string) => void",description:"Canonical value callback, compatible with FormFieldControl."},{name:"mask",type:"boolean | string",description:"Mask filled visual slots without changing the submitted code."},{name:"formatter",type:"(value: string) => string",description:"Normalize typed and pasted codes."},{name:"size",type:'"xs" | "sm" | "md" | "lg"',description:"Shared control height tier."},{name:"status",type:'"error" | "warning"',description:"Validation appearance."},{name:"variant",type:'"outlined" | "filled" | "borderless" | "underlined"',description:"Shared field chrome."},{name:"readOnly",type:"boolean",description:"Refuse edits including paste."},{name:"maxLength",type:"number",required:!0,description:"Number of slots (e.g. 6)."},{name:"value",type:"string",description:"Controlled value."},{name:"onChange",type:"(value: string) => void",description:"Value callback (this is a true text input \u2014 onChange is the DOM-style value handler here)."},{name:"pattern",type:"string",description:"Allowed-char regex (e.g. digits only)."},{name:"align",type:'"start" | "center" | "end"',defaultValue:'"start"',description:"Main-axis alignment of the whole code row (groups + separators) inside the field. `center` is the canonical auth challenge. Before this existed, every consumer wrapped the OTP in their own flex-centring div \u2014 do not. A service that wants all code fields centred sets `--otp-container-align` once instead."},{name:"onComplete",type:"(value: string) => void",description:"Fires ONCE the last slot fills, whether the user typed it or pasted the whole code. This is the auto-submit hook: a 2FA challenge with a visible submit button is a step nobody wants, and the alternative \u2014 watching `value.length === maxLength` in an effect \u2014 re-fires on every re-render. Keep the submit button anyway for the paste-then-correct case."},{name:"pasteTransformer",type:"(pasted: string) => string",description:'Rewrites CLIPBOARD text before it reaches the field. Distinct from `formatter`, which normalises every value: this one only sees a paste, which is where the junk arrives \u2014 `"123 456"`, `"code: 123456"`, a copied SMS line. Note the order the field applies them: `pattern` is matched against the RAW keystroke first, so a pattern must accept what a user actually types, not only what these two produce.'},{name:"containerClassName",type:"string",description:"Class on the ROW container `input-otp` renders (the slots' flex parent), which `className` cannot reach \u2014 `className` lands on the hidden input, because that is the real field. Prefer `align` and the `--otp-*` tokens; this is the vendored escape hatch underneath them."},{name:"pushPasswordManagerStrategy",type:'"increase-width" | "none"',description:"`input-otp`'s answer to the 1Password / LastPass badge that browsers float over a code field and that covers the last slot. `increase-width` (its default) reserves room so the badge sits beside the row; `none` turns the accommodation off, which is what a row already centred by `align` usually wants. Pure layout \u2014 it changes no value and no keyboard behaviour."},{name:"noScriptCSSFallback",type:"string | null",description:"The `<noscript>` stylesheet `input-otp` injects so the slots are still visible with JS disabled. Pass `null` to suppress it \u2014 the one real reason being a CSP that forbids inline styles and that `nonce` cannot satisfy. Leave it alone otherwise."},{name:"nonce",type:"string",description:"CSP nonce stamped on the stylesheet `input-otp` injects. Required only under a `style-src 'nonce-\u2026'` policy, where the field otherwise renders unstyled and the console reports a blocked inline style. Pass the same nonce the document was served with."},{name:"render",type:"(props: InputOTPRenderProps) => React.ReactNode",description:"`input-otp`'s headless mode: you draw the entire row from the slot state instead of composing InputOTPGroup / InputOTPSlot. It is mutually exclusive with `children` \u2014 the vendor types the two as a union and this component keeps that union. Reaching for it means giving up the slot styling, the group outline and the separator this package owns, so it is the last resort, not a customisation point."}],usage:["DO set `maxLength` to the code length and render that many InputOTPSlot with sequential `index`.",'DO centre a challenge with `align="center"` on InputOTP \u2014 never with a wrapper `<div className="flex justify-center">`. The container is owned by input-otp, so a wrapper is the only thing a consumer CAN reach, which is exactly why the prop exists.',"DO wrap slots in InputOTPGroup; use InputOTPSeparator between groups (e.g. 3 + 3).","For device codes, set `appearance='grouped'` on each InputOTPGroup to render one outline per group while preserving the single hidden input, paste, caret, keyboard and screen-reader behavior.","DON'T build N separate Inputs \u2014 this is ONE field with paste, arrow-key, and caret handling built in.","DO widen the slots with `--otp-slot-size` when a challenge row must fill a wide auth panel \u2014 it defaults to the live `--control-height` tier, so re-scoping `--control-height` on the card instead would also resize the submit button and every other input in it. Set a NAMED tier (`var(--control-height-lg)`), never an ad-hoc calc offset.",'DO use `--otp-slot-inline-size` / `--otp-slot-block-size` when the code field is NOT square \u2014 a device-grant slot is taller than it is wide. They win over the `--otp-slot-size` shorthand and fall back to it, so setting neither keeps the square control tier. You rarely set them by hand inside `AuthShell preset="device-authorization"`: that preset already owns its code-field measure.',"DO drive the sign-in MFA challenge from FormField: `error` wires aria-invalid + aria-errormessage + a role=alert message onto the single field, and the slot borders turn destructive. State is never colour-only."],useCases:["2FA / OTP verification code","Email / SMS confirmation code","PIN entry","Invite / redemption code"],related:["Input (a normal single text field)","PasswordInput (masked secret field)"],example:`import { InputOTP, InputOTPGroup, InputOTPSlot } from "@godxjp/ui/data-entry";
|
|
2141
2141
|
|
|
2142
2142
|
<InputOTP maxLength={6}>
|
|
2143
2143
|
<InputOTPGroup>
|
|
@@ -2345,7 +2345,7 @@ import { Badge } from "@godxjp/ui/data-display";
|
|
|
2345
2345
|
value={organizationId}
|
|
2346
2346
|
onValueChange={setOrganizationId}
|
|
2347
2347
|
labels={labels}
|
|
2348
|
-
/>`,storyPath:"layout/OrgSwitcher.stories.tsx",related:["AppShell (navRail) \u2014 WHERE this control goes when switching organization is constant: the rail is the docked platform-scope column, and its own prop doc is the authority on which of the three columns owns which scope. Pass the collapsed trigger; the rail is 3.5rem.","AppLauncher \u2014 the OTHER platform-scope control: which APP, not which ORGANIZATION. They compose (a launcher in the bar and a switcher in the rail is one coherent platform surface); neither replaces the other.","Sidebar \u2014 APP scope, and therefore NOT where this goes. Its `brand` slot is the app's own lockup; putting the organization switcher there mixes two scopes in one header."],rules:[]},{name:"AppLauncher",group:"layout",tagline:"Nine-dot topbar app grid \u2014 the platform-standard way to switch app (the Google Workspace shape).",props:[{name:"apps",type:"readonly AppLauncherApp[]",required:!0,description:"Ungrouped app tiles, rendered first with no heading. Each is { id, name, href, icon?, current?, external? } and each tile is a REAL <a href> \u2014 pass only apps the viewer may open; there is no disabled tile."},{name:"groups",type:"readonly AppLauncherGroup[]",description:'Labelled sections rendered after `apps` \u2014 the "more from \u2026" band. Each group is a named role="group", not a heading, so the same markup is correct inside the popover and inside the Sheet.'},{name:"labels",type:"AppLauncherLabels",required:!0,description:"Localized trigger name, panel title, empty/loading/retry copy, and the optional `externalHint` announced on an external tile (WCAG 3.2.5). `trigger` is a plain string, not a function of the current app: the nine-dot button shows no current value."},{name:"columns",type:"number",defaultValue:"3",description:"Grid column count, written inline to the `--app-launcher-columns` custom property. Omit it and `.ui-app-launcher-panel` keeps its own declaration of 3 \u2014 the default sits in the stylesheet, where a theme can reach it, rather than in a component token (the token NAME vocabulary has no word for a count)."},{name:"linkComponent",type:"SidebarLinkComponentProp",description:'Framework router link \u2014 the SAME contract `Sidebar` and `NavList` take, so `inertiaSidebarLink(Link)` / `createSidebarLink(Link, "to")` is reused verbatim. The launcher still composes the tile; `external` apps bypass it and render a plain anchor.'},{name:"loading",type:"boolean",defaultValue:"false",description:"Shows the loading state; the trigger stays openable."},{name:"error",type:"ReactNode",description:"Consumer-supplied error state."},{name:"onRetry",type:"() => void",description:"Consumer-owned retry callback."},{name:"responsive",type:'"auto" | "popover" | "sheet" | "fullscreen"',defaultValue:'"auto"',description:`Presentation contract. "auto" resolves through the SHARED Sheet hook useSheetResponsiveMode(): desktop popover above --sheet-responsive-breakpoint-width (48rem/768px), focus-trapped bottom Sheet at/below it. "fullscreen" is the LAUNCHPAD \u2014 one full-viewport modal at every width, the page behind it blurred and the grid floating on that ground (macOS Launchpad / Windows Start), with the column count stepping 3\xB74\xB75\xB76 on the house container ladder. Reach for it in a PLATFORM start bar, where the launcher is the primary way to move between products; keep "auto" for a launcher inside one app's topbar, where the grid is a shortcut and the work behind it should stay visible.`},{name:"appearance",type:'"bar" | "icon"',defaultValue:'"bar"',description:"The BOX the trigger takes \u2014 the same split AppSettingToggle draws, for the same reason. `bar` (default) is a TopbarItem: a cell as tall as the bar, whose hover IS the bar's surface. `icon` is a square ghost Button, for chrome that is NOT a bar \u2014 a nav rail (GoDX Dock puts it there), a card header, a toolbar. A TopbarItem outside a bar has nothing to bleed to: it stretches to a container that never set a band height, and its squared corners and full-bleed hover read as a broken cell rather than a control. The panel, the grid, the labels and the responsive contract are identical in both."},{name:"open",type:"boolean",description:"Controlled open state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Open-state callback."}],usage:["DO drop it straight into a Topbar slot. It renders NO wrapper element, so the trigger is the slot's own flex child and `.ui-topbar-item { align-self: stretch }` reaches the bar's height.",'DON\'T hand-roll the trigger as `Button variant="ghost"` IN A BAR. A Button in a bar is a --control-height pill floating in a taller strip, with its own hover fill and its own focus ring \u2014 the exact regression corrected in 20.0.0. In a bar the trigger is a `TopbarItem`, and that is what `appearance="bar"` (the default) renders. OUTSIDE a bar the square ghost Button is correct, and `appearance="icon"` renders it for you \u2014 still hand-roll nothing.','DO mark the app the viewer is inside with `current` \u2014 it becomes `aria-current="page"`, which is what both the tint and the announcement key off.',"DO set `external` on a destination outside this SPA. It renders a plain anchor with target/rel and skips `linkComponent`, because a client-side router link to another origin routes nowhere.",'DON\'T wrap it in your own media query to pick popover vs sheet \u2014 `responsive="auto"` already reads the shared --sheet-responsive-breakpoint-width token.',"`className`, `id` and every `data-*` land on the TRIGGER, so an end-to-end selector can hold it without depending on the localized accessible name."],useCases:["A platform with several apps (console, billing, chat, people) where the bar must offer all of them from every page.","Replacing a hand-rolled dropdown of product links in the topbar end slot."],related:["ServiceLauncherCard \u2014 the PAGE-SIZED launcher tile (status, hostname, plan, action, locked reason) for a service-catalogue page, where choosing is a considered act. AppLauncher's tile is bar-sized: mark + name, the whole tile one link, because changing app is a reflex. Neither is built out of the other; a grid of ServiceLauncherCards inside a popover is the wrong component.","AppShell (navRail) \u2014 the SAME platform scope expressed as a docked column instead of a bar control. Pick ONE: the launcher for a platform with MANY apps where switching is occasional (Google Workspace), the rail for a single product where switching workspace is constant enough to deserve permanent screen width (Slack).","OrgSwitcher \u2014 the other platform-scope control: which ORGANIZATION you are in, not which app. They compose; they do not replace each other.","TopbarItem \u2014 the bar cell the trigger is built from; use it directly for a one-off bar control."],example:`import { AppLauncher, Topbar } from "@godxjp/ui/layout";
|
|
2348
|
+
/>`,storyPath:"layout/OrgSwitcher.stories.tsx",related:["AppShell (navRail) \u2014 WHERE this control goes when switching organization is constant: the rail is the docked platform-scope column, and its own prop doc is the authority on which of the three columns owns which scope. Pass the collapsed trigger; the rail is 3.5rem.","AppLauncher \u2014 the OTHER platform-scope control: which APP, not which ORGANIZATION. They compose (a launcher in the bar and a switcher in the rail is one coherent platform surface); neither replaces the other.","Sidebar \u2014 APP scope, and therefore NOT where this goes. Its `brand` slot is the app's own lockup; putting the organization switcher there mixes two scopes in one header."],rules:[]},{name:"AppLauncher",group:"layout",tagline:"Nine-dot topbar app grid \u2014 the platform-standard way to switch app (the Google Workspace shape).",props:[{name:"apps",type:"readonly AppLauncherApp[]",required:!0,description:"Ungrouped app tiles, rendered first with no heading. Each is { id, name, href, icon?, current?, external? } and each tile is a REAL <a href> \u2014 pass only apps the viewer may open; there is no disabled tile."},{name:"groups",type:"readonly AppLauncherGroup[]",description:'Labelled sections rendered after `apps` \u2014 the "more from \u2026" band. Each group is a named role="group", not a heading, so the same markup is correct inside the popover and inside the Sheet.'},{name:"labels",type:"AppLauncherLabels",required:!0,description:"Localized trigger name, panel title, empty/loading/retry copy, and the optional `externalHint` announced on an external tile (WCAG 3.2.5). `trigger` is a plain string, not a function of the current app: the nine-dot button shows no current value."},{name:"columns",type:"number",defaultValue:"3",description:"Grid column count, written inline to the `--app-launcher-columns` custom property. Omit it and `.ui-app-launcher-panel` keeps its own declaration of 3 \u2014 the default sits in the stylesheet, where a theme can reach it, rather than in a component token (the token NAME vocabulary has no word for a count)."},{name:"linkComponent",type:"SidebarLinkComponentProp",description:'Framework router link \u2014 the SAME contract `Sidebar` and `NavList` take, so `inertiaSidebarLink(Link)` / `createSidebarLink(Link, "to")` is reused verbatim. The launcher still composes the tile; `external` apps bypass it and render a plain anchor.'},{name:"loading",type:"boolean",defaultValue:"false",description:"Shows the loading state; the trigger stays openable."},{name:"error",type:"ReactNode",description:"Consumer-supplied error state."},{name:"onRetry",type:"() => void",description:"Consumer-owned retry callback."},{name:"responsive",type:'"auto" | "popover" | "sheet" | "fullscreen"',defaultValue:'"auto"',description:`Presentation contract. "auto" resolves through the SHARED Sheet hook useSheetResponsiveMode(): desktop popover above --sheet-responsive-breakpoint-width (48rem/768px), focus-trapped bottom Sheet at/below it. "fullscreen" is the LAUNCHPAD \u2014 one full-viewport modal at every width, the page behind it blurred and the grid floating on that ground (macOS Launchpad / Windows Start), with the column count stepping 3\xB74\xB75\xB76 on the house container ladder. Reach for it in a PLATFORM start bar, where the launcher is the primary way to move between products; keep "auto" for a launcher inside one app's topbar, where the grid is a shortcut and the work behind it should stay visible.`},{name:"appearance",type:'"bar" | "icon"',defaultValue:'"bar"',description:"The BOX the trigger takes \u2014 the same split AppSettingToggle draws, for the same reason. `bar` (default) is a TopbarItem: a cell as tall as the bar, whose hover IS the bar's surface. `icon` is a square ghost Button, for chrome that is NOT a bar \u2014 a nav rail (GoDX Dock puts it there), a card header, a toolbar. A TopbarItem outside a bar has nothing to bleed to: it stretches to a container that never set a band height, and its squared corners and full-bleed hover read as a broken cell rather than a control. The panel, the grid, the labels and the responsive contract are identical in both."},{name:"side",type:'"top" | "right" | "bottom" | "left"',description:'Which way the panel opens. DERIVED from `appearance` when unset \u2014 `bottom` in a bar (the panel drops below the trigger, the only direction that does not cover the bar itself), `right` otherwise, because a rail is vertical and its panel goes beside it. State it when the chrome can be RE-DOCKED: `appearance` says the trigger is NOT in a bar, and it cannot say which way is out \u2014 a rail pinned to the top edge is not a bar and still opens downward. Measured without it, an embedded bar trigger at (2,50) put its panel at (12,90), lying over the host application\'s sidebar. Not used by `responsive="fullscreen"`, which has no side.'},{name:"align",type:'"start" | "center" | "end"',description:"Where the panel sits along the `side` edge \u2014 the cross-axis half of the same decision, and derived the same way: `end` in a bar (the Workspace shape, flush with the bar's end), `start` otherwise (aligned to the rail trigger's own start). State it alongside `side` when you state either."},{name:"open",type:"boolean",description:"Controlled open state."},{name:"onOpenChange",type:"(open: boolean) => void",description:"Open-state callback."}],usage:["DO drop it straight into a Topbar slot. It renders NO wrapper element, so the trigger is the slot's own flex child and `.ui-topbar-item { align-self: stretch }` reaches the bar's height.",'DON\'T hand-roll the trigger as `Button variant="ghost"` IN A BAR. A Button in a bar is a --control-height pill floating in a taller strip, with its own hover fill and its own focus ring \u2014 the exact regression corrected in 20.0.0. In a bar the trigger is a `TopbarItem`, and that is what `appearance="bar"` (the default) renders. OUTSIDE a bar the square ghost Button is correct, and `appearance="icon"` renders it for you \u2014 still hand-roll nothing.','DO mark the app the viewer is inside with `current` \u2014 it becomes `aria-current="page"`, which is what both the tint and the announcement key off.',"DO set `external` on a destination outside this SPA. It renders a plain anchor with target/rel and skips `linkComponent`, because a client-side router link to another origin routes nowhere.",'DON\'T wrap it in your own media query to pick popover vs sheet \u2014 `responsive="auto"` already reads the shared --sheet-responsive-breakpoint-width token.',"`className`, `id` and every `data-*` land on the TRIGGER, so an end-to-end selector can hold it without depending on the localized accessible name."],useCases:["A platform with several apps (console, billing, chat, people) where the bar must offer all of them from every page.","Replacing a hand-rolled dropdown of product links in the topbar end slot."],related:["ServiceLauncherCard \u2014 the PAGE-SIZED launcher tile (status, hostname, plan, action, locked reason) for a service-catalogue page, where choosing is a considered act. AppLauncher's tile is bar-sized: mark + name, the whole tile one link, because changing app is a reflex. Neither is built out of the other; a grid of ServiceLauncherCards inside a popover is the wrong component.","AppShell (navRail) \u2014 the SAME platform scope expressed as a docked column instead of a bar control. Pick ONE: the launcher for a platform with MANY apps where switching is occasional (Google Workspace), the rail for a single product where switching workspace is constant enough to deserve permanent screen width (Slack).","OrgSwitcher \u2014 the other platform-scope control: which ORGANIZATION you are in, not which app. They compose; they do not replace each other.","TopbarItem \u2014 the bar cell the trigger is built from; use it directly for a one-off bar control."],example:`import { AppLauncher, Topbar } from "@godxjp/ui/layout";
|
|
2349
2349
|
|
|
2350
2350
|
<Topbar
|
|
2351
2351
|
start={brand}
|
|
@@ -2379,7 +2379,7 @@ import { Badge } from "@godxjp/ui/data-display";
|
|
|
2379
2379
|
hasActiveFilters={hasFilters}
|
|
2380
2380
|
resultCount={rows.length}
|
|
2381
2381
|
actions={<Button onClick={openCreate}>Add member</Button>}
|
|
2382
|
-
/>`,storyPath:"navigation/FilterBar.stories.tsx",rules:[]},{name:"PermissionMatrix",group:"data-display",tagline:"Domain data is 100% consumer-supplied.",props:[{name:"roles",type:"{ id: string; name: string; description?: string; locked?: boolean }[]",required:!0,description:"Role COLUMNS in render order. `locked` keeps that role's cells read-only (with a localized lock badge) even in an editable matrix."},{name:"permissions",type:"{ id: string; name: string; description?: string; group?: string }[]",required:!0,description:"Permission ROWS in render order, with an optional category caption."},{name:"grants",type:"ReadonlySet<string> | { roleId: string; permissionId: string }[]",required:!0,description:"The grant relation: the grantKey(roleId, permissionId) Set from @godxjp/ui/lib/permission-grid (canonical, O(1)), or a plain pair array normalized through the same encoding."},{name:"onGrantChange",type:"(roleId: string, permissionId: string, granted: boolean) => void",description:"Its PRESENCE makes the matrix editable (real Checkbox cells, Space toggles). Omitted, the matrix is the canonical read-only \u2713/\u2014 grid."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Force the read-only grid even when onGrantChange is present (viewer permission)."},{name:"compare",type:"[string, string] | null",description:"Two role ids compared side by side: their columns tint, and rows where they disagree carry a localized difference badge."},{name:"diffOnly",type:"boolean",defaultValue:"false",description:"With `compare`, keep only the rows the two roles disagree on (\u5DEE\u5206\u306E\u307F)."},{name:"loading / denied / error / empty / onRetry",type:"boolean | ReactNode / handler",description:"`true` renders the built-in localized surface; a node replaces it; onRetry adds Retry to the built-in error only."},{name:"label",type:"string",description:"Accessible table name (localized default)."}],usage:["DO import it \u2014 it is a real export from @godxjp/ui/data-display (the lesson: a docs page is not importable). Never hand-compose the sticky-column grid per app.","DO keep grants in the lib/permission-grid grantKey Set form when you already hold role:permission tuples \u2014 the pair-array form exists for convenience and is normalized through the same encoding.","DO put it in a Card with CardContent flush: <Card><CardContent flush><PermissionMatrix \u2026/></CardContent></Card>. Below its natural measure the grid scrolls horizontally INSIDE its own container (390px keeps the sticky permission column).","DO NOT encode platform roles/permissions in the library \u2014 roles, permissions and grants are consumer data by contract.","DO NOT pass compare pickers/toggles into the matrix \u2014 compose Select + Switch beside it and drive `compare`/`diffOnly` (see the showcase)."],useCases:["RBAC role tab on a service detail screen: read-only matrix + role compare.","Org role editor: editable matrix (onGrantChange) with the system role locked.","Permission-denied / failed read states without hand-rolling: denied / error / onRetry."],related:["lib/permission-grid \u2014 the pure grant/diff data helpers the matrix (and any custom RBAC UI) shares.","DataTable \u2014 general tabular data with sorting/selection/pagination; PermissionMatrix is the fixed role-grid specialization with a sticky FIRST column (which DataTable cannot pin).","ServiceRolePanel \u2014 the master-detail roles surface a matrix typically renders inside."],example:`import { Card, CardContent, PermissionMatrix } from "@godxjp/ui/data-display";
|
|
2382
|
+
/>`,storyPath:"navigation/FilterBar.stories.tsx",rules:[]},{name:"PermissionMatrix",group:"data-display",tagline:"Domain data is 100% consumer-supplied.",props:[{name:"roles",type:"{ id: string; name: string; description?: string; locked?: boolean }[]",required:!0,description:"Role COLUMNS in render order. `locked` keeps that role's cells read-only (with a localized lock badge) even in an editable matrix."},{name:"permissions",type:"{ id: string; name: string; description?: string; group?: string }[]",required:!0,description:"Permission ROWS in render order, with an optional category caption."},{name:"grants",type:"ReadonlySet<string> | { roleId: string; permissionId: string }[]",required:!0,description:"The grant relation: the grantKey(roleId, permissionId) Set from @godxjp/ui/lib/permission-grid (canonical, O(1)), or a plain pair array normalized through the same encoding."},{name:"onGrantChange",type:"(roleId: string, permissionId: string, granted: boolean) => void",description:"Its PRESENCE makes the matrix editable (real Checkbox cells, Space toggles). Omitted, the matrix is the canonical read-only \u2713/\u2014 grid."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Force the read-only grid even when onGrantChange is present (viewer permission)."},{name:"compare",type:"[string, string] | null",description:"Two role ids compared side by side: their columns tint, and rows where they disagree carry a localized difference badge."},{name:"diffOnly",type:"boolean",defaultValue:"false",description:"With `compare`, keep only the rows the two roles disagree on (\u5DEE\u5206\u306E\u307F)."},{name:"loading / denied / error / empty / onRetry",type:"boolean | ReactNode / handler",description:"`true` renders the built-in localized surface; a node replaces it; onRetry adds Retry to the built-in error only."},{name:"label",type:"string",description:"Accessible table name (localized default)."},{name:"id",type:"string",description:"DOM id on the grid root. Worth setting on a permissions page that also renders a summary or a legend elsewhere: it is the anchor those can point at, and the stable handle for an E2E selector that must not depend on the localized `label`."}],usage:["DO import it \u2014 it is a real export from @godxjp/ui/data-display (the lesson: a docs page is not importable). Never hand-compose the sticky-column grid per app.","DO keep grants in the lib/permission-grid grantKey Set form when you already hold role:permission tuples \u2014 the pair-array form exists for convenience and is normalized through the same encoding.","DO put it in a Card with CardContent flush: <Card><CardContent flush><PermissionMatrix \u2026/></CardContent></Card>. Below its natural measure the grid scrolls horizontally INSIDE its own container (390px keeps the sticky permission column).","DO NOT encode platform roles/permissions in the library \u2014 roles, permissions and grants are consumer data by contract.","DO NOT pass compare pickers/toggles into the matrix \u2014 compose Select + Switch beside it and drive `compare`/`diffOnly` (see the showcase)."],useCases:["RBAC role tab on a service detail screen: read-only matrix + role compare.","Org role editor: editable matrix (onGrantChange) with the system role locked.","Permission-denied / failed read states without hand-rolling: denied / error / onRetry."],related:["lib/permission-grid \u2014 the pure grant/diff data helpers the matrix (and any custom RBAC UI) shares.","DataTable \u2014 general tabular data with sorting/selection/pagination; PermissionMatrix is the fixed role-grid specialization with a sticky FIRST column (which DataTable cannot pin).","ServiceRolePanel \u2014 the master-detail roles surface a matrix typically renders inside."],example:`import { Card, CardContent, PermissionMatrix } from "@godxjp/ui/data-display";
|
|
2383
2383
|
import { grantKey } from "@godxjp/ui/lib/permission-grid";
|
|
2384
2384
|
|
|
2385
2385
|
const grants = new Set(rolePermissions.map((rp) => grantKey(rp.roleId, rp.permissionId)));
|
|
@@ -2393,7 +2393,7 @@ const grants = new Set(rolePermissions.map((rp) => grantKey(rp.roleId, rp.permis
|
|
|
2393
2393
|
onGrantChange={(roleId, permissionId, granted) => mutate({ roleId, permissionId, granted })}
|
|
2394
2394
|
/>
|
|
2395
2395
|
</CardContent>
|
|
2396
|
-
</Card>`,docPath:"data-display/permission-matrix.tsx",storyPath:"data-display/PermissionMatrix.stories.tsx",rules:[24]},{name:"BranchScopePicker",group:"data-entry",tagline:"Canonical scope control: all branches vs an explicit subset.",props:[{name:"value",type:"BranchScopeValueProp",description:"Gi\xE1 tr\u1ECB c\xF3 ki\u1EC3m so\xE1t: ph\u1EA1m vi \u0111ang ch\u1ECDn."},{name:"defaultValue",type:"BranchScopeValueProp",description:"Gi\xE1 tr\u1ECB kh\u1EDFi t\u1EA1o khi kh\xF4ng ki\u1EC3m so\xE1t."},{name:"onValueChange",type:"(value: BranchScopeValueProp) => void",description:"Ph\xE1t khi ph\u1EA1m vi \u0111\u1ED5i."},{name:"branches",type:"{ id: string; name: string; description?: string; disabled?: boolean }[]",required:!0,description:"The selectable branches \u2014 consumer domain data."},{name:"value / defaultValue / onValueChange",type:'{ mode: "all" | "selected"; branchIds?: string[] }',description:'The controlled triad; default { mode: "all" }. Mode flips PRESERVE branchIds so switching back to all never destroys a curated subset.'},{name:"error",type:"ReactNode",description:"Field VALIDATION message \u2014 rendered under the control and wired via aria-invalid/aria-errormessage on the radiogroup. Collection read failures are `listError`, not this."},{name:"listError / denied / loading / empty",type:"boolean | ReactNode",description:"`true` renders the built-in localized message."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Locked view: the current scope as a static summary (mode + branch badges)."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Controls visible but inert."},{name:"searchable",type:"boolean",defaultValue:"true",description:"Built-in branch search above the checkbox list."},{name:"allLabel / selectedLabel",type:"ReactNode",description:"Override the localized radio labels (e.g. domain wording like \u5168\u5E97\u8217)."}],usage:["DO treat scope as ONE form field: the single { mode, branchIds } value goes through FormField like any other control.","DO import it from @godxjp/ui/data-entry \u2014 never compose an ad-hoc radio+checkbox scope block per screen.","DO use `error` ONLY for validation (e.g. mode=selected with zero branches checked); a failed branch fetch is `listError`, a 403 is `denied`.",'DO NOT preselect branches for the user \u2014 default is { mode: "all" }; an explicit subset is a user decision.'],useCases:["Role/permission assignment scoped to branches (service role forms).","Report or notification audience: whole org vs selected branches.","Read-only scope display on a detail screen (readOnly)."],related:["CheckboxGroup / RadioGroup \u2014 the primitives underneath; use them directly for non-scope choices.","Transfer \u2014 large two-list assignment; BranchScopePicker is the compact all-vs-subset scope idiom.","TreeSelect \u2014 hierarchical selection when branches nest."],example:`import { BranchScopePicker, FormField } from "@godxjp/ui/data-entry";
|
|
2396
|
+
</Card>`,docPath:"data-display/permission-matrix.tsx",storyPath:"data-display/PermissionMatrix.stories.tsx",rules:[24]},{name:"BranchScopePicker",group:"data-entry",tagline:"Canonical scope control: all branches vs an explicit subset.",props:[{name:"value",type:"BranchScopeValueProp",description:"Gi\xE1 tr\u1ECB c\xF3 ki\u1EC3m so\xE1t: ph\u1EA1m vi \u0111ang ch\u1ECDn."},{name:"defaultValue",type:"BranchScopeValueProp",description:"Gi\xE1 tr\u1ECB kh\u1EDFi t\u1EA1o khi kh\xF4ng ki\u1EC3m so\xE1t."},{name:"onValueChange",type:"(value: BranchScopeValueProp) => void",description:"Ph\xE1t khi ph\u1EA1m vi \u0111\u1ED5i."},{name:"branches",type:"{ id: string; name: string; description?: string; disabled?: boolean }[]",required:!0,description:"The selectable branches \u2014 consumer domain data."},{name:"value / defaultValue / onValueChange",type:'{ mode: "all" | "selected"; branchIds?: string[] }',description:'The controlled triad; default { mode: "all" }. Mode flips PRESERVE branchIds so switching back to all never destroys a curated subset.'},{name:"error",type:"ReactNode",description:"Field VALIDATION message \u2014 rendered under the control and wired via aria-invalid/aria-errormessage on the radiogroup. Collection read failures are `listError`, not this."},{name:"listError / denied / loading / empty",type:"boolean | ReactNode",description:"`true` renders the built-in localized message."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Locked view: the current scope as a static summary (mode + branch badges)."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Controls visible but inert."},{name:"searchable",type:"boolean",defaultValue:"true",description:"Built-in branch search above the checkbox list."},{name:"allLabel / selectedLabel",type:"ReactNode",description:"Override the localized radio labels (e.g. domain wording like \u5168\u5E97\u8217)."},{name:"name",type:"string",description:"Native form name, forwarded to the MODE radio group \u2014 the all / selected choice is what submits under it. The checked branch ids are not native fields; they live in the single `{ mode, branchIds }` value and are yours to serialise."},{name:"id",type:"string",description:"DOM id on the picker root, and the SEED for the ids beneath it \u2014 the validation message is `${id}-error`, which is what `aria-errormessage` points at. Left out, a `useId`-based id is generated, so the association still holds; set it when a server-rendered page needs those ids to be stable."}],usage:["DO treat scope as ONE form field: the single { mode, branchIds } value goes through FormField like any other control.","DO import it from @godxjp/ui/data-entry \u2014 never compose an ad-hoc radio+checkbox scope block per screen.","DO use `error` ONLY for validation (e.g. mode=selected with zero branches checked); a failed branch fetch is `listError`, a 403 is `denied`.",'DO NOT preselect branches for the user \u2014 default is { mode: "all" }; an explicit subset is a user decision.'],useCases:["Role/permission assignment scoped to branches (service role forms).","Report or notification audience: whole org vs selected branches.","Read-only scope display on a detail screen (readOnly)."],related:["CheckboxGroup / RadioGroup \u2014 the primitives underneath; use them directly for non-scope choices.","Transfer \u2014 large two-list assignment; BranchScopePicker is the compact all-vs-subset scope idiom.","TreeSelect \u2014 hierarchical selection when branches nest."],example:`import { BranchScopePicker, FormField } from "@godxjp/ui/data-entry";
|
|
2397
2397
|
|
|
2398
2398
|
<FormField label="\u9069\u7528\u7BC4\u56F2" required error={errors.scope}>
|
|
2399
2399
|
<BranchScopePicker
|
|
@@ -2402,7 +2402,7 @@ const grants = new Set(rolePermissions.map((rp) => grantKey(rp.roleId, rp.permis
|
|
|
2402
2402
|
onValueChange={setScope}
|
|
2403
2403
|
error={scope.mode === "selected" && !scope.branchIds?.length ? "1\u4EF6\u4EE5\u4E0A\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" : undefined}
|
|
2404
2404
|
/>
|
|
2405
|
-
</FormField>`,docPath:"data-entry/branch-scope-picker.tsx",storyPath:"data-entry/BranchScopePicker.stories.tsx",rules:[24]},{name:"ServiceRolePanel",group:"layout",tagline:"Geometry (1440/1024 two-track, 390 stacked) is MasterDetail's tokens.",props:[{name:"value",type:"string",description:"Gi\xE1 tr\u1ECB c\xF3 ki\u1EC3m so\xE1t: id vai tr\xF2 \u0111ang ch\u1ECDn."},{name:"defaultValue",type:"string",description:"Id vai tr\xF2 kh\u1EDFi t\u1EA1o khi kh\xF4ng ki\u1EC3m so\xE1t."},{name:"onValueChange",type:"(roleId: string) => void",description:"Ph\xE1t khi vai tr\xF2 \u0111\u1ED5i."},{name:"roles",type:"{ id: string; name: string; description?: string; memberCount?: number; locked?: boolean }[]",required:!0,description:"The role collection \u2014 consumer domain data. `locked` = system role: lock badge, never deletable. memberCount is CLDR-pluralized."},{name:"value / defaultValue / onValueChange",type:"string / (roleId: string) => void",description:"Controlled selection triad; defaults to the first role."},{name:"children",type:"ReactNode | (role) => ReactNode",description:"The detail surface. A render function receives the selected role \u2014 typically renders a PermissionMatrix + role metadata."},{name:"onDeleteRole",type:"(roleId: string) => void",description:"Its PRESENCE arms a per-role delete affordance behind the built-in destructive AlertDialog; it fires only after the user confirms. Locked roles never offer it."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Hide every mutating affordance."},{name:"loading / denied / error / empty / onRetry",type:"boolean | ReactNode / handler",description:"The #216 lifecycle vocabulary (precedence loading \u2192 denied \u2192 error \u2192 empty), same semantics as DataTable/PermissionMatrix."},{name:"railWidth / masterViewport / collapseBelow / masterLabel / detailLabel",type:"MasterDetail geometry + region labels",description:"Forwarded to MasterDetail (localized region labels by default). Never re-derive tracks or breakpoints in the app."}],usage:["DO compose the detail from real primitives \u2014 the panel deliberately owns NO detail layout; PermissionMatrix + Descriptions is the typical body.","DO rely on the built-in AlertDialog for deletion \u2014 never wire a bare onClick delete; the confirm gate is the contract.",'DO pass masterViewport="compact" for long role collections so the rail scrolls inside its region after stacking.',"DO NOT nest interactive controls in a role row \u2014 the select button and the delete button are SIBLINGS by design; keep any custom row content non-interactive."],useCases:["Service detail \u2192 roles tab: role list rail + permission matrix detail.","Org role management screen with locked system roles and confirmed deletion.","Read-only role browser for auditors (readOnly)."],related:["MasterDetail \u2014 the geometry underneath; use it directly for non-role collections.","PermissionMatrix \u2014 the canonical detail body for a selected role.","AlertDialog \u2014 the confirm primitive the panel embeds; use directly for other destructive flows."],example:`import { PermissionMatrix } from "@godxjp/ui/data-display";
|
|
2405
|
+
</FormField>`,docPath:"data-entry/branch-scope-picker.tsx",storyPath:"data-entry/BranchScopePicker.stories.tsx",rules:[24]},{name:"ServiceRolePanel",group:"layout",tagline:"Geometry (1440/1024 two-track, 390 stacked) is MasterDetail's tokens.",props:[{name:"value",type:"string",description:"Gi\xE1 tr\u1ECB c\xF3 ki\u1EC3m so\xE1t: id vai tr\xF2 \u0111ang ch\u1ECDn."},{name:"defaultValue",type:"string",description:"Id vai tr\xF2 kh\u1EDFi t\u1EA1o khi kh\xF4ng ki\u1EC3m so\xE1t."},{name:"onValueChange",type:"(roleId: string) => void",description:"Ph\xE1t khi vai tr\xF2 \u0111\u1ED5i."},{name:"roles",type:"{ id: string; name: string; description?: string; memberCount?: number; locked?: boolean }[]",required:!0,description:"The role collection \u2014 consumer domain data. `locked` = system role: lock badge, never deletable. memberCount is CLDR-pluralized."},{name:"value / defaultValue / onValueChange",type:"string / (roleId: string) => void",description:"Controlled selection triad; defaults to the first role."},{name:"children",type:"ReactNode | (role) => ReactNode",description:"The detail surface. A render function receives the selected role \u2014 typically renders a PermissionMatrix + role metadata."},{name:"onDeleteRole",type:"(roleId: string) => void",description:"Its PRESENCE arms a per-role delete affordance behind the built-in destructive AlertDialog; it fires only after the user confirms. Locked roles never offer it."},{name:"readOnly",type:"boolean",defaultValue:"false",description:"Hide every mutating affordance."},{name:"loading / denied / error / empty / onRetry",type:"boolean | ReactNode / handler",description:"The #216 lifecycle vocabulary (precedence loading \u2192 denied \u2192 error \u2192 empty), same semantics as DataTable/PermissionMatrix."},{name:"railWidth / masterViewport / collapseBelow / masterLabel / detailLabel",type:"MasterDetail geometry + region labels",description:"Forwarded to MasterDetail (localized region labels by default). Never re-derive tracks or breakpoints in the app."},{name:"id",type:"string",description:"DOM id on the panel root \u2014 the two-region MasterDetail wrapper, not the rail or the detail. It is the handle for a deep link onto the roles panel of a settings page, and for an E2E selector that must survive `masterLabel` being localized."}],usage:["DO compose the detail from real primitives \u2014 the panel deliberately owns NO detail layout; PermissionMatrix + Descriptions is the typical body.","DO rely on the built-in AlertDialog for deletion \u2014 never wire a bare onClick delete; the confirm gate is the contract.",'DO pass masterViewport="compact" for long role collections so the rail scrolls inside its region after stacking.',"DO NOT nest interactive controls in a role row \u2014 the select button and the delete button are SIBLINGS by design; keep any custom row content non-interactive."],useCases:["Service detail \u2192 roles tab: role list rail + permission matrix detail.","Org role management screen with locked system roles and confirmed deletion.","Read-only role browser for auditors (readOnly)."],related:["MasterDetail \u2014 the geometry underneath; use it directly for non-role collections.","PermissionMatrix \u2014 the canonical detail body for a selected role.","AlertDialog \u2014 the confirm primitive the panel embeds; use directly for other destructive flows."],example:`import { PermissionMatrix } from "@godxjp/ui/data-display";
|
|
2406
2406
|
import { ServiceRolePanel } from "@godxjp/ui/layout";
|
|
2407
2407
|
|
|
2408
2408
|
<ServiceRolePanel
|
|
@@ -2457,7 +2457,7 @@ const messages: ChatMessageProp[] = [
|
|
|
2457
2457
|
`),docPath:"navigation/mega-menu.tsx",storyPath:"navigation/MegaMenu.stories.tsx",rules:[2,6,23,44,45]},{name:"Welcome",group:"data-display",tagline:"The greeting block at the head of an empty conversation (Ant Design X Welcome): glyph, greeting, one line under it, and a trailing slot ON THE TITLE ROW \u2014 which is the placement a hand-roll gets wrong.",props:[{name:"icon",type:"React.ReactNode | string",description:'Leading glyph. A STRING beginning with http(s) is rendered as a decorative <img alt=""> (Ant Design X does the same, with alt="icon"); any other string renders as text.'},{name:"title",type:"React.ReactNode",description:"The greeting. Renders as an <h4>, which is Ant Design X's hardcoded Typography.Title level={4}."},{name:"description",type:"React.ReactNode",description:"The line under the greeting."},{name:"extra",type:"React.ReactNode",description:"Trailing slot on the TITLE row \u2014 a dismiss button, a model picker. Not under the description."},{name:"variant",type:'"filled" | "borderless"',defaultValue:'"filled"',description:"filled gives the block its own tinted ground and hairline; borderless lets it sit on the page."},{name:"id",type:"string",description:"DOM id of the block."}],usage:["DO put it above the composer on an empty chat, with ChatSuggestion or a Prompts row beneath it \u2014 that is the surface it belongs to.","DO pass `extra` for the one action the greeting carries (dismiss, switch model). It lands beside the title, top-aligned, so a two-line title does not float it.","DON'T reach for it as a generic page header \u2014 that is PageContainer's title/subtitle/extra, which owns the page rhythm.","DON'T expect a heading-level prop: Ant Design X hardcodes level 4 and this ports that. Wrap it in your own heading hierarchy if the page needs a different rung.","DON'T expect `styles`/`classNames` from Ant Design X \u2014 retune through the --welcome-* tokens."],useCases:["The first screen of an assistant, before the first message.","The head of a fresh conversation started from the Conversations rail.","A feature introduction card inside a chat surface, dismissed through `extra`."],related:["EmptyState \u2014 the general 'nothing here yet' block for a list or a table. Welcome is the chat surface's greeting and carries an icon/title/description/extra shape of its own.","PageContainer \u2014 owns the PAGE header; Welcome sits inside the page body.","ChatSuggestion / ChatBubbleList \u2014 the rest of the same surface."],example:['import { Welcome } from "@godxjp/ui/data-display";','import { Button } from "@godxjp/ui/general";','import { Bot } from "lucide-react";',"","<Welcome"," icon={<Bot />}",' title="\u3053\u3093\u306B\u3061\u306F"',' description="\u8ACB\u6C42\u3001\u7D4C\u8CBB\u3001\u52E4\u6020\u306E\u3053\u3068\u306A\u3089\u304A\u624B\u4F1D\u3044\u3067\u304D\u307E\u3059\u3002"',' extra={<Button variant="ghost" size="sm">\u9589\u3058\u308B</Button>}',"/>"].join(`
|
|
2458
2458
|
`),docPath:"data-display/welcome.tsx",storyPath:"data-display/Welcome.stories.tsx",rules:[2,6,44,45]},{name:"Actions",group:"general",tagline:"The strip of actions under an assistant message (Ant Design X Actions): copy, retry, like, and a menu for the rest \u2014 a WAI-ARIA toolbar with ONE tab stop, where Ant X's own strip is <div onClick> with no role and no accessible name.",subParts:["ActionsItem","ActionsCopy","ActionsFeedback"],props:[{name:"items",type:"ActionsItemsProp[]",required:!0,description:"The actions: { key, label?, icon?, onItemClick?, danger?, subItems?, actionRender? }. `label` is the accessible name AND the tooltip. `subItems` folds the action into a menu; `actionRender` replaces it entirely."},{name:"onClick",type:"(info: { item, key, keyPath, domEvent }) => void",description:"Fires for any action WITHOUT its own onItemClick \u2014 a per-item handler wins and this does not also fire, exactly as in Ant Design X. A sub-item reports keyPath [subKey, parentKey]."},{name:"variant",type:'"borderless" | "filled" | "outlined"',defaultValue:'"borderless"',description:"Chrome of the STRIP, not the intent of the buttons (that is `danger` per item)."},{name:"fadeIn",type:"boolean",description:"The strip fades in on mount. Zeroed under prefers-reduced-motion."},{name:"fadeInLeft",type:"boolean",description:"The same fade, arriving along the LOGICAL inline axis (so it mirrors under dir=rtl)."},{name:"label",type:"string",description:"Accessible name of the toolbar (a plain string). Localized default otherwise."},{name:"id",type:"string",description:"DOM id of the strip."}],usage:["DO give every action a `label`. It becomes the accessible name and the tooltip; without one the key is used, which is better than nameless but worse than a sentence.","DO use `subItems` once the strip passes about five actions \u2014 it folds them behind one trigger instead of widening the row under every message.","DO reach for ActionsCopy and ActionsFeedback instead of hand-rolling copy and thumbs: ActionsCopy announces the copy through a live region (a tick alone is invisible to a screen reader), and ActionsFeedback keeps BOTH buttons on screen with aria-pressed rather than hiding the one you did not pick.","DON'T put a form control in the strip. It is a toolbar of buttons with one tab stop; a field inside would be unreachable by Tab.","DON'T expect `dropdownProps`, `triggerSubMenuAction`, `styles` or `classNames` from Ant Design X \u2014 they are not ported; the strip is retuned through the --actions-* tokens."],useCases:["Under an assistant answer: copy, regenerate, like/dislike, and a menu with share and report.",'Under a streaming answer: an ActionsItem with status="running" while the audio plays back, error when it fails.',"In a message hover strip inside ChatBubbleList."],related:["Toolbar / FilterBar \u2014 the list-page filter strip. Actions is the per-message action cluster, not a page-level control bar.","DropdownMenu \u2014 what `subItems` renders; compose it directly when the menu is not one action in a strip.","ChatBubble \u2014 the message the strip belongs to.","CredentialReveal \u2014 a copy affordance for a SECRET field; ActionsCopy copies message text."],example:['import { Actions, ActionsCopy, ActionsFeedback } from "@godxjp/ui/general";','import { RefreshCw, Share2 } from "lucide-react";',"","<Actions",' label="\u56DE\u7B54\u306E\u64CD\u4F5C"'," items={[",' { key: "retry", label: "\u3084\u308A\u76F4\u3059", icon: <RefreshCw />, onItemClick: () => regenerate() },'," {",' key: "more",',' label: "\u305D\u306E\u4ED6",',' subItems: [{ key: "share", label: "\u5171\u6709", icon: <Share2 /> }],'," },",' { key: "copy", actionRender: <ActionsCopy text={answer} /> },',' { key: "feedback", actionRender: <ActionsFeedback value={vote} onChange={setVote} /> },'," ]}"," onClick={({ key }) => run(key)}","/>"].join(`
|
|
2459
2459
|
`),docPath:"general/actions.tsx",storyPath:"general/Actions.stories.tsx",rules:[2,6,23,44,45]},{name:"ThoughtChain",group:"data-display",tagline:"The assistant's reasoning, step by step (Ant Design X ThoughtChain): an ORDERED list of steps, each with an ordinal or a glyph, a status, and a body it can collapse \u2014 where Ant X's own step is a <div onClick> with no role and no aria-expanded.",subParts:["ThoughtChainItem"],props:[{name:"items",type:"ThoughtChainItemsProp[]",description:"The steps: { key?, icon?, title?, description?, content?, footer?, status?, collapsible?, blink?, destroyOnHidden? }. `icon: false` drops the glyph column; omitted, the step shows its 1-based ordinal (Ant Design X's own default)."},{name:"defaultExpandedKeys",type:"string[]",description:"Uncontrolled initially-open steps."},{name:"expandedKeys",type:"string[]",description:"Controlled open steps."},{name:"onExpand",type:"(keys: string[]) => void",description:"Fires with the NEXT open set."},{name:"line",type:'boolean | "solid" | "dashed" | "dotted"',defaultValue:"true",description:'The connector drawn between steps. false draws none. (Ant Design X\'s own type spells the third with a stray U+200C, so `line="dotted"` does not type-check there; the clean spelling is used here.)'},{name:"label",type:"string",description:"Accessible name of the chain (a plain string). Localized default otherwise."},{name:"id",type:"string",description:"DOM id of the chain root."}],usage:["DO give every step a stable `key` \u2014 it is what expandedKeys addresses and what onExpand reports.","DO set `collapsible` on a step whose `content` is long (a tool's raw output, a retrieved passage). The title then becomes a real disclosure button with aria-expanded, keyboard-reachable; without `collapsible` the body is simply always shown.","DO use `status` for how a step ENDED \u2014 loading / success / error / abort. The word rides along in a visually hidden span, so the state is never carried by the tint alone.","DO use `blink` while a step is still streaming; it pulses the title and body and collapses to nothing under prefers-reduced-motion.","DON'T reach for it for events that already happened \u2014 that is Timeline. A thought chain is a run IN PROGRESS, which is why it has loading and abort states and a body that opens.","DON'T expect `styles`/`classNames` from Ant Design X \u2014 retune through the --thought-chain-* tokens."],useCases:["An agent's tool calls under its answer: read the documents, search the policy, draft the reply \u2014 each with its output collapsed.","A long-running job's progress inside a chat: the current step blinking, the finished ones ticked, an aborted one greyed.","ThoughtChainItem alone: the chip an assistant drops inline to name the tool it just reached for."],related:["Timeline \u2014 the same vertical rail for events that ALREADY happened. Use it when nothing is in flight.","Steps \u2014 a wizard's progress across a form. ThoughtChain is the assistant's own reasoning, not the user's path.","Accordion \u2014 a general disclosure list with no rail, no ordinal and no status.","ChatBubble \u2014 the answer the chain explains."],example:['import { ThoughtChain } from "@godxjp/ui/data-display";',"","<ThoughtChain",' label="\u601D\u8003\u306E\u624B\u9806"',' defaultExpandedKeys={["search"]}'," items={[",' { key: "read", title: "\u8CC7\u6599\u3092\u8AAD\u3080", description: "3\u4EF6", status: "success" },'," {",' key: "search",',' title: "\u793E\u5185\u898F\u7A0B\u3092\u691C\u7D22",',' status: "loading",'," collapsible: true,"," blink: true,"," content: <pre>{hits}</pre>,"," },",' { key: "write", title: "\u4E0B\u66F8\u304D\u3092\u66F8\u304F", status: "abort" },'," ]}","/>"].join(`
|
|
2460
|
-
`),docPath:"data-display/thought-chain.tsx",storyPath:"data-display/ThoughtChain.stories.tsx",rules:[2,6,23,44,45]},{name:"Attachments",group:"data-entry",tagline:"The chat-surface attachment collection (Ant Design X Attachments): file cards, inline placeholder, optional full-screen drop target, and ref.select/ref.upload \u2014 inherits antd Upload props but names the list `items`.",props:[{name:"items",type:"AttachmentsItemProp[]",description:"Controlled attachment list. Ant Design X `items` (= antd Upload `fileList`)."},{name:"onChange",type:"(info: { file: AttachmentsItemProp; fileList: AttachmentsItemProp[] }) => void",description:"antd Upload `onChange` \u2014 NOT `onValueChange`."},{name:"overflow",type:'"wrap" | "scrollX" | "scrollY"',description:"Ant Design X `overflow`."},{name:"placeholder",type:"AttachmentsPlaceholderProp | ((type) => AttachmentsPlaceholderProp)",description:"Empty-state copy for inline and drop surfaces."},{name:"getDropContainer",type:"() => HTMLElement | null",description:"Host for a full-screen drop overlay."},{name:"maxCount",type:"number",description:"Maximum files (antd Upload `maxCount`)."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Blocks selection and drop; the control stays focusable so a reader can still reach it."},{name:"accept",type:"string",description:"Forwarded to the hidden file input."},{name:"children",type:"ReactElement",description:"Child mode: visible trigger; upload runs through a hidden input beside it."}],usage:["DO keep antd field names on each item (`thumbUrl`, `originFileObj`, `uid`) \u2014 an Ant X call site should compile unchanged.","DO use `ref.select({ accept, multiple })` to open the picker programmatically (Ant X 2.0).","
|
|
2460
|
+
`),docPath:"data-display/thought-chain.tsx",storyPath:"data-display/ThoughtChain.stories.tsx",rules:[2,6,23,44,45]},{name:"Attachments",group:"data-entry",tagline:"The chat-surface attachment collection (Ant Design X Attachments): file cards, inline placeholder, optional full-screen drop target, and ref.select/ref.upload \u2014 inherits antd Upload props but names the list `items`.",props:[{name:"items",type:"AttachmentsItemProp[]",description:"Controlled attachment list. Ant Design X `items` (= antd Upload `fileList`)."},{name:"onChange",type:"(info: { file: AttachmentsItemProp; fileList: AttachmentsItemProp[] }) => void",description:"antd Upload `onChange` \u2014 NOT `onValueChange`."},{name:"overflow",type:'"wrap" | "scrollX" | "scrollY"',description:"Ant Design X `overflow`."},{name:"placeholder",type:"AttachmentsPlaceholderProp | ((type) => AttachmentsPlaceholderProp)",description:"Empty-state copy for inline and drop surfaces."},{name:"getDropContainer",type:"() => HTMLElement | null",description:"Host for a full-screen drop overlay."},{name:"maxCount",type:"number",description:"Maximum files (antd Upload `maxCount`)."},{name:"disabled",type:"boolean",defaultValue:"false",description:"Blocks selection and drop; the control stays focusable so a reader can still reach it."},{name:"accept",type:"string",description:"Forwarded to the hidden file input."},{name:"children",type:"ReactElement",description:"Child mode: visible trigger; upload runs through a hidden input beside it."},{name:"onRemove",type:"(item: AttachmentsItemProp) => boolean | void | Promise<boolean | void>",description:"antd Upload `onRemove`, narrowed to the attachment row. RETURNING `false` (or a promise of it) VETOES the removal and the card stays \u2014 anything else, including `undefined`, lets it go. That is how you gate a removal behind a confirm dialog without owning `items` yourself. It is awaited, so an async guard works."},{name:"classNames",type:"Partial<Record<AttachmentsSemanticProp, string>>",description:"Ant Design X `classNames` \u2014 per-part classes (root, list, card, file, upload, placeholder). DECLARED AND FORWARDED, and the only semantic part map in this package: docs/DESIGN-AUTHORITY.md rules that antd's `classNames`/`styles` maps are NOT adopted because this library answers that layer with tokens (cardinal rule #45), and Attachments is the one component that carries them anyway. Recorded there as a contradiction, not a pattern \u2014 do not copy it onto another component, and retune through `--attachments-*` instead."},{name:"styles",type:"Partial<Record<AttachmentsSemanticProp, React.CSSProperties>>",description:"Ant Design X `styles` \u2014 the same part map as `classNames`, as inline styles, and under the same standing ruling against it. Inline styles beat every stylesheet rule, so this is the one handle in the package that can take a part off the design system entirely. The `--attachments-*` tokens are the supported route."},{name:"rootClassName",type:"string",description:"Ant Design X `rootClassName` \u2014 the outermost node. It is not a duplicate of `className`: in the full-screen-drop mode (`getDropContainer`) the outermost node is the OVERLAY rather than the inline tray, which is why antd separates the two. Both are applied."},{name:"imageProps",type:"Record<string, unknown>",description:"ACCEPTED AND INERT. Ant Design X forwards it to its own Image preview; this package has no Image primitive yet, so the prop exists only so an Ant X call site type-checks, and passing it changes nothing on screen. Do not reach for it expecting a preview knob."}],usage:["DO keep antd field names on each item (`thumbUrl`, `originFileObj`, `uid`) \u2014 an Ant X call site should compile unchanged.","DO use `ref.select({ accept, multiple })` to open the picker programmatically (Ant X 2.0).","Ant X's `classNames` / `styles` / `rootClassName` ARE declared and forwarded, although docs/DESIGN-AUTHORITY.md rules that antd's semantic part maps are not adopted here. The older note in this slot said not to expect them at all; the type has never agreed with it, so an agent reading only the catalog was told the opposite of what autocomplete offered. Retune through the `--attachments-*` tokens, and treat the maps as a recorded contradiction on this one component rather than a pattern to reuse.","The card is a FIXED box \u2014 268x68 (Ant X's own), from `--attachments-card-size` (inline) and `--attachments-card-block-size`. The block size is also the `+` tile's square and the `overflow=\"scrollY\"` one-row viewport, so retune it once and all three follow. The file input is `sr-only`: never style it visible."],useCases:["The attachment tray above a ChatComposer in an assistant surface.","A Sender.Header slot showing picked files before send."],related:["Upload","ChatComposer","ChatBubbleList"],example:`import { Attachments } from "@godxjp/ui/data-entry";
|
|
2461
2461
|
|
|
2462
2462
|
<Attachments
|
|
2463
2463
|
items={files}
|
|
@@ -4558,7 +4558,7 @@ A block with no reason is IGNORED and the finding stands. An unclosed block runs
|
|
|
4558
4558
|
The class-shaped rules (gap-*/p-*/m-*, bg-<palette>-*, w-[\u2026], pr-*, dark:*) only read class
|
|
4559
4559
|
expressions \u2014 a className/class attribute, a class-named binding (\`baseClass\`, \`statusStyles\`,
|
|
4560
4560
|
\`badgeVariants\`) or a cn()/clsx()/cva() call \u2014 so prose that merely spells a utility is not a
|
|
4561
|
-
finding and needs no suppression.`,Z=[{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 \xB7 Accessible Name Computation 1.2",fix:"Name <Button size='icon'> with aria-label={t('\u2026')} OR from content \u2014 a <VisuallyHidden>/sr-only child beside the aria-hidden glyph. Text inside an aria-hidden subtree names nothing."},{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-hand-rolled-scrollport",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 2.1.1 (Keyboard) \xB7 WAI-ARIA 1.2 (group) \xB7 Deque axe-core scrollable-region-focusable",fix:'Replace className="overflow-auto / overflow-y-auto / overflow-x-auto / overflow-scroll" on your own element with <ScrollArea label={t("\u2026")} orientation>, which is the tab stop, the role and the localized name \u2014 and withholds all three while there is nothing to scroll. A browser audit only fails a scrollport whose content has NO focusable child, so the same markup is clean or broken depending on the data; this reads the markup instead (gh#825). overflow-hidden is a clipping box, not a scrollport, and is not flagged.'},{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."},{id:"lucide-icon-needs-size",severity:"warn",category:"composition",standard:"WCAG 2.2 SC 1.4.4 \xB7 @godxjp/ui icon scale (--icon-size-*)",fix:'A lucide glyph outside a sizing context draws at its intrinsic 24px. Render it as <Icon as={Lock} size="sm" tone="muted" /> \u2014 the primitive puts it on the --icon-size-* scale and is aria-hidden unless you pass a label.'},{id:"no-hand-rolled-list",severity:"warn",category:"composition",standard:"WAI-ARIA 1.2 (list / listitem) \xB7 HTML Living Standard (ul/ol/li) \xB7 WCAG 2.2 SC 1.3.1",fix:'Build the list as <Flex as="ul" marker="none" direction="col" gap="none"> with <ListRow as="li"> rows \u2014 not a raw <ul>/<ol> (no gap token), not <div role="list">/<div role="listitem">, and never a wrapper around each row: the divider is :not(:last-child) among SIBLINGS, so a row alone in its own wrapper loses it silently (a consumer lost every divider in a settings menu and a dashboard this way). marker="none" keeps the element, the <li> semantics and the gap, and drops the bullet and the --space-5 indent (gh#714). A deliberate exception \u2014 a drag-and-drop Kanban column, an evidence list inside a TableCell \u2014 takes an ui-audit-disable-line that says so.'}];function ae(e){return e?Z.filter(t=>t.category===e):Z}var ne="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)",oe=[{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. The accent @godxjp/ui itself ships is exempt (gh#823) \u2014 this finding is always a colour someone chose."},{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 ie(e){return e?oe.filter(t=>t.category===e):oe}var d={name:"@godxjp/ui-mcp",version:"28.12.0",godxUiCompatibility:"28.12.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 B=[{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"]}}],De=new Set(B.map(e=>e.name));function Oe(e=V()){let t=d.godxUiCompatibility??d.version,a=`@godxjp/ui-mcp ${d.version} (catalog for @godxjp/ui ${t})`;if(!e)return a;let o=e.source==="node_modules"?"read from node_modules at answer time":"from GODX_UI_VERSION at launch \u2014 node_modules/@godxjp/ui not resolved";return`${a} \u2014 installed @godxjp/ui ${e.version} (${o})`}function Ee(e=V()){let t=e?P(e.version):null,a=P(d.version);return!e||!t||!a?null:Le(t,a)>0?`\u26A0\uFE0F SERVER OLDER THAN INSTALLED PACKAGE: this server is @godxjp/ui-mcp ${d.version}, the project has @godxjp/ui ${e.version} installed \u2014 this catalog is BEHIND the package and may be missing props and components that exist. Do not conclude from this answer that a prop is unavailable. Restart the session so the server relaunches on the installed version (run \`npx @godxjp/ui sync-rules\` first if the project's .mcp.json still pins an older @godxjp/ui-mcp).`:t.major===a.major?null:`\u26A0\uFE0F MAJOR MISMATCH: this server is @godxjp/ui-mcp ${d.version}, the project has @godxjp/ui ${e.version} installed \u2014 this catalog may describe components that do not exist in the installed package. Pin the MCP to @godxjp/ui-mcp@${e.version} (\`npx @godxjp/ui sync-rules\` updates the project's .mcp.json; a registration outside the project needs \`claude mcp remove <key>\`), then restart the agent.`}async function ce(e,t){let a=await Ne(e,t);if(!De.has(e))return a;let o=V(),n=Ee(o);return`${Oe(o)}
|
|
4561
|
+
finding and needs no suppression.`,Z=[{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 \xB7 Accessible Name Computation 1.2",fix:"Name <Button size='icon'> with aria-label={t('\u2026')} OR from content \u2014 a <VisuallyHidden>/sr-only child beside the aria-hidden glyph. Text inside an aria-hidden subtree names nothing."},{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-hand-rolled-scrollport",severity:"warn",category:"a11y",standard:"WCAG 2.2 SC 2.1.1 (Keyboard) \xB7 WAI-ARIA 1.2 (group) \xB7 Deque axe-core scrollable-region-focusable",fix:'Replace className="overflow-auto / overflow-y-auto / overflow-x-auto / overflow-scroll" on your own element with <ScrollArea label={t("\u2026")} orientation>, which is the tab stop, the role and the localized name \u2014 and withholds all three while there is nothing to scroll. A browser audit only fails a scrollport whose content has NO focusable child, so the same markup is clean or broken depending on the data; this reads the markup instead (gh#825). overflow-hidden is a clipping box, not a scrollport, and is not flagged.'},{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."},{id:"lucide-icon-needs-size",severity:"warn",category:"composition",standard:"WCAG 2.2 SC 1.4.4 \xB7 @godxjp/ui icon scale (--icon-size-*)",fix:'A lucide glyph outside a sizing context draws at its intrinsic 24px. Render it as <Icon as={Lock} size="sm" tone="muted" /> \u2014 the primitive puts it on the --icon-size-* scale and is aria-hidden unless you pass a label.'},{id:"no-hand-rolled-list",severity:"warn",category:"composition",standard:"WAI-ARIA 1.2 (list / listitem) \xB7 HTML Living Standard (ul/ol/li) \xB7 WCAG 2.2 SC 1.3.1",fix:'Build the list as <Flex as="ul" marker="none" direction="col" gap="none"> with <ListRow as="li"> rows \u2014 not a raw <ul>/<ol> (no gap token), not <div role="list">/<div role="listitem">, and never a wrapper around each row: the divider is :not(:last-child) among SIBLINGS, so a row alone in its own wrapper loses it silently (a consumer lost every divider in a settings menu and a dashboard this way). marker="none" keeps the element, the <li> semantics and the gap, and drops the bullet and the --space-5 indent (gh#714). A deliberate exception \u2014 a drag-and-drop Kanban column, an evidence list inside a TableCell \u2014 takes an ui-audit-disable-line that says so.'}];function ae(e){return e?Z.filter(t=>t.category===e):Z}var ne="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)",oe=[{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. The accent @godxjp/ui itself ships is exempt (gh#823) \u2014 this finding is always a colour someone chose."},{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 ie(e){return e?oe.filter(t=>t.category===e):oe}var d={name:"@godxjp/ui-mcp",version:"28.13.0",godxUiCompatibility:"28.13.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 B=[{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"]}}],De=new Set(B.map(e=>e.name));function Oe(e=V()){let t=d.godxUiCompatibility??d.version,a=`@godxjp/ui-mcp ${d.version} (catalog for @godxjp/ui ${t})`;if(!e)return a;let o=e.source==="node_modules"?"read from node_modules at answer time":"from GODX_UI_VERSION at launch \u2014 node_modules/@godxjp/ui not resolved";return`${a} \u2014 installed @godxjp/ui ${e.version} (${o})`}function Ee(e=V()){let t=e?P(e.version):null,a=P(d.version);return!e||!t||!a?null:Le(t,a)>0?`\u26A0\uFE0F SERVER OLDER THAN INSTALLED PACKAGE: this server is @godxjp/ui-mcp ${d.version}, the project has @godxjp/ui ${e.version} installed \u2014 this catalog is BEHIND the package and may be missing props and components that exist. Do not conclude from this answer that a prop is unavailable. Restart the session so the server relaunches on the installed version (run \`npx @godxjp/ui sync-rules\` first if the project's .mcp.json still pins an older @godxjp/ui-mcp).`:t.major===a.major?null:`\u26A0\uFE0F MAJOR MISMATCH: this server is @godxjp/ui-mcp ${d.version}, the project has @godxjp/ui ${e.version} installed \u2014 this catalog may describe components that do not exist in the installed package. Pin the MCP to @godxjp/ui-mcp@${e.version} (\`npx @godxjp/ui sync-rules\` updates the project's .mcp.json; a registration outside the project needs \`claude mcp remove <key>\`), then restart the agent.`}async function ce(e,t){let a=await Ne(e,t);if(!De.has(e))return a;let o=V(),n=Ee(o);return`${Oe(o)}
|
|
4562
4562
|
${n?`${n}
|
|
4563
4563
|
`:""}
|
|
4564
4564
|
${a}`}async function Ne(e,t){switch(e){case"list_skills":return Re();case"list_primitives":return me(t.group);case"list_patterns":return He();case"list_anti_ai_tells":return je(t.category);case"list_redesign_checks":return qe(t.category);case"list_audit_rules":return Ve(t.category);case"list_visual_checks":return Ue(t.category);case"get_anti_ai_tell":return Ge(String(t.name??""));case"get_redesign_check":return We(String(t.symptom??""));case"get_skill_section":return ge(String(t.skill??""),String(t.section??""));case"get_component":return Ye(String(t.name??""),t.verbose===!0);case"get_pattern":return Qe(String(t.name??""));case"get_rule":return Je(typeof t.number=="number"?t.number:void 0);case"get_vocab":return Ze(t.name==null?void 0:String(t.name));case"get_tokens":return et(t.category);case"list_consumer_skills":return Ie();case"get_consumer_skill":return ze(String(t.skill??""),String(t.section??""));case"route_consumer_task":return le(String(t.task??""),{consumerOnly:!0});case"draft_bug_report":return Pe(t);case"check_compatibility":return ue(t.version==null?void 0:String(t.version));case"route_task":return le(String(t.task??""));case"suggest_primitive":return tt(String(t.use_case??""));case"search_components":return at(String(t.query??""));case"get_frame_coverage":return $e(t.name===void 0?void 0:String(t.name));case"lint_jsx":return ot(String(t.jsx??""));default:return`Unknown tool: ${e}`}}function Re(){let e=`# Available skills (${x.length})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui-mcp",
|
|
3
|
-
"version": "28.
|
|
4
|
-
"godxUiCompatibility": "28.
|
|
3
|
+
"version": "28.13.0",
|
|
4
|
+
"godxUiCompatibility": "28.13.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",
|