@godxjp/ui-mcp 17.0.0 → 18.0.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 CHANGED
@@ -40,6 +40,11 @@ var COMPONENTS = [
40
40
  type: "BreadcrumbItemProp[]",
41
41
  description: "Ordered trail of { label, to? } segments above the title."
42
42
  },
43
+ {
44
+ name: "breadcrumbAriaLabel",
45
+ type: "string",
46
+ 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.`
47
+ },
43
48
  {
44
49
  name: "variant",
45
50
  type: '"default" | "narrow" | "flush" | "ghost"',
@@ -292,10 +297,31 @@ import { StatCard } from "@godxjp/ui/data-display";
292
297
  name: "breadcrumb",
293
298
  type: "BreadcrumbProp",
294
299
  description: "Breadcrumb trail rendered in the topbar header for back-navigation."
300
+ },
301
+ {
302
+ name: "mobileNav",
303
+ type: "ReactNode",
304
+ description: "Navigation shown in the AppShell-owned mobile drawer below `lg` (where the docked sidebar is hidden). Defaults to the `sidebar` node; pass a tailored menu, or null to opt out."
305
+ },
306
+ {
307
+ name: "mobileNavLabel",
308
+ type: "string",
309
+ description: "Accessible title for the mobile navigation drawer. Defaults to localized 'Menu'."
310
+ },
311
+ {
312
+ name: "mobileNavOpen",
313
+ type: "boolean",
314
+ description: "Controlled open state of the mobile drawer. Omit for AppShell-owned state."
315
+ },
316
+ {
317
+ name: "onMobileNavOpenChange",
318
+ type: "(open: boolean) => void",
319
+ description: "Change handler for the mobile drawer open state."
295
320
  }
296
321
  ],
297
322
  usage: [
298
323
  "DO pass a <Sidebar> node to `sidebar` (required) and page content to `children` (required) \u2014 these are the only two required props. Everything else is optional and omitting optional slots simply removes that zone from the rendered DOM.",
324
+ "DO rely on AppShell's OWNED mobile drawer below `lg`: it renders a hamburger trigger in the topbar and a focus-trapped Sheet (Esc + overlay close, focus returns to the trigger). `mobileNav` defaults to the `sidebar` node, so the same nav is reachable on mobile with no wiring \u2014 never hide the sidebar without providing this. Pass a tailored `mobileNav`, or `mobileNav={null}` only when navigation lives elsewhere (e.g. a bottom bar).",
299
325
  "DO use the auto-built topbar rail (logo / topbarLeft / topbarRight) for simple shells. Pass a fully configured <Topbar> to the `topbar` prop only when you need live handlers (entity switcher via productMenu, search, notifications, user avatar) \u2014 when `topbar` is provided, logo/topbarLeft/topbarRight are ignored entirely.",
300
326
  "DO wire a single `sidebarCollapsed` boolean between AppShell's `sidebarCollapsed` prop and Sidebar's `collapsed` prop \u2014 AppShell sets `data-collapsed='true'` on the root div (which CSS reads for width transitions) but does NOT own the collapsed state itself; lift the state and pass it down to both.",
301
327
  "DO place breadcrumb content in AppShell's `breadcrumb` prop (renders in the `app-breadcrumb` div inside `<main>` ABOVE children) \u2014 do NOT hand-roll a breadcrumb bar as the first child of children, and do NOT put breadcrumbs inside <Sidebar>.",
@@ -452,6 +478,11 @@ export function LoginPage() {
452
478
  name: "footer",
453
479
  type: "ReactNode",
454
480
  description: "Slot pinned to the bottom of the sidebar below the scrollable nav area. Commonly used for user identity, online status, or version info."
481
+ },
482
+ {
483
+ name: "aria-label",
484
+ type: "string",
485
+ description: `Override the nav landmark's accessible name (defaults to a localized "Main navigation"). Required when more than one Sidebar renders at once (e.g. a docked sidebar + its mobile-drawer twin) \u2014 two nav landmarks sharing one name/role fail landmark-unique.`
455
486
  }
456
487
  ],
457
488
  usage: [
@@ -459,6 +490,8 @@ export function LoginPage() {
459
490
  "DO: Add content: SidebarItemProp[] to any SidebarItemProp to create a collapsible submenu group. The parent item's icon is required even for groups. The group auto-opens and highlights when activeId matches any descendant.",
460
491
  "DO: Mirror the collapsed boolean between AppShell's sidebarCollapsed prop and Sidebar's collapsed prop \u2014 they must stay in sync so the shell layout grid adjusts correctly.",
461
492
  "DO: Use the footer prop for user info or status \u2014 it is pinned below the scroll area and does not scroll away.",
493
+ "DO: Render a leaf as a real link with `item.href` (a real <a>, so right-click / open-in-new-tab work) or, for a framework router <Link>, return that single element from `renderItem` \u2014 the Sidebar merges the row onto it via Slot so the link IS the row and the ONLY interactive element (no nested <button>). Never put a <button>/<a> inside a default row.",
494
+ "DO: Rely on route-synchronized group expansion \u2014 a group OPENS automatically whenever `activeId` moves to one of its children (e.g. after a deep-link navigation), revealing the newly-active child; users can still collapse/expand manually.",
462
495
  "DON'T: Manage collapse state inside the Sidebar \u2014 it is stateless. Hoist the boolean to your shell/page state and pass it down via both AppShell.sidebarCollapsed and Sidebar.collapsed.",
463
496
  "DON'T: Nest children more than one level deep \u2014 only top-level items can have children; grandchild items are not rendered."
464
497
  ],
@@ -698,6 +731,11 @@ import { PanelLeftClose, Search } from "lucide-react";
698
731
  type: "BreadcrumbItemProp[]",
699
732
  required: true,
700
733
  description: "Array of { label, to? } \u2014 omit `to` on the last (current) segment."
734
+ },
735
+ {
736
+ name: "aria-label",
737
+ type: "string",
738
+ description: `Override the nav landmark's accessible name (defaults to a localized "Breadcrumb"). Required when more than one Breadcrumb renders on the same page/view \u2014 two nav landmarks sharing one name/role fail landmark-unique.`
701
739
  }
702
740
  ],
703
741
  usage: [
@@ -1061,7 +1099,7 @@ import { Card, CardContent } from "@godxjp/ui/data-display";
1061
1099
  name: "columns",
1062
1100
  type: "ColumnDef<T>[]",
1063
1101
  required: true,
1064
- description: "Lean column definitions (adapted to TanStack internally). Each column: { key: string; header: ReactNode; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hiddenOnMobile?: boolean; enableHiding?: boolean; pin?: 'end' }. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow \u2014 pin at most one column."
1102
+ description: "Lean column definitions (adapted to TanStack internally). Each column: { key: string; header: ReactNode; ariaLabel?: string; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hiddenOnMobile?: boolean; enableHiding?: boolean; pin?: 'end' }. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow \u2014 pin at most one column. ariaLabel gives a VISUALLY-EMPTY header (header='' \u2014 an action or selection column) a screen-reader name (e.g. 'Actions'/'Select'): it renders as an sr-only label inside the <th> so the column is never nameless (axe: empty-table-header). DataTable dev-warns when a column has an empty header and no ariaLabel."
1065
1103
  },
1066
1104
  {
1067
1105
  name: "getRowId",
@@ -1179,6 +1217,8 @@ import { Card, CardContent } from "@godxjp/ui/data-display";
1179
1217
  "DO use DataTable.Toolbar as the immediate child that wraps search/filter controls on the left and DataTable.DensityToggle/action buttons on the right. DataTable.BulkActions inside the toolbar auto-hides when selection count is 0; it accepts either plain ReactNode children (built-in 'N selected' status bar) or a (count)=>node render-prop (you own the whole bar).",
1180
1218
  "DO reach for the grid chrome (DataTable.Search, DataTable.ViewOptions, DataTable.Pagination pageSizeOptions) when you need global search, a column 'set view' picker, or numbered pagination \u2014 these are the merged former-DataGrid features, now on the one DataTable. Drive them client-side by default; pass the matching state + manual* flag for a server query.",
1181
1219
  "DO use ColumnDef.render for custom cell content (Badge, Link, RowActions). For plain string/number fields render can be omitted \u2014 DataTable falls back to String(row[key]).",
1220
+ "DO give every visually-empty column an accessible header via `ariaLabel` \u2014 a row-actions column (`header: ''`, `pin: 'end'`) sets `ariaLabel: t('actions')`, so screen readers announce the column and axe reports no `empty-table-header`. DataTable dev-warns any column that renders a `<th>` with neither visible text nor an `ariaLabel`. The selection column added by `selectable` is already named by its SelectAll checkbox \u2014 no `ariaLabel` needed there.",
1221
+ "COLUMN SEMANTICS + KEYBOARD: a `sortable` header renders as a real <button> inside the <th> with `aria-sort` (ascending/descending/none) on the <th>; it is Tab-reachable and toggles asc \u2192 desc \u2192 cleared on Enter/Space/click. A selection column exposes a header 'select all' Checkbox (indeterminate when a subset is selected) and a per-row Checkbox, each keyboard-operable with Space. An action column is visually empty but carries an `ariaLabel`; its per-row controls (kebab menu / buttons) own their own accessible names and keyboard behavior. Row click (`onRowClick`) is suppressed when the user activates an interactive descendant.",
1182
1222
  "DO NOT nest DataTable.Content in a conditional \u2014 it is already guarded internally. If you need to override the table body slot, drop exactly one <DataTable.Content /> in children; DataTable auto-detects it by displayName and skips the default."
1183
1223
  ],
1184
1224
  useCases: [
@@ -1300,6 +1340,7 @@ export default function InvoiceList({
1300
1340
  usage: [
1301
1341
  'DO always wrap body content in <CardContent> \u2014 the bare <Card> div has zero inner padding; content renders flush against card edges without it. Never add className="p-4" directly on <Card> as a substitute.',
1302
1342
  "DO put titles/descriptions in <CardHeader>/<CardTitle>/<CardDescription>. Use <CardHeader banded> for a visually separated muted-background header band (mirrors <CardFooter separated>). Pair with <CardAction> inside a flex-row CardHeader for header-level action buttons.",
1343
+ 'DO set <CardTitle level={n}> to keep a valid document outline (h1 \u2192 h2 \u2192 h3, no skipped levels): CardTitle renders <h3> by default, so a section card directly under a page <h1> needs level={2}. Pick the level by OUTLINE position, NEVER for visual size \u2014 the title size is fixed by tokens and does not change with level. When the card title is a styled label rather than a section heading, use <CardTitle as="p"> so it is not announced as a heading.',
1303
1344
  "DO use <CardContent flush> for edge-to-edge children such as DataTable, Table, or a Tabs list \u2014 this removes horizontal padding. Combine with <CardContent tight> when there is no visual gap needed after the header, and <CardContent solo> when there is no CardHeader above (top padding matches the card shell).",
1304
1345
  "DO use <CardFooter separated> to render a top-bordered action band (Save/Cancel buttons, table summary row). Use <CardFooter flush> for a full-bleed footer bar.",
1305
1346
  "DO use <CardCover> as the first child for full-bleed cover media \u2014 the header below it uses card-section top spacing, not the card shell.",
@@ -1688,17 +1729,31 @@ import { Smartphone } from "lucide-react";
1688
1729
  type: '"muted" | "success" | "warning" | "destructive" | "info"',
1689
1730
  defaultValue: '"muted"',
1690
1731
  description: "Medallion colour intent (a subset of the shared tone vocabulary; `destructive` is the DS name for a danger state). Tints the icon foreground + fill from the matching role token \u2014 set `success` for a confirmation zero-state (e.g. device approved) instead of hand-rolling a `.ui-success-state` class."
1732
+ },
1733
+ {
1734
+ name: "titleLevel",
1735
+ type: "1 | 2 | 3 | 4",
1736
+ defaultValue: "3",
1737
+ description: "Semantic heading level of the title. Pick it to keep the page outline valid (h1 \u2192 h2 \u2192 h3, no skipped levels), NEVER for visual size \u2014 the title size is fixed regardless of level. A page/onboarding empty state directly under the page h1 uses titleLevel={2}; one nested in an already-h2 section keeps the default 3."
1738
+ },
1739
+ {
1740
+ name: "titleAs",
1741
+ type: '"h1" | "h2" | "h3" | "h4" | "p" | "div"',
1742
+ description: "Render the title as a non-heading element (p/div) instead of a heading \u2014 for a compact/section empty state inside a section that already owns its heading, so the message is not announced as a heading and cannot skip an outline level. Overrides titleLevel."
1691
1743
  }
1692
1744
  ],
1693
1745
  usage: [
1694
- "DO always pass `title` \u2014 it is the only required prop and renders an `<h3>`; omitting it causes a blank silent render with no visible error.",
1746
+ "DO always pass `title` \u2014 it is the only required prop and renders a heading (`<h3>` by default); omitting it causes a blank silent render with no visible error.",
1747
+ 'DO set `titleLevel` to match the page outline (page h1 \u2192 section h2 \u2192 nested h3) so the empty state does not trigger a heading-order violation. Choose the level for OUTLINE position, never for visual size \u2014 the size never changes with the level. When the empty state sits in a section that already has its own heading, use `titleAs="p"` so the message is not a heading at all.',
1695
1748
  'DO use `tone="success"` (or warning/destructive/info) for a semantic confirmation/alert zero-state \u2014 it recolours the icon medallion from the role token; do NOT hand-roll a `.ui-success-state` class that scopes `--empty-state-icon-*`.',
1696
1749
  "DO use the `icon` prop (a Lucide icon component, not a JSX element) to give visual context \u2014 e.g. `icon={InboxIcon}` for empty inboxes, `icon={SearchIcon}` after a failed search. Pass the component reference, not `<InboxIcon />`.",
1697
1750
  "DO use `action` (a `ReactNode`, typically a `<Button>`) for actionable zero-states \u2014 e.g. 'Create first invoice' \u2014 so users have a clear next step instead of a dead end.",
1698
1751
  "DO NOT hand-roll a `data.length === 0 ? <EmptyState /> : <DataTable />` conditional \u2014 `DataTable` already embeds an `EmptyState` in its body when `data` is empty. Use the `empty=` prop on `DataTable` to customise it, not a wrapper conditional.",
1699
1752
  "DO NOT use EmptyState inside a `DataState` or `InfiniteQueryState` for the loading or error states \u2014 those widgets handle skeleton/error themselves; pass `EmptyState` only to their `empty=` prop for the zero-items case.",
1700
1753
  "DO NOT add padding directly on `EmptyState` via `className` when placing it inside a `Card` \u2014 wrap it in `<CardContent>` first; EmptyState is a self-contained block with its own internal spacing via `ui-empty-state` styles.",
1701
- "DO omit optional secondary sections when absence has no user value. Otherwise use variant='compact' or 'section'; reserve page for the primary page job."
1754
+ "DO omit optional secondary sections when absence has no user value. Otherwise use variant='compact' or 'section'; reserve page for the primary page job.",
1755
+ "DO match empty-state visual weight to the section's importance and expected content density \u2014 a low-priority 'no received invitations' block uses variant='compact' (no medallion, minimal padding), not the full page treatment that would outweigh real content.",
1756
+ "DO NOT wrap every empty condition in its own bordered Card. A compact/section empty state sits directly in the existing CardContent / section it belongs to; a dedicated bordered Card is only for a page-level or standalone zero-state."
1702
1757
  ],
1703
1758
  useCases: [
1704
1759
  "Zero-row admin list pages (invoices, accounts, transactions) that are NOT backed by a `DataTable` \u2014 e.g. a card-grid or custom list layout where DataTable's built-in empty state doesn't apply.",
@@ -1926,7 +1981,9 @@ import { Smartphone } from "lucide-react";
1926
1981
  "DON'T: wrap DataState in your own conditional \u2014 e.g. `{query.isSuccess && <DataState \u2026>}`. DataState IS the conditional; the outer guard is redundant and breaks the retry/refetch skeleton.",
1927
1982
  "DON'T: use DataState for `useInfiniteQuery` results. The `query` prop type is `UseQueryResult<T>`, not `UseInfiniteQueryResult`. Use `InfiniteQueryState` (from `@godxjp/ui/query`) instead, which accepts `flatten` and renders a load-more footer.",
1928
1983
  "DO: classify errors by cause. Use session renewal/sign-in for 401, access guidance for 403, contextual correction for domain errors, and opt into showRetry only for transient network/5xx errors.",
1929
- "DO: pass prerequisite for enabled:false queries. Pending + fetchStatus idle is unstarted, not loading, and never renders a skeleton."
1984
+ "DO: pass prerequisite for enabled:false queries. Pending + fetchStatus idle is unstarted, not loading, and never renders a skeleton.",
1985
+ "DO: rely on the localized, cause-specific error message \u2014 the raw backend/token/stack text is never shown. For a domain-specific message (e.g. a 422 field error) pass a custom errorRenderer.",
1986
+ "DO: expect a background refetch over existing data to keep the content on screen with a polite sr-only busy status \u2014 it does not flash the skeleton. Only the initial fetch (isPending) shows the skeleton."
1930
1987
  ],
1931
1988
  useCases: [
1932
1989
  "A detail page that loads a single invoice/journal entry via `useQuery` \u2014 DataState renders the skeleton row while fetching, an error alert with retry if the API fails, and the `<InvoiceCard>` only when data is confirmed non-null.",
@@ -1947,12 +2004,7 @@ import { Smartphone } from "lucide-react";
1947
2004
  {(d) => <MemberTable items={d.items} />}
1948
2005
  </DataState>`,
1949
2006
  storyPath: "query/DataState.stories.tsx",
1950
- rules: [
1951
- "Errors are classified by cause (via classifyQueryError). Retry is offered ONLY for transient/network/5xx; a 401/expired-token routes to session renewal through onAuthError; 403/404/422 present a cause-aware message with no blind retry.",
1952
- "The user-facing detail is a localized, cause-specific message \u2014 the raw backend/token/stack text is never shown. For a domain-specific message (e.g. a 422 field error) pass a custom errorRenderer.",
1953
- "A disabled/unstarted query (enabled:false \u2192 isPending with fetchStatus 'idle') renders the prerequisite slot, never the skeleton. Always pass prerequisite for tenant/org-gated queries.",
1954
- "A background refetch over existing data keeps the content on screen with a polite sr-only busy status \u2014 it does not flash the skeleton. Only the initial fetch (isPending) shows the skeleton."
1955
- ]
2007
+ rules: []
1956
2008
  },
1957
2009
  {
1958
2010
  name: "InfiniteQueryState",
@@ -2087,7 +2139,7 @@ import { Smartphone } from "lucide-react";
2087
2139
  {
2088
2140
  name: "FormField",
2089
2141
  group: "data-entry",
2090
- tagline: "Wraps a control with label, helper, and error; injects aria-describedby/aria-invalid onto the child. Reads the parent Form's layout (vertical/horizontal) \u2014 overridable per field.",
2142
+ tagline: "Wraps a control with label, helper, and error; injects the accessible name (aria-labelledby), description (aria-describedby) and validation (aria-errormessage/aria-invalid/aria-required) contract onto the child, which forwards it to its real semantic focus target. Reads the parent Form's layout (vertical/horizontal) \u2014 overridable per field.",
2091
2143
  props: [
2092
2144
  {
2093
2145
  name: "id",
@@ -2147,7 +2199,10 @@ import { Smartphone } from "lucide-react";
2147
2199
  "DO use the `error` prop (not a hand-rolled `<p>`) for validation messages \u2014 it renders with `role='alert'` and `text-destructive` styling and overrides `helper` automatically. Never render an error paragraph alongside FormField.",
2148
2200
  "DO use `labelAddon` (a ReactNode rendered inline after the label text) for supplementary controls such as a tooltip trigger or a 'copy' icon button \u2014 never insert such controls as siblings outside FormField, which breaks layout.",
2149
2201
  "DON'T wrap `Switch` in FormField \u2014 use `Field` instead, which already handles the label, hidden `<input name>` for HTML form submission, error, and helper internally.",
2150
- "DON'T use FormField for checkbox-beside-label or radio-beside-label patterns \u2014 use `Field` (single checkbox/radio with description) or `CheckboxGroup` / `RadioGroup` (multiple options), which have their own integrated labelling."
2202
+ "DON'T use FormField for checkbox-beside-label or radio-beside-label patterns \u2014 use `Field` (single checkbox/radio with description) or `CheckboxGroup` / `RadioGroup` (multiple options), which have their own integrated labelling.",
2203
+ "CONTRACT (which element owns each ARIA relationship): every data-entry control accepts and FORWARDS the injected props to its real semantic focus target, not a wrapper div \u2014 Input/Textarea/NumberInput \u2192 the `<input>/<textarea>`; Select/SearchSelect/Cascader/TreeSelect \u2192 the `role=combobox` trigger (with aria-expanded + aria-haspopup + aria-controls per the WAI-ARIA APG combobox pattern); DatePicker/MonthPicker/TimePicker \u2192 the typeable `role=combobox` input (aria-haspopup=dialog); ColorPicker \u2192 the `<input type=color>` swatch; SearchInput \u2192 the `role=searchbox` input. GROUP controls own the relationship on their container: RadioGroup \u2192 `role=radiogroup` (full validation incl. aria-invalid/-errormessage/-required); CheckboxGroup, DateRangePicker/MonthRangePicker (two inputs), and Transfer \u2192 `role=group` \u2014 per ARIA 1.2 a group is not a widget, so the error id is folded into aria-describedby instead of aria-invalid/-errormessage. Upload forwards the label/description onto its native `<input type=file>`; its visible dropzone/button keeps its own action label. This forwarding is implemented once in `src/lib/field-a11y.ts` (`pickFieldA11y` / `pickGroupFieldA11y` / `resolveFieldA11y`) \u2014 do not reinvent it per control.",
2204
+ "NATIVE FORM PARTICIPATION: pass `name` to a control for HTML form submission \u2014 Input/Textarea/NumberInput/Select submit natively; SearchSelect submits via a hidden input; DatePicker/TimePicker emit ISO strings (`yyyy-MM-dd` / 24h `HH:mm`); the range pickers emit `${name}_from` / `${name}_to`. `required`/`readOnly`/`disabled` map to the underlying control. Cascader/TreeSelect/Transfer/Upload are NOT native-form-submittable \u2014 read their value via `onValueChange` and submit programmatically.",
2205
+ "ERROR TIMING & RECOVERY: pass `error` only after a field is dirty or the form is submitted (don't show errors on pristine mount). The error node renders with `role='alert'` so it is announced live the moment it appears; clearing `error` (e.g. after the user corrects the value or a server round-trip succeeds) removes aria-invalid and restores the helper. On submit, focus the first invalid control and/or render an error summary that links to each field by `id`."
2151
2206
  ],
2152
2207
  useCases: [
2153
2208
  "Labelling a text `Input` or `Textarea` in an invoice-entry form, showing a red asterisk for required fields and surfacing server validation errors returned from a Laravel FormRequest.",
@@ -2520,6 +2575,52 @@ import { Smartphone } from "lucide-react";
2520
2575
  description: "Label for the clear row (data-driven combobox mode)."
2521
2576
  },
2522
2577
  { name: "disabled", type: "boolean", description: "Disables the entire select." },
2578
+ {
2579
+ name: "readOnly",
2580
+ type: "boolean",
2581
+ defaultValue: "false",
2582
+ 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."
2583
+ },
2584
+ {
2585
+ name: "size",
2586
+ type: '"xs" | "sm" | "md" | "lg"',
2587
+ description: "Searchable mode only. Height tier forwarded to the SearchSelect trigger Button. For the compound API use SelectTrigger's own size prop instead (below)."
2588
+ },
2589
+ {
2590
+ name: "open",
2591
+ type: "boolean",
2592
+ description: "Searchable mode only. Controlled popover open state (uncontrolled by default). Pair with onOpenChange."
2593
+ },
2594
+ {
2595
+ name: "onOpenChange",
2596
+ type: "(open: boolean) => void",
2597
+ 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."
2598
+ },
2599
+ {
2600
+ name: "search",
2601
+ type: "string",
2602
+ description: "Searchable mode only. Controlled search-box query (uncontrolled by default). Pair with onSearchChange."
2603
+ },
2604
+ {
2605
+ name: "onSearchChange",
2606
+ type: "(query: string) => void",
2607
+ description: "Searchable mode only. Fires on every keystroke in the search box."
2608
+ },
2609
+ {
2610
+ name: "filterOption",
2611
+ type: "(option: SearchSelectOptionProp, query: string) => boolean",
2612
+ 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."
2613
+ },
2614
+ {
2615
+ name: "renderError",
2616
+ type: "(params: { message: string; retry: () => void }) => React.ReactNode",
2617
+ description: "Searchable mode only. Custom error slot, overriding the default errorMessage row. retry() reloads from the first page."
2618
+ },
2619
+ {
2620
+ name: "renderLoadMore",
2621
+ type: "(params: { hasMore: boolean; loading: boolean; loadMore: () => void }) => React.ReactNode",
2622
+ 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."
2623
+ },
2523
2624
  {
2524
2625
  name: "name",
2525
2626
  type: "string",
@@ -2545,6 +2646,12 @@ import { Smartphone } from "lucide-react";
2545
2646
  type: '"sm" | "md"',
2546
2647
  defaultValue: '"md"',
2547
2648
  description: "Compound API only. Size variant on the SelectTrigger sub-component."
2649
+ },
2650
+ {
2651
+ name: "SelectTrigger showIndicator",
2652
+ type: "boolean",
2653
+ defaultValue: "true",
2654
+ 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."
2548
2655
  }
2549
2656
  ],
2550
2657
  usage: [
@@ -2555,7 +2662,12 @@ import { Smartphone } from "lucide-react";
2555
2662
  "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.",
2556
2663
  "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.",
2557
2664
  "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.",
2558
- "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."
2665
+ "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.",
2666
+ "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.",
2667
+ "DO use readOnly (searchable mode) for a value that must stay visible and submittable but not editable in this view \u2014 it differs from disabled: the control stays focusable and its value still submits. clearable is ignored while readOnly.",
2668
+ "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).",
2669
+ "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.",
2670
+ "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."
2559
2671
  ],
2560
2672
  useCases: [
2561
2673
  "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.",
@@ -3582,10 +3694,14 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
3582
3694
  {
3583
3695
  name: "items",
3584
3696
  type: "{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean }[]",
3585
- description: "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels."
3697
+ 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 (gh#175)."
3586
3698
  },
3587
3699
  { name: "value", type: "string", description: "Controlled active tab key." },
3588
- { name: "defaultValue", type: "string", description: "Uncontrolled initial tab key." },
3700
+ {
3701
+ name: "defaultValue",
3702
+ type: "string",
3703
+ description: "Uncontrolled initial tab key. Ignored (falls back to the first enabled item) when it names a disabled item or an unknown key."
3704
+ },
3589
3705
  {
3590
3706
  name: "onValueChange",
3591
3707
  type: "(value: string) => void",
@@ -3598,7 +3714,9 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
3598
3714
  "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.",
3599
3715
  "DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",
3600
3716
  '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.',
3601
- "DON'T: hand-roll the active-indicator underline or selected-state ring \u2014 `TabsTrigger` already applies `data-[state=active]` styles including the `after:` line element for the `line` variant. Adding your own underline breaks the design."
3717
+ "DON'T: hand-roll the active-indicator underline or selected-state ring \u2014 `TabsTrigger` already applies `data-[state=active]` styles including the `after:` line element for the `line` variant. Adding your own underline breaks the design.",
3718
+ "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 was gh#175 and is now the framework's job (#175).",
3719
+ "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`."
3602
3720
  ],
3603
3721
  useCases: [
3604
3722
  "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.",
@@ -3656,10 +3774,33 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
3656
3774
  name: "showSizeChanger",
3657
3775
  type: "boolean",
3658
3776
  description: "Show the page-size selector beside the pager."
3777
+ },
3778
+ {
3779
+ name: "hideOnSinglePage",
3780
+ type: "boolean",
3781
+ defaultValue: "true",
3782
+ description: "Hide the control when there is nothing to page through \u2014 zero items OR exactly one page. Set false to opt in to the bar on a single page (e.g. to keep showTotal visible); total=0 is always hidden."
3783
+ },
3784
+ {
3785
+ name: "simple",
3786
+ type: "boolean",
3787
+ description: "Compact form for narrow contexts \u2014 Prev / n\xB7N / Next, no page-number buttons. The intentional mobile transformation (desktop never wraps)."
3788
+ },
3789
+ {
3790
+ name: "disabled",
3791
+ type: "boolean",
3792
+ description: "Disable all navigation controls."
3793
+ },
3794
+ {
3795
+ name: "aria-label",
3796
+ type: "string",
3797
+ description: `Override the nav landmark's accessible name (defaults to a localized "Pagination"). Required when more than one Pagination renders on the same page/view \u2014 two nav landmarks sharing one name/role fail landmark-unique.`
3659
3798
  }
3660
3799
  ],
3661
3800
  usage: [
3662
3801
  "DO always control Pagination externally: store `value` (page) and `pageSize` in React state (or URL params), and update both in the `onValueChange(page, pageSize)` callback. Pagination is fully controlled \u2014 it has no internal state and will not move unless `value` changes.",
3802
+ "DO let Pagination hide itself for zero items and single pages (`hideOnSinglePage`, default true) \u2014 it is navigation between multiple result pages. Render it inside a table footer only in the DATA state: never during loading, empty, error, or an unmet prerequisite. Pass `hideOnSinglePage={false}` only when you still want the bar on one page to keep `showTotal` visible.",
3803
+ "DO trust Pagination to stay ONE horizontal row on desktop (it never wraps). For genuinely narrow viewports use `simple` for the intentional compact transformation rather than letting controls wrap.",
3663
3804
  "DO pass `total` as the raw item count (not page count). The component computes `Math.ceil(total / pageSize)` internally; passing a pre-computed page count as `total` will over-paginate.",
3664
3805
  "DO use `showSizeChanger` together with `pageSizeOptions` when the user needs density control (default options are [10, 20, 50, 100]). When `showSizeChanger` is omitted the page-size Select is not rendered at all \u2014 do NOT hand-roll your own Select beside Pagination.",
3665
3806
  "DO use `simple` mode for compact contexts (mobile, sidebars, sheet footers) \u2014 it renders Prev / `n / total` / Next with no page-number buttons. Use the full form for primary admin list pages.",
@@ -7248,7 +7389,7 @@ export default function PasswordBlock() {
7248
7389
  {
7249
7390
  name: "appearance",
7250
7391
  type: '"labeled" | "icon"',
7251
- description: `Trigger presentation. "labeled" (default) shows the leading icon + selected value in a full-width control. "icon" is the supported icon-only topbar trigger (e.g. a globe locale switcher): it structurally drops the value text and the picker's owned width and hides the chevron, squares the box to the density-aware --control-height tap target (\u226544px on touch), and always keeps the localized aria-label so it can never ship nameless. Menu options still show localized names. Use it instead of overriding internal descendants / width classes with CSS.`
7392
+ description: 'Trigger presentation. "labeled" (default) shows the leading icon + selected value: it hugs its content below `sm` (`w-auto max-w-full`) and takes a per-kind fixed width from `sm` up \u2014 it no longer stretches to `w-full` on narrow screens, so it fits a topbar (pass className="w-full" for a full-width form field). "icon" is the supported icon-only topbar trigger (e.g. a globe locale switcher): it structurally drops the value text and the picker\'s owned width and hides the chevron, squares the box to the density-aware --control-height tap target (\u226544px on touch), and always keeps the localized aria-label so it can never ship nameless. Menu options still show localized names (the selected value is checked in the popup). Use these instead of overriding internal descendants / width classes with CSS.'
7252
7393
  },
7253
7394
  {
7254
7395
  name: "className",
@@ -7833,8 +7974,8 @@ var PROP_VOCABULARY = [
7833
7974
  name: "GapProp",
7834
7975
  concept: "Shared layout gap scale.",
7835
7976
  values: ["xs", "sm", "md", "lg", "xl"],
7836
- usedBy: ["Stack", "Inline"],
7837
- notes: "Inline uses an Exclude<GapProp, 'xl'> subset."
7977
+ usedBy: ["Flex"],
7978
+ notes: 'The single shared gap scale on Flex \u2014 the one layout primitive (default direction="row"; use direction="col" for vertical rhythm; these replaced the removed Stack/Inline).'
7838
7979
  },
7839
7980
  {
7840
7981
  name: "TitleProp",
@@ -7893,7 +8034,7 @@ var TOKENS = [
7893
8034
  name: "--duration-{fast,base,slow}",
7894
8035
  category: "primitive",
7895
8036
  tier: "primitive",
7896
- role: "Motion durations (150 / 250 / 500ms). Read these instead of a literal `0.5s` for enter/transition timing (rule #2). dxs-kintai keeps motion short; honour `prefers-reduced-motion` at the call site."
8037
+ role: "Motion durations (150 / 250 / 500ms). Read these instead of a literal `0.5s` for enter/transition timing (rule #2). the reference design keeps motion short; honour `prefers-reduced-motion` at the call site."
7897
8038
  },
7898
8039
  {
7899
8040
  name: "--ease-{standard,emphasized,decelerate,accelerate}",
@@ -8119,7 +8260,7 @@ var COMPONENT_TOKENS = [
8119
8260
  {
8120
8261
  "name": "--card-shadow",
8121
8262
  "value": "0 0 0 0 transparent",
8122
- "description": "Resting elevation \u2014 quiet by default (rule #44): cards are flat (1px border, no shadow) in the * dxs-kintai baseline. A service that wants lifted cards sets this to an elevation token once, * e.g. --card-shadow: var(--shadow-sm), and every Card picks up the shadow with no markup change."
8263
+ "description": "Resting elevation \u2014 quiet by default (rule #44): cards are flat (1px border, no shadow) in the * reference-design baseline. A service that wants lifted cards sets this to an elevation token once, * e.g. --card-shadow: var(--shadow-sm), and every Card picks up the shadow with no markup change."
8123
8264
  },
8124
8265
  {
8125
8266
  "name": "--card-glow",
@@ -9118,7 +9259,7 @@ var CARDINAL_RULES = [
9118
9259
  {
9119
9260
  number: 11,
9120
9261
  title: "Submodule discipline",
9121
- body: "Two-PR workflow: (1) submodule PR \u2192 `main`, (2) umbrella PR \u2192 bump pin. Never push a pin to a SHA not on the submodule remote."
9262
+ body: "Two-PR workflow: (1) submodule PR \u2192 `main`, (2) downstream PR \u2192 bump pin. Never push a pin to a SHA not on the submodule remote."
9122
9263
  },
9123
9264
  {
9124
9265
  number: 12,
@@ -9263,7 +9404,7 @@ var CARDINAL_RULES = [
9263
9404
  {
9264
9405
  number: 40,
9265
9406
  title: "Pages are mobile-first",
9266
- body: "Author and verify every page at 320\u2013390px FIRST. Spacing comes only from `Stack` / `Inline` `gap` + `ResponsiveGrid columns={2|3|4}` (which collapse to a single column on narrow screens) \u2014 never raw `p-*` / `gap-*` / `space-*` utilities for page layout. Wide tables scroll horizontally on small screens (don't force-fit them); dialogs and sheets are full-height on mobile. Touch targets \u2265 44\xD744px."
9407
+ body: 'Author and verify every page at 320\u2013390px FIRST. Spacing comes only from `Flex` `gap` (vertical rhythm = `Flex direction="col"`, control rows = the default `direction="row"`) + `ResponsiveGrid columns={2|3|4}` (which collapse to a single column on narrow screens) \u2014 never raw `p-*` / `gap-*` / `space-*` utilities for page layout. Wide tables scroll horizontally on small screens (don\'t force-fit them); dialogs and sheets are full-height on mobile. Touch targets \u2265 44\xD744px.'
9267
9408
  },
9268
9409
  {
9269
9410
  number: 41,
@@ -9320,48 +9461,46 @@ var PATTERNS = [
9320
9461
  // 0) \u2605 MOST COMMON: <Card> body has NO padding (content is flush against the edges)
9321
9462
  // Cause: the bare <Card> has ZERO inner padding \u2014 it MUST contain <CardContent>.
9322
9463
  // Don't hand-roll padding with className="p-4" on the Card either.
9323
- // \u274C <Card><Stack gap="md">\u2026fields\u2026</Stack></Card> // flush, no padding
9324
- // \u274C <Card className="p-4">\u2026fields\u2026</Card> // hand-rolled padding
9325
- // \u2705 <Card><CardContent><Stack gap="md">\u2026fields\u2026</Stack></CardContent></Card>
9464
+ // \u274C <Card><Flex direction="col" gap="md">\u2026fields\u2026</Flex></Card> // flush, no padding
9465
+ // \u274C <Card className="p-4">\u2026fields\u2026</Card> // hand-rolled padding
9466
+ // \u2705 <Card><CardContent><Flex direction="col" gap="md">\u2026fields\u2026</Flex></CardContent></Card>
9326
9467
  // Titles \u2192 <CardHeader><CardTitle>. Only go flush deliberately for a full-bleed table:
9327
9468
  // \u2705 <Card><CardContent flush><DataTable/></CardContent></Card>
9328
9469
  // GENERAL RULE \u2014 compose godx-ui primitives FULLY; never hand-roll what one ships:
9329
9470
  // padding \u2192 CardContent (not p-4) \xB7 controls \u2192 Input/Select/Button (not raw <input>/<select>/<button>)
9330
- // empty rows \u2192 DataTable's built-in empty / <EmptyState> (not a custom data.length===0 guard).
9331
- // If a primitive exists, USE it \u2014 don't reinvent it.
9471
+ // vertical spacing \u2192 Flex direction="col" gap (not space-y-*) \xB7 empty rows \u2192 DataTable's built-in
9472
+ // empty / <EmptyState> (not a custom data.length===0 guard). If a primitive exists, USE it.
9332
9473
 
9333
9474
  // 1) StatCard shows a DOUBLE border (too thick)
9334
9475
  // Cause: StatCard IS already a bordered Card. Don't wrap it.
9335
9476
  // \u274C <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>
9336
9477
  // \u2705 <ResponsiveGrid columns={4}><StatCard label="x" value="1" /></ResponsiveGrid>
9337
9478
  // Need a section title? Use a heading, NOT a Card:
9338
- // \u2705 <Stack gap="sm"><div className="text-sm font-medium">KPI</div>
9339
- // <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Stack>
9479
+ // \u2705 <Flex direction="col" gap="sm"><div className="text-sm font-medium">KPI</div>
9480
+ // <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Flex>
9340
9481
 
9341
9482
  // 2) Badge renders grey with a \u25CB (no colour) for localized/tier labels
9342
- // Cause: it auto-maps only English lifecycle keys. (@godxjp/ui >= 6.1)
9483
+ // Cause: it auto-maps only English lifecycle keys.
9343
9484
  // \u274C <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" />
9344
9485
  // \u2705 <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} /> // tier \u2192 pill, no icon
9345
- // \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
9486
+ // \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
9346
9487
 
9347
9488
  // 3) Table text collapses to one char per line, or a chip wraps
9348
- // Cause: pre-6.1.2. (@godxjp/ui >= 6.1.2 \u2192 cells + chips are nowrap)
9349
- // \u2705 npm i @godxjp/ui@^6.2.0
9350
9489
  // \u2705 give long columns a width: { key: "name", header: "\u6C0F\u540D", width: "w-64" }
9490
+ // (cells + chips are pinned white-space: nowrap by the library.)
9351
9491
 
9352
9492
  // 4) Empty (icon/action) column header shows a blank grey block
9353
- // (@godxjp/ui >= 6.2.0 auto-hides it: [data-slot=table-head][data-empty] \u2192 transparent)
9354
- // \u2705 npm i @godxjp/ui@^6.2.0 // header: "" now renders a transparent cell
9493
+ // \u2705 header: "" renders a transparent cell \u2014 leave it empty, don't inject a space.
9355
9494
 
9356
9495
  // 5) DataTable columns are crushed / squeezed
9357
9496
  // Cause: the table is nested in a narrow grid column.
9358
9497
  // \u274C <ResponsiveGrid columns={3}><div className="lg:col-span-2"><Card><DataTable/></Card></div></ResponsiveGrid>
9359
9498
  // \u2705 Table gets its OWN full-width row: <Card><CardContent flush><DataTable/></CardContent></Card>
9360
9499
 
9361
- // 6) FilterBar has no padding (sticks to the edge)
9500
+ // 6) Toolbar (filter bar) has no padding (sticks to the edge)
9362
9501
  // Cause: it's inside CardContent flush (flush strips padding \u2014 that's for tables).
9363
- // \u274C <Card><CardContent flush><FilterBar/><DataTable/></CardContent></Card>
9364
- // \u2705 <FilterBar/> then <Card><CardContent flush><DataTable/></CardContent></Card>
9502
+ // \u274C <Card><CardContent flush><Toolbar/><DataTable/></CardContent></Card>
9503
+ // \u2705 <Toolbar/> then <Card><CardContent flush><DataTable/></CardContent></Card>
9365
9504
 
9366
9505
  // 7) Sidebar footer looks washed-out / off-design
9367
9506
  // Cause: raw opacity-*/text-[11px]. Use semantic tokens.
@@ -9377,7 +9516,7 @@ var PATTERNS = [
9377
9516
  // Cause: Math.random()/argless new Date() during render (SSR \u2260 client).
9378
9517
  // \u2705 seed deterministically by index, or compute in an event handler.
9379
9518
 
9380
- // 10) Hide a column on mobile / sign-aware KPI delta (@godxjp/ui >= 6.2.0)
9519
+ // 10) Hide a column on mobile / sign-aware KPI delta
9381
9520
  // \u2705 columns: [{ key: "email", header: "\u30E1\u30FC\u30EB", hiddenOnMobile: true }]
9382
9521
  // \u2705 <StatCard label="\u58F2\u4E0A" value="\xA58.2M" delta="+12%" /> // + green / - red; inverse flips`
9383
9522
  },
@@ -9391,7 +9530,7 @@ import { z } from "zod";
9391
9530
  import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@godxjp/ui/data-display";
9392
9531
  import { FormField, Input } from "@godxjp/ui/data-entry";
9393
9532
  import { Button } from "@godxjp/ui/general";
9394
- import { Stack } from "@godxjp/ui/layout";
9533
+ import { Flex } from "@godxjp/ui/layout";
9395
9534
 
9396
9535
  const schema = z.object({
9397
9536
  name: z.string().min(1, "\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059"),
@@ -9409,18 +9548,19 @@ export function SignUpCard() {
9409
9548
  <CardHeader><CardTitle>\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210</CardTitle></CardHeader>
9410
9549
  <CardContent>
9411
9550
  <form id="signup" onSubmit={onSubmit}>
9412
- <Stack gap="md">
9551
+ {/* Vertical rhythm comes from Flex direction="col" gap \u2014 never space-y-*/gap-* utilities. */}
9552
+ <Flex direction="col" gap="md">
9413
9553
  <FormField id="name" label="\u6C0F\u540D" required error={errors.name?.message}>
9414
9554
  <Input id="name" {...register("name")} />
9415
9555
  </FormField>
9416
9556
  <FormField id="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required error={errors.email?.message}>
9417
9557
  <Input id="email" type="email" {...register("email")} />
9418
9558
  </FormField>
9419
- </Stack>
9559
+ </Flex>
9420
9560
  </form>
9421
9561
  </CardContent>
9422
9562
  <CardFooter separated>
9423
- <Button type="submit" form="signup" disabled={isSubmitting}>\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210</Button>
9563
+ <Button type="submit" form="signup" loading={isSubmitting}>\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210</Button>
9424
9564
  </CardFooter>
9425
9565
  </Card>
9426
9566
  );
@@ -9428,33 +9568,97 @@ export function SignUpCard() {
9428
9568
  },
9429
9569
  {
9430
9570
  name: "settings-page-responsive",
9431
- tagline: "Route-backed settings: persistent desktop local navigation, compact mobile navigation, and bounded form content.",
9432
- tags: ["settings", "form", "tabs", "admin"],
9433
- code: `// Research basis: GitHub/Google/Microsoft/Atlassian account settings + Carbon form grid.
9434
- // Use URLs for every destination; do not keep broad settings IA in local tab state.
9435
- import { NavLink, Outlet } from "react-router-dom";
9436
- import { Flex, Stack } from "@godxjp/ui/layout";
9571
+ aliases: ["settings-tabs", "settings-page", "settings-layout"],
9572
+ tagline: "Route-backed settings: persistent desktop local navigation, compact mobile tab strip, deep links + browser history, and bounded form content \u2014 NO Card wrapper, NO undefined CSS.",
9573
+ tags: ["settings", "form", "tabs", "admin", "responsive", "navigation"],
9574
+ code: `// Research basis: GitHub, Google Account, Microsoft Account, Atlassian Account settings +
9575
+ // the IBM Carbon form grid. Every destination is a URL, so deep links and browser back/forward
9576
+ // work for free and the active item is derived from the route \u2014 never from local tab state.
9577
+ //
9578
+ // Desktop (>= lg): persistent vertical local nav on the left + bounded content on the right.
9579
+ // Mobile (< lg): the SAME routes become a horizontal, scrollable tab strip above the content.
9580
+ // Styling uses ONLY real semantic token classes (bg-muted / text-foreground / text-muted-foreground)
9581
+ // and responsive/visibility utilities \u2014 the library ships NO bespoke settings shell/nav CSS classes.
9582
+ import { NavLink, Outlet, Navigate } from "react-router-dom";
9583
+ import { Flex } from "@godxjp/ui/layout";
9437
9584
  import { FormField, Input } from "@godxjp/ui/data-entry";
9585
+ import { Button } from "@godxjp/ui/general";
9438
9586
 
9439
- export function WorkspaceSettings() {
9587
+ const SECTIONS = [
9588
+ { to: "general", label: "\u57FA\u672C\u60C5\u5831" },
9589
+ { to: "security", label: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3" },
9590
+ { to: "notifications", label: "\u901A\u77E5" },
9591
+ ] as const;
9592
+
9593
+ // One link renderer for BOTH breakpoints. NavLink gives us the active state (and sets
9594
+ // aria-current="page") straight from the URL; semantic tokens only, no invented classes.
9595
+ const link = ({ isActive }: { isActive: boolean }) =>
9596
+ [
9597
+ "rounded-md px-3 py-2 text-sm no-underline transition-colors whitespace-nowrap",
9598
+ isActive
9599
+ ? "bg-muted text-foreground font-medium"
9600
+ : "text-muted-foreground hover:text-foreground",
9601
+ ].join(" ");
9602
+
9603
+ export function SettingsLayout() {
9440
9604
  return (
9441
- <Flex gap="lg" className="settings-layout">
9442
- <nav aria-label="Settings" className="settings-local-nav">
9443
- <NavLink to="general">\u57FA\u672C\u60C5\u5831</NavLink>
9444
- <NavLink to="security">\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3</NavLink>
9445
- <NavLink to="notifications">\u901A\u77E5</NavLink>
9605
+ <Flex direction="col" gap="lg" className="lg:flex-row lg:items-start">
9606
+ {/* < lg: horizontal, scrollable route tabs. */}
9607
+ <nav aria-label="\u8A2D\u5B9A" className="flex gap-1 overflow-x-auto lg:hidden">
9608
+ {SECTIONS.map((s) => (
9609
+ <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>
9610
+ ))}
9446
9611
  </nav>
9447
- <main className="settings-content"><Outlet /></main>
9612
+ {/* >= lg: persistent vertical local nav (bounded width, does not shrink). */}
9613
+ <nav aria-label="\u8A2D\u5B9A" className="hidden lg:flex lg:w-56 lg:shrink-0 lg:flex-col lg:gap-1">
9614
+ {SECTIONS.map((s) => (
9615
+ <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>
9616
+ ))}
9617
+ </nav>
9618
+ {/* Bounded content region (~42rem) \u2014 the routed section renders here. min-w-0 lets it shrink. */}
9619
+ <main className="min-w-0 max-w-2xl flex-1"><Outlet /></main>
9448
9620
  </Flex>
9449
9621
  );
9450
9622
  }
9451
9623
 
9452
- // Desktop: local nav + bounded content (roughly 40rem), horizontal form rows where useful.
9453
- // Mobile (375/390px): same route links become a compact scrollable nav; labels stack above controls.
9454
- // Do not add a Card around the whole page. Tabs are only for 2\u20134 peer views within one task.
9455
- export function GeneralSettingsForm() {
9456
- return <Stack gap="md"><FormField id="ws-name" label="\u540D\u524D" layout="horizontal" controlWidth="md"><Input id="ws-name" /></FormField></Stack>;
9457
- }`
9624
+ // A settings section. Horizontal label/control rows on desktop (collapse to stacked on mobile),
9625
+ // controls bounded to a semantic width. FormField OWNS the label\u2194control relationship \u2014 do NOT
9626
+ // hand-roll <Label> + <input>. Save feedback = Button loading prop + a toast in onSuccess.
9627
+ export function GeneralSettingsSection({
9628
+ defaults, onSave, saving, error,
9629
+ }: { defaults: { name: string; email: string }; onSave: (v: FormData) => void; saving: boolean; error?: string }) {
9630
+ return (
9631
+ <form onSubmit={(e) => { e.preventDefault(); onSave(new FormData(e.currentTarget)); }}>
9632
+ <Flex direction="col" gap="md">
9633
+ <FormField id="ws-name" label="\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u540D" layout="horizontal" controlWidth="24rem"
9634
+ helper="\u8ACB\u6C42\u66F8\u3084\u5171\u6709\u30EA\u30F3\u30AF\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002" error={error}>
9635
+ <Input id="ws-name" name="name" defaultValue={defaults.name} />
9636
+ </FormField>
9637
+ <FormField id="ws-email" label="\u9023\u7D61\u5148\u30E1\u30FC\u30EB" layout="horizontal" controlWidth="24rem">
9638
+ <Input id="ws-email" name="email" type="email" defaultValue={defaults.email} />
9639
+ </FormField>
9640
+ <Flex gap="sm"><Button type="submit" loading={saving}>\u5909\u66F4\u3092\u4FDD\u5B58</Button></Flex>
9641
+ </Flex>
9642
+ </form>
9643
+ );
9644
+ }
9645
+
9646
+ // Router wiring \u2014 each destination is addressable, so deep links + back/forward just work:
9647
+ // <Route path="settings" element={<SettingsLayout/>}>
9648
+ // <Route index element={<Navigate to="general" replace/>} />
9649
+ // <Route path="general" element={<GeneralSettingsSection .../>} />
9650
+ // <Route path="security" element={<SecuritySection/>} />
9651
+ // <Route path="notifications" element={<NotificationsSection/>} />
9652
+ // </Route>
9653
+
9654
+ // \u2500\u2500 DO / DON'T \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
9655
+ // \u2705 Tabs (navigation) are fine for a SMALL number (2\u20134) of PEER views inside ONE task/section
9656
+ // \u2014 e.g. "\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB" \u21C4 "\u74B0\u5883\u8A2D\u5B9A" within a single account page \u2014 where losing the URL is OK.
9657
+ // \u26D4 Tabs as the PRIMARY IA for broad settings (Account / Security / Billing / Members / \u2026):
9658
+ // use route-backed local navigation so every area is deep-linkable and back/forward works.
9659
+ // \u26D4 Do NOT wrap the whole settings page in a Card, and do NOT make desktop a horizontal tab bar
9660
+ // \u2014 persistent local nav scales to many sections; a tab strip does not.
9661
+ // \u26D4 Do NOT invent CSS class names for the shell/nav/content \u2014 compose Flex + semantic tokens.`
9458
9662
  },
9459
9663
  {
9460
9664
  name: "confirm-destructive",
@@ -9464,7 +9668,7 @@ export function GeneralSettingsForm() {
9464
9668
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
9465
9669
  import { Input } from "@godxjp/ui/data-entry";
9466
9670
  import { Button } from "@godxjp/ui/general";
9467
- import { Stack } from "@godxjp/ui/layout";
9671
+ import { Flex } from "@godxjp/ui/layout";
9468
9672
  import { toast } from "sonner";
9469
9673
 
9470
9674
  export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {
@@ -9476,9 +9680,9 @@ export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolea
9476
9680
  <DialogTitle>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664</DialogTitle>
9477
9681
  <DialogDescription>\u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002\u78BA\u8A8D\u306E\u305F\u3081\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D "{slug}" \u3068\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002</DialogDescription>
9478
9682
  </DialogHeader>
9479
- <Stack gap="md">
9683
+ <Flex direction="col" gap="md">
9480
9684
  <Input value={confirm} onValueChange={(e) => setConfirm(e.target.value)} placeholder={slug} />
9481
- </Stack>
9685
+ </Flex>
9482
9686
  <DialogFooter>
9483
9687
  <Button variant="outline" onClick={() => onOpenChange(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
9484
9688
  <Button tone="destructive" disabled={confirm !== slug} onClick={() => { toast.success("\u524A\u9664\u3057\u307E\u3057\u305F"); onOpenChange(false); }}>\u5B8C\u5168\u306B\u524A\u9664</Button>
@@ -9518,14 +9722,14 @@ export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
9518
9722
  },
9519
9723
  {
9520
9724
  name: "inertia-list-page",
9521
- tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable + Badge + Pagination (current primitive API).",
9725
+ tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + Toolbar + DataTable + Badge + Pagination (current primitive API).",
9522
9726
  tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
9523
9727
  code: `import { Head, router } from "@inertiajs/react"
9524
9728
  import { useMemo, useState } from "react"
9525
- import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
9729
+ import { PageContainer, ResponsiveGrid, Flex } from "@godxjp/ui/layout"
9526
9730
  import { Card, CardContent, StatCard, DataTable, EmptyState, Badge, type ColumnDef } from "@godxjp/ui/data-display"
9527
9731
  import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
9528
- import { FilterBar, FilterGroup, Pagination } from "@godxjp/ui/navigation"
9732
+ import { Toolbar, ToolbarGroup, Pagination } from "@godxjp/ui/navigation"
9529
9733
  import { formatDate } from "@godxjp/ui/datetime"
9530
9734
  import { withCrmLayout } from "@/layouts/crm-layout" // see "inertia-persistent-layout"
9531
9735
 
@@ -9556,19 +9760,20 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
9556
9760
  return (
9557
9761
  <>
9558
9762
  <Head title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" />
9559
- {/* RULE: every page wraps in PageContainer; spacing via Stack/ResponsiveGrid, never p-*/gap-* */}
9763
+ {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction="col"/ResponsiveGrid, never p-*/gap-* */}
9560
9764
  <PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
9561
- <Stack gap="lg">
9765
+ <Flex direction="col" gap="lg">
9562
9766
  <ResponsiveGrid columns={3}>
9563
9767
  <StatCard label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
9564
9768
  <StatCard label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
9565
9769
  <StatCard label="\u4EF6\u6570" value={coupons.length} />
9566
9770
  </ResponsiveGrid>
9567
9771
 
9568
- <FilterBar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
9772
+ {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}
9773
+ <Toolbar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
9569
9774
  {/* SearchInput is value + onSearch(v) \u2014 NOT onChange */}
9570
9775
  <SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />
9571
- <FilterGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
9776
+ <ToolbarGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
9572
9777
  <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>
9573
9778
  <SelectTrigger><SelectValue /></SelectTrigger>
9574
9779
  <SelectContent>
@@ -9577,8 +9782,8 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
9577
9782
  <SelectItem value="\u4E0B\u66F8\u304D">\u4E0B\u66F8\u304D</SelectItem>
9578
9783
  </SelectContent>
9579
9784
  </Select>
9580
- </FilterGroup>
9581
- </FilterBar>
9785
+ </ToolbarGroup>
9786
+ </Toolbar>
9582
9787
 
9583
9788
  <Card>
9584
9789
  <CardContent flush>
@@ -9588,10 +9793,11 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
9588
9793
  </CardContent>
9589
9794
  </Card>
9590
9795
 
9796
+ {/* Pagination ONLY for real multi-page data \u2014 hide it while empty or single-page. */}
9591
9797
  {filtered.length > PAGE_SIZE && (
9592
9798
  <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />
9593
9799
  )}
9594
- </Stack>
9800
+ </Flex>
9595
9801
  </PageContainer>
9596
9802
  </>
9597
9803
  )
@@ -9605,7 +9811,7 @@ export default Coupons`
9605
9811
  tagline: "Inertia detail page \u2014 receives {id} prop, Descriptions (compound) + StatCard + EmptyState fallback.",
9606
9812
  tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
9607
9813
  code: `import { Head, router } from "@inertiajs/react"
9608
- import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
9814
+ import { PageContainer, ResponsiveGrid, Flex } from "@godxjp/ui/layout"
9609
9815
  import { Card, CardContent, StatCard, EmptyState, Descriptions, Badge } from "@godxjp/ui/data-display"
9610
9816
  import { Button } from "@godxjp/ui/general"
9611
9817
  import { formatDate } from "@godxjp/ui/datetime"
@@ -9633,7 +9839,7 @@ function MemberShow({ id }: { id: string }) {
9633
9839
  <>
9634
9840
  <Head title={member.name} />
9635
9841
  <PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
9636
- <Stack gap="lg">
9842
+ <Flex direction="col" gap="lg">
9637
9843
  <ResponsiveGrid columns={4}>
9638
9844
  <StatCard label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
9639
9845
  <StatCard label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
@@ -9651,7 +9857,7 @@ function MemberShow({ id }: { id: string }) {
9651
9857
  </Descriptions>
9652
9858
  </CardContent>
9653
9859
  </Card>
9654
- </Stack>
9860
+ </Flex>
9655
9861
  </PageContainer>
9656
9862
  </>
9657
9863
  )
@@ -9695,7 +9901,7 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
9695
9901
  },
9696
9902
  {
9697
9903
  name: "badge-coloring",
9698
- tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props, @godxjp/ui \u2265 6.1).",
9904
+ tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props).",
9699
9905
  tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
9700
9906
  code: `import { Badge } from "@godxjp/ui/data-display"
9701
9907
 
@@ -9704,7 +9910,7 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
9704
9910
  // scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
9705
9911
  // Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
9706
9912
 
9707
- // 1) Lifecycle with localized text \u2014 map to the key, keep JP via \`label\` (icon stays):
9913
+ // 1) Lifecycle with localized text \u2014 map to the key, keep JP via children (icon stays):
9708
9914
  <Badge status="active">\u516C\u958B\u4E2D</Badge> // green \u2713 \u516C\u958B\u4E2D
9709
9915
 
9710
9916
  // 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
@@ -9715,78 +9921,348 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
9715
9921
  <Badge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
9716
9922
  <Badge status="\u6CD5\u4EBA\u5171\u901A" tone="info" icon={null} />
9717
9923
 
9718
- // tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type BadgeTone)
9924
+ // tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type ToneProp)
9719
9925
  // RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
9720
9926
  // narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
9721
9927
  // import that instead of the raw Badge across pages.`
9722
9928
  },
9723
9929
  {
9724
9930
  name: "async-data-state",
9725
- tagline: "Mutually exclusive prerequisite, loading, data, empty, and cause-aware error states.",
9726
- tags: ["async", "loading", "query", "error", "empty"],
9727
- code: `import { DataState } from "@godxjp/ui/query";
9728
- import { EmptyState, SkeletonTable } from "@godxjp/ui/data-display";
9729
-
9730
- // enabled:false is prerequisite/idle, not loading \u2014 DataState checks fetchStatus, not just isPending.
9731
- // Errors are classified by cause: onAuthError handles 401/expired token (session renewal, NOT retry);
9732
- // Retry appears automatically only for transient/network/5xx; 403/404/422 show a cause-aware message.
9733
- <DataState query={query} prerequisite={<EmptyState variant="section" title="\u7D44\u7E54\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" />}
9734
- skeleton={<SkeletonTable />} empty={<EmptyState variant="section" title="\u7D50\u679C\u304C\u3042\u308A\u307E\u305B\u3093" />}
9735
- isEmpty={(data) => data.items.length === 0}
9736
- onAuthError={() => auth.signInAgain()}>
9737
- {(data) => <Results items={data.items} />}
9738
- </DataState>
9739
-
9740
- // Need a bespoke error UI? Pass errorRenderer and branch on classifyQueryError(error).category
9741
- // ("auth" | "forbidden" | "notFound" | "validation" | "transient" | "unknown").
9742
- // The default detail is a localized message \u2014 never raw token/endpoint/stack text.
9743
- // Never render pagination outside the successful populated-data branch.`
9931
+ aliases: ["loading-states", "data-state", "query-states"],
9932
+ tagline: "The full async state machine \u2014 ONE primary state at a time: prerequisite, disabled-vs-loading, stale refresh, populated, real-empty, and cause-aware error (401/403/404/422/transient) with correct recovery.",
9933
+ tags: ["async", "loading", "query", "error", "empty", "prerequisite", "retry", "react-query"],
9934
+ code: `import { DataState, classifyQueryError } from "@godxjp/ui/query";
9935
+ import { EmptyState } from "@godxjp/ui/data-display";
9936
+ import { SkeletonTable } from "@godxjp/ui/feedback";
9937
+ import { Building2, Inbox } from "lucide-react";
9938
+
9939
+ // ONE primary state renders at a time; DataState makes them mutually exclusive:
9940
+ // prerequisite \u2192 skeleton(loading) \u2192 data \u2192 empty \u2192 error (never two at once).
9941
+ //
9942
+ // enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is
9943
+ // disabled, but fetchStatus stays "idle" (no request in flight) \u2014 DataState renders the prerequisite
9944
+ // slot, so a disabled query shows an instruction, NOT an endless skeleton.
9945
+ // const query = useQuery({ queryKey: ["members", orgId], queryFn, enabled: Boolean(orgId) });
9946
+ //
9947
+ // A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and
9948
+ // announces the busy state politely \u2014 stale/placeholder refresh is handled for you.
9949
+ //
9950
+ // Errors are classified by CAUSE, not blanket-retried:
9951
+ // auth (401) \u2192 onAuthError: renew session / sign in again (NOT a retry)
9952
+ // forbidden (403) \u2192 permission message + access path (no retry)
9953
+ // notFound (404) \u2192 contextual not-found (no retry)
9954
+ // validation (400/422)\u2192 corrective guidance (no retry)
9955
+ // transient (408/429/5xx/network) \u2192 Retry offered automatically
9956
+ // unknown \u2192 neutral; opt into Retry via showRetry/onRetry only if it can help
9957
+ export function MembersPanel({ query, orgId, onSignIn }: {
9958
+ query: any; orgId?: string; onSignIn: () => void;
9959
+ }) {
9960
+ return (
9961
+ <DataState
9962
+ query={query}
9963
+ prerequisite={<EmptyState icon={Building2} variant="section" title="\u7D44\u7E54\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044"
9964
+ description="\u30E1\u30F3\u30D0\u30FC\u3092\u8868\u793A\u3059\u308B\u306B\u306F\u3001\u4E0A\u306E\u30BB\u30EC\u30AF\u30BF\u3067\u7D44\u7E54\u3092\u9078\u3073\u307E\u3059\u3002" />}
9965
+ skeleton={<SkeletonTable rows={8} columns={4} />}
9966
+ empty={<EmptyState icon={Inbox} variant="section" title="\u30E1\u30F3\u30D0\u30FC\u304C\u3044\u307E\u305B\u3093"
9967
+ description="\u3053\u306E\u7D44\u7E54\u306B\u306F\u307E\u3060\u30E1\u30F3\u30D0\u30FC\u304C\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002" />}
9968
+ isEmpty={(data) => data.items.length === 0}
9969
+ onAuthError={onSignIn}
9970
+ >
9971
+ {(data) => <MemberTable items={data.items} />}
9972
+ </DataState>
9973
+ );
9974
+ }
9975
+
9976
+ // Need a bespoke error surface? Pass errorRenderer and branch on the classified category \u2014 the
9977
+ // default detail is always a localized message (never raw token / endpoint / stack text):
9978
+ // errorRenderer={(error, retry) => {
9979
+ // const { category } = classifyQueryError(error);
9980
+ // if (category === "auth") return <SessionExpired onRenew={onSignIn} />;
9981
+ // if (category === "forbidden") return <NoAccess />;
9982
+ // if (category === "transient") return <Retryable onRetry={retry} />;
9983
+ // return <GenericError />;
9984
+ // }}
9985
+ // RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).`
9744
9986
  },
9745
9987
  {
9746
9988
  name: "data-table-page",
9747
- tagline: "Filter + table + single-row footer pagination, visible only for successful multi-page data.",
9748
- tags: ["table", "pagination", "filter", "async"],
9749
- code: `// FilterBar is standalone. Card owns one table surface; do not nest Card/Alert surfaces.
9750
- // Inside DataState's success branch:
9751
- <Card><CardContent flush><DataTable data={data.items} columns={columns} />
9752
- {data.totalPages > 1 && <Flex justify="between" align="center"><span>{range}</span><Pagination /></Flex>}
9753
- </CardContent></Card>
9754
- // Hide pagination for prerequisite/loading/error/empty and one-page results.
9755
- // Preserve filters/page during a transient retry; stack only at narrow mobile widths.`
9989
+ aliases: ["filter-bar", "table-state", "list-page"],
9990
+ tagline: "Filter (Toolbar) + table surface + bottom pagination driven by a query state machine \u2014 pagination shows ONLY for successful multi-page data; filters/page survive a transient retry.",
9991
+ tags: ["table", "pagination", "filter", "async", "toolbar", "datatable"],
9992
+ code: `import { useState } from "react";
9993
+ import { DataState } from "@godxjp/ui/query";
9994
+ import { Card, CardContent, DataTable, EmptyState, type ColumnDef } from "@godxjp/ui/data-display";
9995
+ import { SkeletonTable } from "@godxjp/ui/feedback";
9996
+ import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry";
9997
+ import { Toolbar, ToolbarGroup, Pagination } from "@godxjp/ui/navigation";
9998
+ import { Flex } from "@godxjp/ui/layout";
9999
+ import { useQuery, keepPreviousData } from "@tanstack/react-query";
10000
+
10001
+ type Row = { id: string; name: string; status: string };
10002
+
10003
+ export function MembersTablePage({ orgId }: { orgId?: string }) {
10004
+ const [q, setQ] = useState("");
10005
+ const [status, setStatus] = useState("all");
10006
+ const [page, setPage] = useState(1);
10007
+ const pageSize = 20;
10008
+
10009
+ // placeholderData: keepPreviousData \u2192 on page/filter change the PREVIOUS page stays visible
10010
+ // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).
10011
+ const query = useQuery({
10012
+ queryKey: ["members", orgId, q, status, page],
10013
+ queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),
10014
+ enabled: Boolean(orgId),
10015
+ placeholderData: keepPreviousData,
10016
+ });
10017
+
10018
+ const columns: ColumnDef<Row>[] = [
10019
+ { key: "name", header: "\u6C0F\u540D", render: (r) => <span className="font-medium">{r.name}</span> },
10020
+ { key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9" },
10021
+ ];
10022
+
10023
+ return (
10024
+ <Flex direction="col" gap="lg">
10025
+ {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even
10026
+ while a query is idle/loading/empty. Changing a filter resets to page 1. */}
10027
+ <Toolbar hasActiveFilters={q !== "" || status !== "all"}
10028
+ onClear={() => { setQ(""); setStatus("all"); setPage(1); }}>
10029
+ <SearchInput placeholder="\u6C0F\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1); }} />
10030
+ <ToolbarGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
10031
+ <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1); }}>
10032
+ <SelectTrigger><SelectValue /></SelectTrigger>
10033
+ <SelectContent>
10034
+ <SelectItem value="all">\u3059\u3079\u3066</SelectItem>
10035
+ <SelectItem value="active">\u6709\u52B9</SelectItem>
10036
+ <SelectItem value="invited">\u62DB\u5F85\u4E2D</SelectItem>
10037
+ </SelectContent>
10038
+ </Select>
10039
+ </ToolbarGroup>
10040
+ </Toolbar>
10041
+
10042
+ {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it
10043
+ is hidden during prerequisite / loading / empty / error and for single-page results. */}
10044
+ <DataState
10045
+ query={query}
10046
+ prerequisite={<EmptyState variant="section" title="\u7D44\u7E54\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" />}
10047
+ skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}
10048
+ empty={<EmptyState variant="section" title="\u8A72\u5F53\u3059\u308B\u30E1\u30F3\u30D0\u30FC\u304C\u3044\u307E\u305B\u3093" description="\u691C\u7D22\u6761\u4EF6\u3092\u5909\u66F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002" />}
10049
+ isEmpty={(d) => d.items.length === 0}
10050
+ >
10051
+ {(d) => {
10052
+ const totalPages = Math.ceil(d.total / pageSize);
10053
+ return (
10054
+ <>
10055
+ <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>
10056
+ {totalPages > 1 && (
10057
+ <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />
10058
+ )}
10059
+ </>
10060
+ );
10061
+ }}
10062
+ </DataState>
10063
+ </Flex>
10064
+ );
10065
+ }
10066
+ // Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.
10067
+ // A transient retry keeps q/status/page (they live in component state, not the query) \u2014 the
10068
+ // same page reloads in place; the user never loses their filters.`
9756
10069
  },
9757
10070
  {
9758
10071
  name: "organization-memberships",
9759
- tagline: "Workspace identity, current state, role and permission-aware actions; invitations are conditional.",
9760
- tags: ["organization", "workspace", "membership", "invitation", "account"],
9761
- code: `// Research basis: GitHub organizations and Slack workspace switching/invitations.
9762
- // Each row: logo + recognizable name + Current badge + role + Open/Switch/Manage/Leave action.
9763
- // Put Create organization at page level. Do not show raw membership timestamps without labels.
9764
- // No pending invitations \u2192 omit the section entirely.
9765
- // Few pending \u2192 compact actionable list; many/history \u2192 a focused route.
9766
- <MembershipList memberships={memberships} currentId={currentId} />
9767
- {pendingInvitations.length > 0 && <PendingInvitations variant="compact" items={pendingInvitations} />}`
10072
+ aliases: ["organization-switcher", "workspace-switcher", "received-invitations", "memberships"],
10073
+ tagline: "Workspace membership list \u2014 recognizable identity, current/active state, role, permission-aware Open/Switch/Manage/Leave row actions, page-level Create, and CONDITIONAL invitations (omit when none).",
10074
+ tags: ["organization", "workspace", "membership", "invitation", "account", "switcher"],
10075
+ code: `// Research basis: GitHub organization membership + Slack workspace switching/invitations.
10076
+ // Each row = recognizable identity (Avatar/logo, NOT an identical decorative icon) + name +
10077
+ // a "\u73FE\u5728" (Current) badge for the active org + role + a permission-aware action menu.
10078
+ import { Card, CardContent, CardHeader, CardTitle, ListRow, Badge, Avatar, AvatarImage, AvatarFallback, EmptyState } from "@godxjp/ui/data-display";
10079
+ import { Button } from "@godxjp/ui/general";
10080
+ import { Flex } from "@godxjp/ui/layout";
10081
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@godxjp/ui/navigation";
10082
+ import { MoreHorizontal, Plus } from "lucide-react";
10083
+
10084
+ type Role = "owner" | "admin" | "member";
10085
+ type Membership = { id: string; name: string; logoUrl?: string; role: Role; joinedAt: string; isCurrent: boolean; canManage: boolean };
10086
+ type Invite = { id: string; orgName: string; invitedBy: string };
10087
+
10088
+ const ROLE_LABEL: Record<Role, string> = { owner: "\u30AA\u30FC\u30CA\u30FC", admin: "\u7BA1\u7406\u8005", member: "\u30E1\u30F3\u30D0\u30FC" };
10089
+
10090
+ export function OrganizationMemberships({
10091
+ memberships, invitations, onOpen, onSwitch, onManage, onLeave, onCreate, onAccept, onDecline,
10092
+ }: {
10093
+ memberships: Membership[]; invitations: Invite[];
10094
+ onOpen: (id: string) => void; onSwitch: (id: string) => void; onManage: (id: string) => void;
10095
+ onLeave: (id: string) => void; onCreate: () => void; onAccept: (id: string) => void; onDecline: (id: string) => void;
10096
+ }) {
10097
+ return (
10098
+ <Flex direction="col" gap="lg">
10099
+ <Card>
10100
+ <CardHeader>
10101
+ <Flex justify="between" align="center">
10102
+ <CardTitle>\u7D44\u7E54</CardTitle>
10103
+ {/* Create belongs at page/section level, not per row. */}
10104
+ <Button variant="outline" size="sm" onClick={onCreate}><Plus className="size-4" />\u7D44\u7E54\u3092\u4F5C\u6210</Button>
10105
+ </Flex>
10106
+ </CardHeader>
10107
+ {/* flush \u2192 ListRows draw their own edge-to-edge dividers. */}
10108
+ <CardContent flush>
10109
+ {memberships.length === 0 ? (
10110
+ <EmptyState variant="section" title="\u6240\u5C5E\u3057\u3066\u3044\u308B\u7D44\u7E54\u304C\u3042\u308A\u307E\u305B\u3093" description="\u65B0\u3057\u3044\u7D44\u7E54\u3092\u4F5C\u6210\u3059\u308B\u304B\u3001\u62DB\u5F85\u3092\u53D7\u3051\u53D6\u3063\u3066\u304F\u3060\u3055\u3044\u3002" />
10111
+ ) : (
10112
+ memberships.map((m) => (
10113
+ <ListRow
10114
+ key={m.id}
10115
+ leading={
10116
+ <Avatar>
10117
+ {m.logoUrl ? <AvatarImage src={m.logoUrl} alt="" /> : null}
10118
+ <AvatarFallback>{m.name.slice(0, 1)}</AvatarFallback>
10119
+ </Avatar>
10120
+ }
10121
+ title={
10122
+ <Flex gap="sm" align="center">
10123
+ <span className="font-medium">{m.name}</span>
10124
+ {m.isCurrent && <Badge tone="success" icon={null}>\u73FE\u5728</Badge>}
10125
+ </Flex>
10126
+ }
10127
+ description={ROLE_LABEL[m.role]}
10128
+ trailing={
10129
+ <Flex gap="sm" align="center">
10130
+ {m.isCurrent
10131
+ ? <Button size="sm" onClick={() => onOpen(m.id)}>\u958B\u304F</Button>
10132
+ : <Button variant="outline" size="sm" onClick={() => onSwitch(m.id)}>\u5207\u308A\u66FF\u3048</Button>}
10133
+ <DropdownMenu>
10134
+ <DropdownMenuTrigger asChild>
10135
+ <Button variant="ghost" size="icon" aria-label={\`\${m.name} \u306E\u64CD\u4F5C\`}><MoreHorizontal className="size-4" /></Button>
10136
+ </DropdownMenuTrigger>
10137
+ <DropdownMenuContent align="end">
10138
+ <DropdownMenuItem onSelect={() => onOpen(m.id)}>\u958B\u304F</DropdownMenuItem>
10139
+ {m.canManage && <DropdownMenuItem onSelect={() => onManage(m.id)}>\u7BA1\u7406</DropdownMenuItem>}
10140
+ {m.role !== "owner" && <DropdownMenuItem onSelect={() => onLeave(m.id)}>\u9000\u51FA</DropdownMenuItem>}
10141
+ </DropdownMenuContent>
10142
+ </DropdownMenu>
10143
+ </Flex>
10144
+ }
10145
+ />
10146
+ ))
10147
+ )}
10148
+ </CardContent>
10149
+ </Card>
10150
+
10151
+ {/* CONDITIONAL invitations \u2014 no pending invites \u21D2 render NOTHING (no permanent empty Card).
10152
+ Few pending \u21D2 a compact actionable list. Many/history \u21D2 link to a focused route instead. */}
10153
+ {invitations.length > 0 && (
10154
+ <Card>
10155
+ <CardHeader><CardTitle>\u4FDD\u7559\u4E2D\u306E\u62DB\u5F85</CardTitle></CardHeader>
10156
+ <CardContent flush>
10157
+ {invitations.map((inv) => (
10158
+ <ListRow
10159
+ key={inv.id}
10160
+ title={<span className="font-medium">{inv.orgName}</span>}
10161
+ description={\`\${inv.invitedBy} \u304B\u3089\u306E\u62DB\u5F85\`}
10162
+ trailing={
10163
+ <Flex gap="sm">
10164
+ <Button size="sm" onClick={() => onAccept(inv.id)}>\u53C2\u52A0</Button>
10165
+ <Button variant="ghost" size="sm" onClick={() => onDecline(inv.id)}>\u8F9E\u9000</Button>
10166
+ </Flex>
10167
+ }
10168
+ />
10169
+ ))}
10170
+ </CardContent>
10171
+ </Card>
10172
+ )}
10173
+ </Flex>
10174
+ );
10175
+ }
10176
+ // DON'T: passive rows with no action \xB7 identical decorative icons \xB7 unlabeled timestamps
10177
+ // (label them: "2024\u5E74\u306B\u53C2\u52A0") \xB7 a large empty invitation Card shown permanently.`
9768
10178
  },
9769
10179
  {
9770
10180
  name: "account-recovery-settings",
9771
- tagline: "Signed-in recovery method status rows, separate from password change and signed-out recovery.",
9772
- tags: ["account", "recovery", "security", "backup-codes", "password"],
9773
- code: `// Research basis: Google Account recovery, Microsoft Security info, GitHub recovery codes.
9774
- // Use compact method rows: method + verified/available status + Add/Change/Regenerate action.
9775
- // Do not stack generic Info Alerts for ordinary capability status.
9776
- <RecoveryMethodRow method="email" value={maskedEmail} status="verified" action={<Button>\u5909\u66F4</Button>} />
9777
- {backupCodesSupported && <RecoveryMethodRow method="backup-codes" status={codeStatus} action={<Button>\u518D\u751F\u6210</Button>} />}
9778
- // Password change is a separate destination. Forgot-password is a signed-out journey.`
10181
+ aliases: ["recovery-email", "backup-codes", "security-settings", "recovery-settings"],
10182
+ tagline: "Signed-in recovery: compact method status/action rows (email \xB7 phone \xB7 backup codes) + a separate password-change destination \u2014 capability-aware, risk-appropriate, NO stacked Info Alerts.",
10183
+ tags: ["account", "recovery", "security", "backup-codes", "password", "settings"],
10184
+ code: `// Research basis: Google Account recovery (email/phone + backup codes), Microsoft Security info,
10185
+ // GitHub recovery codes. Signed-IN recovery SETTINGS (manage your methods) is a DIFFERENT surface
10186
+ // from the signed-OUT forgot-password JOURNEY \u2014 do not mix them on one page.
10187
+ import { Card, CardContent, CardHeader, CardTitle, ListRow, Badge } from "@godxjp/ui/data-display";
10188
+ import { Button } from "@godxjp/ui/general";
10189
+ import { Flex } from "@godxjp/ui/layout";
10190
+ import { Mail, Smartphone, KeyRound } from "lucide-react";
10191
+
10192
+ type MethodStatus = "verified" | "unverified" | "unavailable";
10193
+
10194
+ const STATUS: Record<MethodStatus, { tone: "success" | "warning" | "neutral"; label: string }> = {
10195
+ verified: { tone: "success", label: "\u78BA\u8A8D\u6E08\u307F" },
10196
+ unverified: { tone: "warning", label: "\u672A\u78BA\u8A8D" },
10197
+ unavailable: { tone: "neutral", label: "\u672A\u8A2D\u5B9A" },
10198
+ };
10199
+
10200
+ function MethodRow({ icon: Icon, name, value, status, action }: {
10201
+ icon: React.ComponentType<{ className?: string }>; name: string; value?: string;
10202
+ status: MethodStatus; action: React.ReactNode;
10203
+ }) {
10204
+ const s = STATUS[status];
10205
+ return (
10206
+ <ListRow
10207
+ leading={<Icon className="text-muted-foreground size-5" />}
10208
+ title={<span className="font-medium">{name}</span>}
10209
+ description={value ?? "\u672A\u8A2D\u5B9A"}
10210
+ trailing={<Flex gap="sm" align="center"><Badge tone={s.tone} icon={null}>{s.label}</Badge>{action}</Flex>}
10211
+ />
10212
+ );
10213
+ }
10214
+
10215
+ // Compact status/action rows \u2014 one row per method, capability-aware. Do NOT stack full-width
10216
+ // Info Alerts for ordinary status, and do NOT show unavailable features as permanent page content
10217
+ // beyond a single row that offers to set them up.
10218
+ export function AccountRecoverySettings({ email, phone, backupCodesSupported, backupCodesRemaining, onChangeEmail, onChangePhone, onRegenerateCodes, onChangePassword }: {
10219
+ email?: string; phone?: string; backupCodesSupported: boolean; backupCodesRemaining: number;
10220
+ onChangeEmail: () => void; onChangePhone: () => void; onRegenerateCodes: () => void; onChangePassword: () => void;
10221
+ }) {
10222
+ return (
10223
+ <Flex direction="col" gap="lg">
10224
+ <Card>
10225
+ <CardHeader><CardTitle>\u30A2\u30AB\u30A6\u30F3\u30C8\u5FA9\u65E7</CardTitle></CardHeader>
10226
+ <CardContent flush>
10227
+ <MethodRow icon={Mail} name="\u5FA9\u65E7\u7528\u30E1\u30FC\u30EB" value={email} status={email ? "verified" : "unavailable"}
10228
+ action={<Button variant="outline" size="sm" onClick={onChangeEmail}>{email ? "\u5909\u66F4" : "\u8FFD\u52A0"}</Button>} />
10229
+ <MethodRow icon={Smartphone} name="\u5FA9\u65E7\u7528\u96FB\u8A71\u756A\u53F7" value={phone} status={phone ? "verified" : "unavailable"}
10230
+ action={<Button variant="outline" size="sm" onClick={onChangePhone}>{phone ? "\u5909\u66F4" : "\u8FFD\u52A0"}</Button>} />
10231
+ {/* Capability-aware: only render backup codes when the account type supports them. */}
10232
+ {backupCodesSupported && (
10233
+ <MethodRow icon={KeyRound} name="\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u30B3\u30FC\u30C9"
10234
+ value={\`\u6B8B\u308A \${backupCodesRemaining} \u500B\`} status={backupCodesRemaining > 0 ? "verified" : "unverified"}
10235
+ action={<Button variant="outline" size="sm" onClick={onRegenerateCodes}>\u518D\u751F\u6210</Button>} />
10236
+ )}
10237
+ </CardContent>
10238
+ </Card>
10239
+
10240
+ {/* Password change is its OWN destination (risk-appropriate confirmation lives there). */}
10241
+ <Card>
10242
+ <CardHeader><CardTitle>\u30D1\u30B9\u30EF\u30FC\u30C9</CardTitle></CardHeader>
10243
+ <CardContent>
10244
+ <Flex justify="between" align="center">
10245
+ <span className="text-muted-foreground text-sm">\u6700\u7D42\u66F4\u65B0: 90\u65E5\u524D</span>
10246
+ <Button variant="outline" onClick={onChangePassword}>\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u5909\u66F4</Button>
10247
+ </Flex>
10248
+ </CardContent>
10249
+ </Card>
10250
+ </Flex>
10251
+ );
10252
+ }
10253
+ // The signed-OUT recovery journey (forgot password \u2192 email link \u2192 reset) is a separate flow on the
10254
+ // auth screens (AuthShell), NOT part of these signed-in settings. Do not surface it here.`
9779
10255
  }
9780
10256
  ];
9781
10257
  function findPattern(name) {
9782
10258
  const slug = name.trim().toLowerCase();
9783
- return PATTERNS.find((p) => p.name === slug);
10259
+ return PATTERNS.find((p) => p.name === slug || p.aliases?.includes(slug));
9784
10260
  }
9785
10261
  function searchPatterns(query) {
9786
10262
  const q = query.trim().toLowerCase();
9787
10263
  if (q === "") return PATTERNS;
9788
10264
  return PATTERNS.filter(
9789
- (p) => p.name.includes(q) || p.tagline.toLowerCase().includes(q) || p.tags.some((t) => t.includes(q))
10265
+ (p) => p.name.includes(q) || p.aliases?.some((a) => a.includes(q)) || p.tagline.toLowerCase().includes(q) || p.tags.some((t) => t.includes(q))
9790
10266
  );
9791
10267
  }
9792
10268
 
@@ -10482,8 +10958,8 @@ rule (get_rule) involved, with a minimal repro, expected vs actual, version, and
10482
10958
  id: "design-to-page",
10483
10959
  audience: "consumer",
10484
10960
  name: "Design handoff \u2192 real page (consumer build guide)",
10485
- whenToUse: "You (a consumer agent) received a Claude Design handoff \u2014 a bundle/mock/screenshot/HTML prototype or a written brief \u2014 and must build it as a REAL page with @godxjp/ui. Read this BEFORE writing any JSX. It teaches: read intent, map every block to a real primitive via this MCP, consume existing tokens, apply the dxs-kintai DNA, treat tables as the centerpiece, resolve gaps by extend-or-ask, and verify.",
10486
- source: "@godxjp/ui .design/research (chats-intent, tables, atomic-components) + dxs-kintai SKILL/colors_and_type.css",
10961
+ whenToUse: "You (a consumer agent) received a Claude Design handoff \u2014 a bundle/mock/screenshot/HTML prototype or a written brief \u2014 and must build it as a REAL page with @godxjp/ui. Read this BEFORE writing any JSX. It teaches: read intent, map every block to a real primitive via this MCP, consume existing tokens, apply the reference-design DNA, treat tables as the centerpiece, resolve gaps by extend-or-ask, and verify.",
10962
+ source: "@godxjp/ui .design/research (chats-intent, tables, atomic-components) + reference-design SKILL/colors_and_type.css",
10487
10963
  sections: [
10488
10964
  {
10489
10965
  id: "read-intent",
@@ -10532,7 +11008,7 @@ never a literal px. Radii: card 6px, control 4px, inner pill 2px.`
10532
11008
  },
10533
11009
  {
10534
11010
  id: "dna",
10535
- title: "Apply the dxs-kintai DNA",
11011
+ title: "Apply the reference-design DNA",
10536
11012
  tagline: "\u6E0B\u307F / \u9593 / \u7C21\u7D20 \u2014 fixed color signaling, dense, small headings, 14/1.7, no emoji.",
10537
11013
  body: `These rules survive when you drop the prototype's divs:
10538
11014
  \u2022 \u6E0B\u307F (restraint): primary chroma \u2264 0.18 \u2014 --primary is the single most-important
@@ -10638,7 +11114,7 @@ just buries a library bug inside every app). Instead:
10638
11114
  audience: "consumer",
10639
11115
  name: "Compose a screen \u2014 primitives \u2192 a finished app view (consumer)",
10640
11116
  whenToUse: "You (a consumer agent) are building a NEW screen/page in an app that imports @godxjp/ui \u2014 from a written brief or product requirement, not a design handoff. Read this to assemble it from real primitives via this MCP: pick the right components, lay out one-intent-per-screen, wire every state + a11y + i18n, and verify. For a Claude Design handoff bundle/mock specifically, use design-to-page instead.",
10641
- source: "@godxjp/ui MCP (consumer surface) \u2014 taste/one-intent + component-discipline + dxs-kintai DNA",
11117
+ source: "@godxjp/ui MCP (consumer surface) \u2014 taste/one-intent + component-discipline + reference-design DNA",
10642
11118
  sections: [
10643
11119
  {
10644
11120
  id: "pick-primitives",
@@ -11037,7 +11513,7 @@ function routeTask(task, opts) {
11037
11513
  ],
11038
11514
  "design-to-page",
11039
11515
  "map-to-primitives",
11040
- "Map every block to a real @godxjp/ui primitive (MCP-first), consume existing tokens, apply the dxs-kintai DNA, tables central, gaps \u2192 extend-or-ask, verify.",
11516
+ "Map every block to a real @godxjp/ui primitive (MCP-first), consume existing tokens, apply the reference-design DNA, tables central, gaps \u2192 extend-or-ask, verify.",
11041
11517
  ["design-to-page/read-intent", "design-to-page/dna", "design-to-page/tables-central"]
11042
11518
  );
11043
11519
  route(
@@ -12004,12 +12480,12 @@ var AUDIT_RULES = [
12004
12480
  standard: "W3C CSS Logical Properties L1 \xB7 WCAG 2.2 (1.3.2)",
12005
12481
  fix: "Use logical utilities (ms-/me-/ps-/pe-, start-/end-, text-start/end, border-s/e, rounded-s/e)."
12006
12482
  },
12007
- // ── copy (dxs-kintai DNA) ────────────────────────────────────────────────
12483
+ // ── copy (reference-design DNA) ──────────────────────────────────────────
12008
12484
  {
12009
12485
  id: "no-em-dash-in-copy",
12010
12486
  severity: "warn",
12011
12487
  category: "copy",
12012
- standard: "@godxjp/ui dxs-kintai typography",
12488
+ standard: "@godxjp/ui reference-design typography",
12013
12489
  fix: "No em-dash (\u2014) in copy; use a middot \xB7 or two calm sentences."
12014
12490
  }
12015
12491
  ];
@@ -12038,7 +12514,7 @@ var VISUAL_RULES = [
12038
12514
  id: "oversaturated-accent",
12039
12515
  severity: "warn",
12040
12516
  category: "color",
12041
- standard: "@godxjp/ui dxs-kintai \u6E0B\u307F (OKLCH chroma \u2264 0.18)",
12517
+ standard: "@godxjp/ui reference-design \u6E0B\u307F (OKLCH chroma \u2264 0.18)",
12042
12518
  fix: "Desaturate brand/primary surfaces (OKLCH chroma \u2264 0.18); read --primary tokens, no raw vivid bars."
12043
12519
  },
12044
12520
  {
@@ -12063,8 +12539,8 @@ function visualRulesByCategory(category) {
12063
12539
  // package.json
12064
12540
  var package_default = {
12065
12541
  name: "@godxjp/ui-mcp",
12066
- version: "17.0.0",
12067
- godxUiCompatibility: "17.0.x",
12542
+ version: "18.0.0",
12543
+ godxUiCompatibility: "18.0.x",
12068
12544
  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).",
12069
12545
  type: "module",
12070
12546
  main: "./dist/index.js",
@@ -12152,7 +12628,7 @@ var TOOL_DEFINITIONS = [
12152
12628
  },
12153
12629
  {
12154
12630
  name: "list_patterns",
12155
- description: "List every canonical code pattern (registration-form / settings-page / data-table / confirm-destructive / app-shell / filter-bar / loading-states). ~500 bytes. Use before `get_pattern`.",
12631
+ description: "List every canonical code pattern (signup-form / settings-page-responsive / data-table-page / async-data-state / organization-memberships / account-recovery-settings / confirm-destructive \u2026). Common aliases resolve too (loading-states\u2192async-data-state, filter-bar\u2192data-table-page, settings-tabs\u2192settings-page-responsive). ~500 bytes. Use before `get_pattern`.",
12156
12632
  inputSchema: { type: "object", properties: {} }
12157
12633
  },
12158
12634
  {
@@ -13162,7 +13638,12 @@ function suggestPrimitive(useCase) {
13162
13638
  "Select=discrete options, AutoComplete=free-text+suggestions.",
13163
13639
  3
13164
13640
  );
13165
- check(["filter"], "Form layout='inline' + pattern 'filter-bar'", "Inline form above table.", 4);
13641
+ check(
13642
+ ["filter"],
13643
+ "Toolbar/ToolbarGroup + pattern 'filter-bar' (\u2192 data-table-page)",
13644
+ "Standalone Toolbar filter bar above a table; see the data-table-page pattern.",
13645
+ 4
13646
+ );
13166
13647
  check(
13167
13648
  ["delete", "destructive"],
13168
13649
  "Pattern 'confirm-destructive'",