@godxjp/ui-mcp 17.0.1 → 18.0.1

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.",
@@ -1850,7 +1905,12 @@ import { Smartphone } from "lucide-react";
1850
1905
  required: true,
1851
1906
  description: "TableHeader / TableBody composition."
1852
1907
  },
1853
- { name: "className", type: "string", description: "Extra classes on the table element." }
1908
+ { name: "className", type: "string", description: "Extra classes on the table element." },
1909
+ {
1910
+ name: "scrollable",
1911
+ type: "boolean",
1912
+ description: "Whether Table owns its own horizontal-scroll region (default true). Leave true for a standalone table so a table wider than its container scrolls in a keyboard-reachable wrapper. Set false only when an ancestor already provides the scroll region (DataTable does) to avoid a redundant nested scroller + duplicate keyboard tab stop."
1913
+ }
1854
1914
  ],
1855
1915
  usage: [
1856
1916
  "DO compose all six sub-parts in order: wrap with `<Table>`, then `<TableHeader>` containing `<TableRow><TableHead>\u2026</TableRow>`, then `<TableBody>` containing one or more `<TableRow><TableCell>\u2026` rows. Skipping any layer (e.g. bare `<th>` inside `<Table>`) bypasses the design tokens and hover/border styles.",
@@ -2084,7 +2144,7 @@ import { Smartphone } from "lucide-react";
2084
2144
  {
2085
2145
  name: "FormField",
2086
2146
  group: "data-entry",
2087
- 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.",
2147
+ 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.",
2088
2148
  props: [
2089
2149
  {
2090
2150
  name: "id",
@@ -2144,7 +2204,10 @@ import { Smartphone } from "lucide-react";
2144
2204
  "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.",
2145
2205
  "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.",
2146
2206
  "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.",
2147
- "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."
2207
+ "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.",
2208
+ "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.",
2209
+ "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.",
2210
+ "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`."
2148
2211
  ],
2149
2212
  useCases: [
2150
2213
  "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.",
@@ -2517,6 +2580,52 @@ import { Smartphone } from "lucide-react";
2517
2580
  description: "Label for the clear row (data-driven combobox mode)."
2518
2581
  },
2519
2582
  { name: "disabled", type: "boolean", description: "Disables the entire select." },
2583
+ {
2584
+ name: "readOnly",
2585
+ type: "boolean",
2586
+ defaultValue: "false",
2587
+ 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."
2588
+ },
2589
+ {
2590
+ name: "size",
2591
+ type: '"xs" | "sm" | "md" | "lg"',
2592
+ description: "Searchable mode only. Height tier forwarded to the SearchSelect trigger Button. For the compound API use SelectTrigger's own size prop instead (below)."
2593
+ },
2594
+ {
2595
+ name: "open",
2596
+ type: "boolean",
2597
+ description: "Searchable mode only. Controlled popover open state (uncontrolled by default). Pair with onOpenChange."
2598
+ },
2599
+ {
2600
+ name: "onOpenChange",
2601
+ type: "(open: boolean) => void",
2602
+ 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."
2603
+ },
2604
+ {
2605
+ name: "search",
2606
+ type: "string",
2607
+ description: "Searchable mode only. Controlled search-box query (uncontrolled by default). Pair with onSearchChange."
2608
+ },
2609
+ {
2610
+ name: "onSearchChange",
2611
+ type: "(query: string) => void",
2612
+ description: "Searchable mode only. Fires on every keystroke in the search box."
2613
+ },
2614
+ {
2615
+ name: "filterOption",
2616
+ type: "(option: SearchSelectOptionProp, query: string) => boolean",
2617
+ 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."
2618
+ },
2619
+ {
2620
+ name: "renderError",
2621
+ type: "(params: { message: string; retry: () => void }) => React.ReactNode",
2622
+ description: "Searchable mode only. Custom error slot, overriding the default errorMessage row. retry() reloads from the first page."
2623
+ },
2624
+ {
2625
+ name: "renderLoadMore",
2626
+ type: "(params: { hasMore: boolean; loading: boolean; loadMore: () => void }) => React.ReactNode",
2627
+ 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."
2628
+ },
2520
2629
  {
2521
2630
  name: "name",
2522
2631
  type: "string",
@@ -2542,6 +2651,12 @@ import { Smartphone } from "lucide-react";
2542
2651
  type: '"sm" | "md"',
2543
2652
  defaultValue: '"md"',
2544
2653
  description: "Compound API only. Size variant on the SelectTrigger sub-component."
2654
+ },
2655
+ {
2656
+ name: "SelectTrigger showIndicator",
2657
+ type: "boolean",
2658
+ defaultValue: "true",
2659
+ 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."
2545
2660
  }
2546
2661
  ],
2547
2662
  usage: [
@@ -2552,7 +2667,12 @@ import { Smartphone } from "lucide-react";
2552
2667
  "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.",
2553
2668
  "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.",
2554
2669
  "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.",
2555
- "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."
2670
+ "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.",
2671
+ "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.",
2672
+ "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.",
2673
+ "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).",
2674
+ "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.",
2675
+ "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."
2556
2676
  ],
2557
2677
  useCases: [
2558
2678
  "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.",
@@ -3579,10 +3699,14 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
3579
3699
  {
3580
3700
  name: "items",
3581
3701
  type: "{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean }[]",
3582
- description: "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels."
3702
+ 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)."
3583
3703
  },
3584
3704
  { name: "value", type: "string", description: "Controlled active tab key." },
3585
- { name: "defaultValue", type: "string", description: "Uncontrolled initial tab key." },
3705
+ {
3706
+ name: "defaultValue",
3707
+ type: "string",
3708
+ description: "Uncontrolled initial tab key. Ignored (falls back to the first enabled item) when it names a disabled item or an unknown key."
3709
+ },
3586
3710
  {
3587
3711
  name: "onValueChange",
3588
3712
  type: "(value: string) => void",
@@ -3595,7 +3719,9 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
3595
3719
  "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.",
3596
3720
  "DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",
3597
3721
  '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.',
3598
- "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."
3722
+ "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.",
3723
+ "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).",
3724
+ "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`."
3599
3725
  ],
3600
3726
  useCases: [
3601
3727
  "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.",
@@ -3653,10 +3779,33 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
3653
3779
  name: "showSizeChanger",
3654
3780
  type: "boolean",
3655
3781
  description: "Show the page-size selector beside the pager."
3782
+ },
3783
+ {
3784
+ name: "hideOnSinglePage",
3785
+ type: "boolean",
3786
+ defaultValue: "true",
3787
+ 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."
3788
+ },
3789
+ {
3790
+ name: "simple",
3791
+ type: "boolean",
3792
+ description: "Compact form for narrow contexts \u2014 Prev / n\xB7N / Next, no page-number buttons. The intentional mobile transformation (desktop never wraps)."
3793
+ },
3794
+ {
3795
+ name: "disabled",
3796
+ type: "boolean",
3797
+ description: "Disable all navigation controls."
3798
+ },
3799
+ {
3800
+ name: "aria-label",
3801
+ type: "string",
3802
+ 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.`
3656
3803
  }
3657
3804
  ],
3658
3805
  usage: [
3659
3806
  "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.",
3807
+ "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.",
3808
+ "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.",
3660
3809
  "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.",
3661
3810
  "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.",
3662
3811
  "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.",
@@ -7245,7 +7394,7 @@ export default function PasswordBlock() {
7245
7394
  {
7246
7395
  name: "appearance",
7247
7396
  type: '"labeled" | "icon"',
7248
- 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.`
7397
+ 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.'
7249
7398
  },
7250
7399
  {
7251
7400
  name: "className",
@@ -7830,8 +7979,8 @@ var PROP_VOCABULARY = [
7830
7979
  name: "GapProp",
7831
7980
  concept: "Shared layout gap scale.",
7832
7981
  values: ["xs", "sm", "md", "lg", "xl"],
7833
- usedBy: ["Stack", "Inline"],
7834
- notes: "Inline uses an Exclude<GapProp, 'xl'> subset."
7982
+ usedBy: ["Flex"],
7983
+ 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).'
7835
7984
  },
7836
7985
  {
7837
7986
  name: "TitleProp",
@@ -9260,7 +9409,7 @@ var CARDINAL_RULES = [
9260
9409
  {
9261
9410
  number: 40,
9262
9411
  title: "Pages are mobile-first",
9263
- 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."
9412
+ 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.'
9264
9413
  },
9265
9414
  {
9266
9415
  number: 41,
@@ -9317,48 +9466,46 @@ var PATTERNS = [
9317
9466
  // 0) \u2605 MOST COMMON: <Card> body has NO padding (content is flush against the edges)
9318
9467
  // Cause: the bare <Card> has ZERO inner padding \u2014 it MUST contain <CardContent>.
9319
9468
  // Don't hand-roll padding with className="p-4" on the Card either.
9320
- // \u274C <Card><Stack gap="md">\u2026fields\u2026</Stack></Card> // flush, no padding
9321
- // \u274C <Card className="p-4">\u2026fields\u2026</Card> // hand-rolled padding
9322
- // \u2705 <Card><CardContent><Stack gap="md">\u2026fields\u2026</Stack></CardContent></Card>
9469
+ // \u274C <Card><Flex direction="col" gap="md">\u2026fields\u2026</Flex></Card> // flush, no padding
9470
+ // \u274C <Card className="p-4">\u2026fields\u2026</Card> // hand-rolled padding
9471
+ // \u2705 <Card><CardContent><Flex direction="col" gap="md">\u2026fields\u2026</Flex></CardContent></Card>
9323
9472
  // Titles \u2192 <CardHeader><CardTitle>. Only go flush deliberately for a full-bleed table:
9324
9473
  // \u2705 <Card><CardContent flush><DataTable/></CardContent></Card>
9325
9474
  // GENERAL RULE \u2014 compose godx-ui primitives FULLY; never hand-roll what one ships:
9326
9475
  // padding \u2192 CardContent (not p-4) \xB7 controls \u2192 Input/Select/Button (not raw <input>/<select>/<button>)
9327
- // empty rows \u2192 DataTable's built-in empty / <EmptyState> (not a custom data.length===0 guard).
9328
- // If a primitive exists, USE it \u2014 don't reinvent it.
9476
+ // vertical spacing \u2192 Flex direction="col" gap (not space-y-*) \xB7 empty rows \u2192 DataTable's built-in
9477
+ // empty / <EmptyState> (not a custom data.length===0 guard). If a primitive exists, USE it.
9329
9478
 
9330
9479
  // 1) StatCard shows a DOUBLE border (too thick)
9331
9480
  // Cause: StatCard IS already a bordered Card. Don't wrap it.
9332
9481
  // \u274C <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>
9333
9482
  // \u2705 <ResponsiveGrid columns={4}><StatCard label="x" value="1" /></ResponsiveGrid>
9334
9483
  // Need a section title? Use a heading, NOT a Card:
9335
- // \u2705 <Stack gap="sm"><div className="text-sm font-medium">KPI</div>
9336
- // <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Stack>
9484
+ // \u2705 <Flex direction="col" gap="sm"><div className="text-sm font-medium">KPI</div>
9485
+ // <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Flex>
9337
9486
 
9338
9487
  // 2) Badge renders grey with a \u25CB (no colour) for localized/tier labels
9339
- // Cause: it auto-maps only English lifecycle keys. (@godxjp/ui >= 6.1)
9488
+ // Cause: it auto-maps only English lifecycle keys.
9340
9489
  // \u274C <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" />
9341
9490
  // \u2705 <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} /> // tier \u2192 pill, no icon
9342
- // \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
9491
+ // \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
9343
9492
 
9344
9493
  // 3) Table text collapses to one char per line, or a chip wraps
9345
- // Cause: pre-6.1.2. (@godxjp/ui >= 6.1.2 \u2192 cells + chips are nowrap)
9346
- // \u2705 npm i @godxjp/ui@^6.2.0
9347
9494
  // \u2705 give long columns a width: { key: "name", header: "\u6C0F\u540D", width: "w-64" }
9495
+ // (cells + chips are pinned white-space: nowrap by the library.)
9348
9496
 
9349
9497
  // 4) Empty (icon/action) column header shows a blank grey block
9350
- // (@godxjp/ui >= 6.2.0 auto-hides it: [data-slot=table-head][data-empty] \u2192 transparent)
9351
- // \u2705 npm i @godxjp/ui@^6.2.0 // header: "" now renders a transparent cell
9498
+ // \u2705 header: "" renders a transparent cell \u2014 leave it empty, don't inject a space.
9352
9499
 
9353
9500
  // 5) DataTable columns are crushed / squeezed
9354
9501
  // Cause: the table is nested in a narrow grid column.
9355
9502
  // \u274C <ResponsiveGrid columns={3}><div className="lg:col-span-2"><Card><DataTable/></Card></div></ResponsiveGrid>
9356
9503
  // \u2705 Table gets its OWN full-width row: <Card><CardContent flush><DataTable/></CardContent></Card>
9357
9504
 
9358
- // 6) FilterBar has no padding (sticks to the edge)
9505
+ // 6) Toolbar (filter bar) has no padding (sticks to the edge)
9359
9506
  // Cause: it's inside CardContent flush (flush strips padding \u2014 that's for tables).
9360
- // \u274C <Card><CardContent flush><FilterBar/><DataTable/></CardContent></Card>
9361
- // \u2705 <FilterBar/> then <Card><CardContent flush><DataTable/></CardContent></Card>
9507
+ // \u274C <Card><CardContent flush><Toolbar/><DataTable/></CardContent></Card>
9508
+ // \u2705 <Toolbar/> then <Card><CardContent flush><DataTable/></CardContent></Card>
9362
9509
 
9363
9510
  // 7) Sidebar footer looks washed-out / off-design
9364
9511
  // Cause: raw opacity-*/text-[11px]. Use semantic tokens.
@@ -9374,7 +9521,7 @@ var PATTERNS = [
9374
9521
  // Cause: Math.random()/argless new Date() during render (SSR \u2260 client).
9375
9522
  // \u2705 seed deterministically by index, or compute in an event handler.
9376
9523
 
9377
- // 10) Hide a column on mobile / sign-aware KPI delta (@godxjp/ui >= 6.2.0)
9524
+ // 10) Hide a column on mobile / sign-aware KPI delta
9378
9525
  // \u2705 columns: [{ key: "email", header: "\u30E1\u30FC\u30EB", hiddenOnMobile: true }]
9379
9526
  // \u2705 <StatCard label="\u58F2\u4E0A" value="\xA58.2M" delta="+12%" /> // + green / - red; inverse flips`
9380
9527
  },
@@ -9388,7 +9535,7 @@ import { z } from "zod";
9388
9535
  import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@godxjp/ui/data-display";
9389
9536
  import { FormField, Input } from "@godxjp/ui/data-entry";
9390
9537
  import { Button } from "@godxjp/ui/general";
9391
- import { Stack } from "@godxjp/ui/layout";
9538
+ import { Flex } from "@godxjp/ui/layout";
9392
9539
 
9393
9540
  const schema = z.object({
9394
9541
  name: z.string().min(1, "\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059"),
@@ -9406,18 +9553,19 @@ export function SignUpCard() {
9406
9553
  <CardHeader><CardTitle>\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210</CardTitle></CardHeader>
9407
9554
  <CardContent>
9408
9555
  <form id="signup" onSubmit={onSubmit}>
9409
- <Stack gap="md">
9556
+ {/* Vertical rhythm comes from Flex direction="col" gap \u2014 never space-y-*/gap-* utilities. */}
9557
+ <Flex direction="col" gap="md">
9410
9558
  <FormField id="name" label="\u6C0F\u540D" required error={errors.name?.message}>
9411
9559
  <Input id="name" {...register("name")} />
9412
9560
  </FormField>
9413
9561
  <FormField id="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required error={errors.email?.message}>
9414
9562
  <Input id="email" type="email" {...register("email")} />
9415
9563
  </FormField>
9416
- </Stack>
9564
+ </Flex>
9417
9565
  </form>
9418
9566
  </CardContent>
9419
9567
  <CardFooter separated>
9420
- <Button type="submit" form="signup" disabled={isSubmitting}>\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210</Button>
9568
+ <Button type="submit" form="signup" loading={isSubmitting}>\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210</Button>
9421
9569
  </CardFooter>
9422
9570
  </Card>
9423
9571
  );
@@ -9425,33 +9573,97 @@ export function SignUpCard() {
9425
9573
  },
9426
9574
  {
9427
9575
  name: "settings-page-responsive",
9428
- tagline: "Route-backed settings: persistent desktop local navigation, compact mobile navigation, and bounded form content.",
9429
- tags: ["settings", "form", "tabs", "admin"],
9430
- code: `// Research basis: GitHub/Google/Microsoft/Atlassian account settings + Carbon form grid.
9431
- // Use URLs for every destination; do not keep broad settings IA in local tab state.
9432
- import { NavLink, Outlet } from "react-router-dom";
9433
- import { Flex, Stack } from "@godxjp/ui/layout";
9576
+ aliases: ["settings-tabs", "settings-page", "settings-layout"],
9577
+ 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.",
9578
+ tags: ["settings", "form", "tabs", "admin", "responsive", "navigation"],
9579
+ code: `// Research basis: GitHub, Google Account, Microsoft Account, Atlassian Account settings +
9580
+ // the IBM Carbon form grid. Every destination is a URL, so deep links and browser back/forward
9581
+ // work for free and the active item is derived from the route \u2014 never from local tab state.
9582
+ //
9583
+ // Desktop (>= lg): persistent vertical local nav on the left + bounded content on the right.
9584
+ // Mobile (< lg): the SAME routes become a horizontal, scrollable tab strip above the content.
9585
+ // Styling uses ONLY real semantic token classes (bg-muted / text-foreground / text-muted-foreground)
9586
+ // and responsive/visibility utilities \u2014 the library ships NO bespoke settings shell/nav CSS classes.
9587
+ import { NavLink, Outlet, Navigate } from "react-router-dom";
9588
+ import { Flex } from "@godxjp/ui/layout";
9434
9589
  import { FormField, Input } from "@godxjp/ui/data-entry";
9590
+ import { Button } from "@godxjp/ui/general";
9435
9591
 
9436
- export function WorkspaceSettings() {
9592
+ const SECTIONS = [
9593
+ { to: "general", label: "\u57FA\u672C\u60C5\u5831" },
9594
+ { to: "security", label: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3" },
9595
+ { to: "notifications", label: "\u901A\u77E5" },
9596
+ ] as const;
9597
+
9598
+ // One link renderer for BOTH breakpoints. NavLink gives us the active state (and sets
9599
+ // aria-current="page") straight from the URL; semantic tokens only, no invented classes.
9600
+ const link = ({ isActive }: { isActive: boolean }) =>
9601
+ [
9602
+ "rounded-md px-3 py-2 text-sm no-underline transition-colors whitespace-nowrap",
9603
+ isActive
9604
+ ? "bg-muted text-foreground font-medium"
9605
+ : "text-muted-foreground hover:text-foreground",
9606
+ ].join(" ");
9607
+
9608
+ export function SettingsLayout() {
9437
9609
  return (
9438
- <Flex gap="lg" className="settings-layout">
9439
- <nav aria-label="Settings" className="settings-local-nav">
9440
- <NavLink to="general">\u57FA\u672C\u60C5\u5831</NavLink>
9441
- <NavLink to="security">\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3</NavLink>
9442
- <NavLink to="notifications">\u901A\u77E5</NavLink>
9610
+ <Flex direction="col" gap="lg" className="lg:flex-row lg:items-start">
9611
+ {/* < lg: horizontal, scrollable route tabs. */}
9612
+ <nav aria-label="\u8A2D\u5B9A" className="flex gap-1 overflow-x-auto lg:hidden">
9613
+ {SECTIONS.map((s) => (
9614
+ <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>
9615
+ ))}
9443
9616
  </nav>
9444
- <main className="settings-content"><Outlet /></main>
9617
+ {/* >= lg: persistent vertical local nav (bounded width, does not shrink). */}
9618
+ <nav aria-label="\u8A2D\u5B9A" className="hidden lg:flex lg:w-56 lg:shrink-0 lg:flex-col lg:gap-1">
9619
+ {SECTIONS.map((s) => (
9620
+ <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>
9621
+ ))}
9622
+ </nav>
9623
+ {/* Bounded content region (~42rem) \u2014 the routed section renders here. min-w-0 lets it shrink. */}
9624
+ <main className="min-w-0 max-w-2xl flex-1"><Outlet /></main>
9445
9625
  </Flex>
9446
9626
  );
9447
9627
  }
9448
9628
 
9449
- // Desktop: local nav + bounded content (roughly 40rem), horizontal form rows where useful.
9450
- // Mobile (375/390px): same route links become a compact scrollable nav; labels stack above controls.
9451
- // Do not add a Card around the whole page. Tabs are only for 2\u20134 peer views within one task.
9452
- export function GeneralSettingsForm() {
9453
- return <Stack gap="md"><FormField id="ws-name" label="\u540D\u524D" layout="horizontal" controlWidth="md"><Input id="ws-name" /></FormField></Stack>;
9454
- }`
9629
+ // A settings section. Horizontal label/control rows on desktop (collapse to stacked on mobile),
9630
+ // controls bounded to a semantic width. FormField OWNS the label\u2194control relationship \u2014 do NOT
9631
+ // hand-roll <Label> + <input>. Save feedback = Button loading prop + a toast in onSuccess.
9632
+ export function GeneralSettingsSection({
9633
+ defaults, onSave, saving, error,
9634
+ }: { defaults: { name: string; email: string }; onSave: (v: FormData) => void; saving: boolean; error?: string }) {
9635
+ return (
9636
+ <form onSubmit={(e) => { e.preventDefault(); onSave(new FormData(e.currentTarget)); }}>
9637
+ <Flex direction="col" gap="md">
9638
+ <FormField id="ws-name" label="\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u540D" layout="horizontal" controlWidth="24rem"
9639
+ helper="\u8ACB\u6C42\u66F8\u3084\u5171\u6709\u30EA\u30F3\u30AF\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002" error={error}>
9640
+ <Input id="ws-name" name="name" defaultValue={defaults.name} />
9641
+ </FormField>
9642
+ <FormField id="ws-email" label="\u9023\u7D61\u5148\u30E1\u30FC\u30EB" layout="horizontal" controlWidth="24rem">
9643
+ <Input id="ws-email" name="email" type="email" defaultValue={defaults.email} />
9644
+ </FormField>
9645
+ <Flex gap="sm"><Button type="submit" loading={saving}>\u5909\u66F4\u3092\u4FDD\u5B58</Button></Flex>
9646
+ </Flex>
9647
+ </form>
9648
+ );
9649
+ }
9650
+
9651
+ // Router wiring \u2014 each destination is addressable, so deep links + back/forward just work:
9652
+ // <Route path="settings" element={<SettingsLayout/>}>
9653
+ // <Route index element={<Navigate to="general" replace/>} />
9654
+ // <Route path="general" element={<GeneralSettingsSection .../>} />
9655
+ // <Route path="security" element={<SecuritySection/>} />
9656
+ // <Route path="notifications" element={<NotificationsSection/>} />
9657
+ // </Route>
9658
+
9659
+ // \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
9660
+ // \u2705 Tabs (navigation) are fine for a SMALL number (2\u20134) of PEER views inside ONE task/section
9661
+ // \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.
9662
+ // \u26D4 Tabs as the PRIMARY IA for broad settings (Account / Security / Billing / Members / \u2026):
9663
+ // use route-backed local navigation so every area is deep-linkable and back/forward works.
9664
+ // \u26D4 Do NOT wrap the whole settings page in a Card, and do NOT make desktop a horizontal tab bar
9665
+ // \u2014 persistent local nav scales to many sections; a tab strip does not.
9666
+ // \u26D4 Do NOT invent CSS class names for the shell/nav/content \u2014 compose Flex + semantic tokens.`
9455
9667
  },
9456
9668
  {
9457
9669
  name: "confirm-destructive",
@@ -9461,7 +9673,7 @@ export function GeneralSettingsForm() {
9461
9673
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
9462
9674
  import { Input } from "@godxjp/ui/data-entry";
9463
9675
  import { Button } from "@godxjp/ui/general";
9464
- import { Stack } from "@godxjp/ui/layout";
9676
+ import { Flex } from "@godxjp/ui/layout";
9465
9677
  import { toast } from "sonner";
9466
9678
 
9467
9679
  export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {
@@ -9473,9 +9685,9 @@ export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolea
9473
9685
  <DialogTitle>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664</DialogTitle>
9474
9686
  <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>
9475
9687
  </DialogHeader>
9476
- <Stack gap="md">
9688
+ <Flex direction="col" gap="md">
9477
9689
  <Input value={confirm} onValueChange={(e) => setConfirm(e.target.value)} placeholder={slug} />
9478
- </Stack>
9690
+ </Flex>
9479
9691
  <DialogFooter>
9480
9692
  <Button variant="outline" onClick={() => onOpenChange(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
9481
9693
  <Button tone="destructive" disabled={confirm !== slug} onClick={() => { toast.success("\u524A\u9664\u3057\u307E\u3057\u305F"); onOpenChange(false); }}>\u5B8C\u5168\u306B\u524A\u9664</Button>
@@ -9515,14 +9727,14 @@ export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
9515
9727
  },
9516
9728
  {
9517
9729
  name: "inertia-list-page",
9518
- tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable + Badge + Pagination (current primitive API).",
9730
+ tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + Toolbar + DataTable + Badge + Pagination (current primitive API).",
9519
9731
  tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
9520
9732
  code: `import { Head, router } from "@inertiajs/react"
9521
9733
  import { useMemo, useState } from "react"
9522
- import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
9734
+ import { PageContainer, ResponsiveGrid, Flex } from "@godxjp/ui/layout"
9523
9735
  import { Card, CardContent, StatCard, DataTable, EmptyState, Badge, type ColumnDef } from "@godxjp/ui/data-display"
9524
9736
  import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
9525
- import { FilterBar, FilterGroup, Pagination } from "@godxjp/ui/navigation"
9737
+ import { Toolbar, ToolbarGroup, Pagination } from "@godxjp/ui/navigation"
9526
9738
  import { formatDate } from "@godxjp/ui/datetime"
9527
9739
  import { withCrmLayout } from "@/layouts/crm-layout" // see "inertia-persistent-layout"
9528
9740
 
@@ -9553,19 +9765,20 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
9553
9765
  return (
9554
9766
  <>
9555
9767
  <Head title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" />
9556
- {/* RULE: every page wraps in PageContainer; spacing via Stack/ResponsiveGrid, never p-*/gap-* */}
9768
+ {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction="col"/ResponsiveGrid, never p-*/gap-* */}
9557
9769
  <PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
9558
- <Stack gap="lg">
9770
+ <Flex direction="col" gap="lg">
9559
9771
  <ResponsiveGrid columns={3}>
9560
9772
  <StatCard label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
9561
9773
  <StatCard label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
9562
9774
  <StatCard label="\u4EF6\u6570" value={coupons.length} />
9563
9775
  </ResponsiveGrid>
9564
9776
 
9565
- <FilterBar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
9777
+ {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}
9778
+ <Toolbar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
9566
9779
  {/* SearchInput is value + onSearch(v) \u2014 NOT onChange */}
9567
9780
  <SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />
9568
- <FilterGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
9781
+ <ToolbarGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
9569
9782
  <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>
9570
9783
  <SelectTrigger><SelectValue /></SelectTrigger>
9571
9784
  <SelectContent>
@@ -9574,8 +9787,8 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
9574
9787
  <SelectItem value="\u4E0B\u66F8\u304D">\u4E0B\u66F8\u304D</SelectItem>
9575
9788
  </SelectContent>
9576
9789
  </Select>
9577
- </FilterGroup>
9578
- </FilterBar>
9790
+ </ToolbarGroup>
9791
+ </Toolbar>
9579
9792
 
9580
9793
  <Card>
9581
9794
  <CardContent flush>
@@ -9585,10 +9798,11 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
9585
9798
  </CardContent>
9586
9799
  </Card>
9587
9800
 
9801
+ {/* Pagination ONLY for real multi-page data \u2014 hide it while empty or single-page. */}
9588
9802
  {filtered.length > PAGE_SIZE && (
9589
9803
  <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />
9590
9804
  )}
9591
- </Stack>
9805
+ </Flex>
9592
9806
  </PageContainer>
9593
9807
  </>
9594
9808
  )
@@ -9602,7 +9816,7 @@ export default Coupons`
9602
9816
  tagline: "Inertia detail page \u2014 receives {id} prop, Descriptions (compound) + StatCard + EmptyState fallback.",
9603
9817
  tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
9604
9818
  code: `import { Head, router } from "@inertiajs/react"
9605
- import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
9819
+ import { PageContainer, ResponsiveGrid, Flex } from "@godxjp/ui/layout"
9606
9820
  import { Card, CardContent, StatCard, EmptyState, Descriptions, Badge } from "@godxjp/ui/data-display"
9607
9821
  import { Button } from "@godxjp/ui/general"
9608
9822
  import { formatDate } from "@godxjp/ui/datetime"
@@ -9630,7 +9844,7 @@ function MemberShow({ id }: { id: string }) {
9630
9844
  <>
9631
9845
  <Head title={member.name} />
9632
9846
  <PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
9633
- <Stack gap="lg">
9847
+ <Flex direction="col" gap="lg">
9634
9848
  <ResponsiveGrid columns={4}>
9635
9849
  <StatCard label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
9636
9850
  <StatCard label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
@@ -9648,7 +9862,7 @@ function MemberShow({ id }: { id: string }) {
9648
9862
  </Descriptions>
9649
9863
  </CardContent>
9650
9864
  </Card>
9651
- </Stack>
9865
+ </Flex>
9652
9866
  </PageContainer>
9653
9867
  </>
9654
9868
  )
@@ -9692,7 +9906,7 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
9692
9906
  },
9693
9907
  {
9694
9908
  name: "badge-coloring",
9695
- tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props, @godxjp/ui \u2265 6.1).",
9909
+ tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props).",
9696
9910
  tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
9697
9911
  code: `import { Badge } from "@godxjp/ui/data-display"
9698
9912
 
@@ -9701,7 +9915,7 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
9701
9915
  // scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
9702
9916
  // Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
9703
9917
 
9704
- // 1) Lifecycle with localized text \u2014 map to the key, keep JP via \`label\` (icon stays):
9918
+ // 1) Lifecycle with localized text \u2014 map to the key, keep JP via children (icon stays):
9705
9919
  <Badge status="active">\u516C\u958B\u4E2D</Badge> // green \u2713 \u516C\u958B\u4E2D
9706
9920
 
9707
9921
  // 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
@@ -9712,78 +9926,348 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
9712
9926
  <Badge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
9713
9927
  <Badge status="\u6CD5\u4EBA\u5171\u901A" tone="info" icon={null} />
9714
9928
 
9715
- // tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type BadgeTone)
9929
+ // tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type ToneProp)
9716
9930
  // RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
9717
9931
  // narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
9718
9932
  // import that instead of the raw Badge across pages.`
9719
9933
  },
9720
9934
  {
9721
9935
  name: "async-data-state",
9722
- tagline: "Mutually exclusive prerequisite, loading, data, empty, and cause-aware error states.",
9723
- tags: ["async", "loading", "query", "error", "empty"],
9724
- code: `import { DataState } from "@godxjp/ui/query";
9725
- import { EmptyState, SkeletonTable } from "@godxjp/ui/data-display";
9726
-
9727
- // enabled:false is prerequisite/idle, not loading \u2014 DataState checks fetchStatus, not just isPending.
9728
- // Errors are classified by cause: onAuthError handles 401/expired token (session renewal, NOT retry);
9729
- // Retry appears automatically only for transient/network/5xx; 403/404/422 show a cause-aware message.
9730
- <DataState query={query} prerequisite={<EmptyState variant="section" title="\u7D44\u7E54\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" />}
9731
- skeleton={<SkeletonTable />} empty={<EmptyState variant="section" title="\u7D50\u679C\u304C\u3042\u308A\u307E\u305B\u3093" />}
9732
- isEmpty={(data) => data.items.length === 0}
9733
- onAuthError={() => auth.signInAgain()}>
9734
- {(data) => <Results items={data.items} />}
9735
- </DataState>
9736
-
9737
- // Need a bespoke error UI? Pass errorRenderer and branch on classifyQueryError(error).category
9738
- // ("auth" | "forbidden" | "notFound" | "validation" | "transient" | "unknown").
9739
- // The default detail is a localized message \u2014 never raw token/endpoint/stack text.
9740
- // Never render pagination outside the successful populated-data branch.`
9936
+ aliases: ["loading-states", "data-state", "query-states"],
9937
+ 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.",
9938
+ tags: ["async", "loading", "query", "error", "empty", "prerequisite", "retry", "react-query"],
9939
+ code: `import { DataState, classifyQueryError } from "@godxjp/ui/query";
9940
+ import { EmptyState } from "@godxjp/ui/data-display";
9941
+ import { SkeletonTable } from "@godxjp/ui/feedback";
9942
+ import { Building2, Inbox } from "lucide-react";
9943
+
9944
+ // ONE primary state renders at a time; DataState makes them mutually exclusive:
9945
+ // prerequisite \u2192 skeleton(loading) \u2192 data \u2192 empty \u2192 error (never two at once).
9946
+ //
9947
+ // enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is
9948
+ // disabled, but fetchStatus stays "idle" (no request in flight) \u2014 DataState renders the prerequisite
9949
+ // slot, so a disabled query shows an instruction, NOT an endless skeleton.
9950
+ // const query = useQuery({ queryKey: ["members", orgId], queryFn, enabled: Boolean(orgId) });
9951
+ //
9952
+ // A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and
9953
+ // announces the busy state politely \u2014 stale/placeholder refresh is handled for you.
9954
+ //
9955
+ // Errors are classified by CAUSE, not blanket-retried:
9956
+ // auth (401) \u2192 onAuthError: renew session / sign in again (NOT a retry)
9957
+ // forbidden (403) \u2192 permission message + access path (no retry)
9958
+ // notFound (404) \u2192 contextual not-found (no retry)
9959
+ // validation (400/422)\u2192 corrective guidance (no retry)
9960
+ // transient (408/429/5xx/network) \u2192 Retry offered automatically
9961
+ // unknown \u2192 neutral; opt into Retry via showRetry/onRetry only if it can help
9962
+ export function MembersPanel({ query, orgId, onSignIn }: {
9963
+ query: any; orgId?: string; onSignIn: () => void;
9964
+ }) {
9965
+ return (
9966
+ <DataState
9967
+ query={query}
9968
+ prerequisite={<EmptyState icon={Building2} variant="section" title="\u7D44\u7E54\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044"
9969
+ 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" />}
9970
+ skeleton={<SkeletonTable rows={8} columns={4} />}
9971
+ empty={<EmptyState icon={Inbox} variant="section" title="\u30E1\u30F3\u30D0\u30FC\u304C\u3044\u307E\u305B\u3093"
9972
+ description="\u3053\u306E\u7D44\u7E54\u306B\u306F\u307E\u3060\u30E1\u30F3\u30D0\u30FC\u304C\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002" />}
9973
+ isEmpty={(data) => data.items.length === 0}
9974
+ onAuthError={onSignIn}
9975
+ >
9976
+ {(data) => <MemberTable items={data.items} />}
9977
+ </DataState>
9978
+ );
9979
+ }
9980
+
9981
+ // Need a bespoke error surface? Pass errorRenderer and branch on the classified category \u2014 the
9982
+ // default detail is always a localized message (never raw token / endpoint / stack text):
9983
+ // errorRenderer={(error, retry) => {
9984
+ // const { category } = classifyQueryError(error);
9985
+ // if (category === "auth") return <SessionExpired onRenew={onSignIn} />;
9986
+ // if (category === "forbidden") return <NoAccess />;
9987
+ // if (category === "transient") return <Retryable onRetry={retry} />;
9988
+ // return <GenericError />;
9989
+ // }}
9990
+ // RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).`
9741
9991
  },
9742
9992
  {
9743
9993
  name: "data-table-page",
9744
- tagline: "Filter + table + single-row footer pagination, visible only for successful multi-page data.",
9745
- tags: ["table", "pagination", "filter", "async"],
9746
- code: `// FilterBar is standalone. Card owns one table surface; do not nest Card/Alert surfaces.
9747
- // Inside DataState's success branch:
9748
- <Card><CardContent flush><DataTable data={data.items} columns={columns} />
9749
- {data.totalPages > 1 && <Flex justify="between" align="center"><span>{range}</span><Pagination /></Flex>}
9750
- </CardContent></Card>
9751
- // Hide pagination for prerequisite/loading/error/empty and one-page results.
9752
- // Preserve filters/page during a transient retry; stack only at narrow mobile widths.`
9994
+ aliases: ["filter-bar", "table-state", "list-page"],
9995
+ 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.",
9996
+ tags: ["table", "pagination", "filter", "async", "toolbar", "datatable"],
9997
+ code: `import { useState } from "react";
9998
+ import { DataState } from "@godxjp/ui/query";
9999
+ import { Card, CardContent, DataTable, EmptyState, type ColumnDef } from "@godxjp/ui/data-display";
10000
+ import { SkeletonTable } from "@godxjp/ui/feedback";
10001
+ import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry";
10002
+ import { Toolbar, ToolbarGroup, Pagination } from "@godxjp/ui/navigation";
10003
+ import { Flex } from "@godxjp/ui/layout";
10004
+ import { useQuery, keepPreviousData } from "@tanstack/react-query";
10005
+
10006
+ type Row = { id: string; name: string; status: string };
10007
+
10008
+ export function MembersTablePage({ orgId }: { orgId?: string }) {
10009
+ const [q, setQ] = useState("");
10010
+ const [status, setStatus] = useState("all");
10011
+ const [page, setPage] = useState(1);
10012
+ const pageSize = 20;
10013
+
10014
+ // placeholderData: keepPreviousData \u2192 on page/filter change the PREVIOUS page stays visible
10015
+ // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).
10016
+ const query = useQuery({
10017
+ queryKey: ["members", orgId, q, status, page],
10018
+ queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),
10019
+ enabled: Boolean(orgId),
10020
+ placeholderData: keepPreviousData,
10021
+ });
10022
+
10023
+ const columns: ColumnDef<Row>[] = [
10024
+ { key: "name", header: "\u6C0F\u540D", render: (r) => <span className="font-medium">{r.name}</span> },
10025
+ { key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9" },
10026
+ ];
10027
+
10028
+ return (
10029
+ <Flex direction="col" gap="lg">
10030
+ {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even
10031
+ while a query is idle/loading/empty. Changing a filter resets to page 1. */}
10032
+ <Toolbar hasActiveFilters={q !== "" || status !== "all"}
10033
+ onClear={() => { setQ(""); setStatus("all"); setPage(1); }}>
10034
+ <SearchInput placeholder="\u6C0F\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1); }} />
10035
+ <ToolbarGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
10036
+ <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1); }}>
10037
+ <SelectTrigger><SelectValue /></SelectTrigger>
10038
+ <SelectContent>
10039
+ <SelectItem value="all">\u3059\u3079\u3066</SelectItem>
10040
+ <SelectItem value="active">\u6709\u52B9</SelectItem>
10041
+ <SelectItem value="invited">\u62DB\u5F85\u4E2D</SelectItem>
10042
+ </SelectContent>
10043
+ </Select>
10044
+ </ToolbarGroup>
10045
+ </Toolbar>
10046
+
10047
+ {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it
10048
+ is hidden during prerequisite / loading / empty / error and for single-page results. */}
10049
+ <DataState
10050
+ query={query}
10051
+ prerequisite={<EmptyState variant="section" title="\u7D44\u7E54\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" />}
10052
+ skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}
10053
+ 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" />}
10054
+ isEmpty={(d) => d.items.length === 0}
10055
+ >
10056
+ {(d) => {
10057
+ const totalPages = Math.ceil(d.total / pageSize);
10058
+ return (
10059
+ <>
10060
+ <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>
10061
+ {totalPages > 1 && (
10062
+ <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />
10063
+ )}
10064
+ </>
10065
+ );
10066
+ }}
10067
+ </DataState>
10068
+ </Flex>
10069
+ );
10070
+ }
10071
+ // Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.
10072
+ // A transient retry keeps q/status/page (they live in component state, not the query) \u2014 the
10073
+ // same page reloads in place; the user never loses their filters.`
9753
10074
  },
9754
10075
  {
9755
10076
  name: "organization-memberships",
9756
- tagline: "Workspace identity, current state, role and permission-aware actions; invitations are conditional.",
9757
- tags: ["organization", "workspace", "membership", "invitation", "account"],
9758
- code: `// Research basis: GitHub organizations and Slack workspace switching/invitations.
9759
- // Each row: logo + recognizable name + Current badge + role + Open/Switch/Manage/Leave action.
9760
- // Put Create organization at page level. Do not show raw membership timestamps without labels.
9761
- // No pending invitations \u2192 omit the section entirely.
9762
- // Few pending \u2192 compact actionable list; many/history \u2192 a focused route.
9763
- <MembershipList memberships={memberships} currentId={currentId} />
9764
- {pendingInvitations.length > 0 && <PendingInvitations variant="compact" items={pendingInvitations} />}`
10077
+ aliases: ["organization-switcher", "workspace-switcher", "received-invitations", "memberships"],
10078
+ 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).",
10079
+ tags: ["organization", "workspace", "membership", "invitation", "account", "switcher"],
10080
+ code: `// Research basis: GitHub organization membership + Slack workspace switching/invitations.
10081
+ // Each row = recognizable identity (Avatar/logo, NOT an identical decorative icon) + name +
10082
+ // a "\u73FE\u5728" (Current) badge for the active org + role + a permission-aware action menu.
10083
+ import { Card, CardContent, CardHeader, CardTitle, ListRow, Badge, Avatar, AvatarImage, AvatarFallback, EmptyState } from "@godxjp/ui/data-display";
10084
+ import { Button } from "@godxjp/ui/general";
10085
+ import { Flex } from "@godxjp/ui/layout";
10086
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@godxjp/ui/navigation";
10087
+ import { MoreHorizontal, Plus } from "lucide-react";
10088
+
10089
+ type Role = "owner" | "admin" | "member";
10090
+ type Membership = { id: string; name: string; logoUrl?: string; role: Role; joinedAt: string; isCurrent: boolean; canManage: boolean };
10091
+ type Invite = { id: string; orgName: string; invitedBy: string };
10092
+
10093
+ const ROLE_LABEL: Record<Role, string> = { owner: "\u30AA\u30FC\u30CA\u30FC", admin: "\u7BA1\u7406\u8005", member: "\u30E1\u30F3\u30D0\u30FC" };
10094
+
10095
+ export function OrganizationMemberships({
10096
+ memberships, invitations, onOpen, onSwitch, onManage, onLeave, onCreate, onAccept, onDecline,
10097
+ }: {
10098
+ memberships: Membership[]; invitations: Invite[];
10099
+ onOpen: (id: string) => void; onSwitch: (id: string) => void; onManage: (id: string) => void;
10100
+ onLeave: (id: string) => void; onCreate: () => void; onAccept: (id: string) => void; onDecline: (id: string) => void;
10101
+ }) {
10102
+ return (
10103
+ <Flex direction="col" gap="lg">
10104
+ <Card>
10105
+ <CardHeader>
10106
+ <Flex justify="between" align="center">
10107
+ <CardTitle>\u7D44\u7E54</CardTitle>
10108
+ {/* Create belongs at page/section level, not per row. */}
10109
+ <Button variant="outline" size="sm" onClick={onCreate}><Plus className="size-4" />\u7D44\u7E54\u3092\u4F5C\u6210</Button>
10110
+ </Flex>
10111
+ </CardHeader>
10112
+ {/* flush \u2192 ListRows draw their own edge-to-edge dividers. */}
10113
+ <CardContent flush>
10114
+ {memberships.length === 0 ? (
10115
+ <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" />
10116
+ ) : (
10117
+ memberships.map((m) => (
10118
+ <ListRow
10119
+ key={m.id}
10120
+ leading={
10121
+ <Avatar>
10122
+ {m.logoUrl ? <AvatarImage src={m.logoUrl} alt="" /> : null}
10123
+ <AvatarFallback>{m.name.slice(0, 1)}</AvatarFallback>
10124
+ </Avatar>
10125
+ }
10126
+ title={
10127
+ <Flex gap="sm" align="center">
10128
+ <span className="font-medium">{m.name}</span>
10129
+ {m.isCurrent && <Badge tone="success" icon={null}>\u73FE\u5728</Badge>}
10130
+ </Flex>
10131
+ }
10132
+ description={ROLE_LABEL[m.role]}
10133
+ trailing={
10134
+ <Flex gap="sm" align="center">
10135
+ {m.isCurrent
10136
+ ? <Button size="sm" onClick={() => onOpen(m.id)}>\u958B\u304F</Button>
10137
+ : <Button variant="outline" size="sm" onClick={() => onSwitch(m.id)}>\u5207\u308A\u66FF\u3048</Button>}
10138
+ <DropdownMenu>
10139
+ <DropdownMenuTrigger asChild>
10140
+ <Button variant="ghost" size="icon" aria-label={\`\${m.name} \u306E\u64CD\u4F5C\`}><MoreHorizontal className="size-4" /></Button>
10141
+ </DropdownMenuTrigger>
10142
+ <DropdownMenuContent align="end">
10143
+ <DropdownMenuItem onSelect={() => onOpen(m.id)}>\u958B\u304F</DropdownMenuItem>
10144
+ {m.canManage && <DropdownMenuItem onSelect={() => onManage(m.id)}>\u7BA1\u7406</DropdownMenuItem>}
10145
+ {m.role !== "owner" && <DropdownMenuItem onSelect={() => onLeave(m.id)}>\u9000\u51FA</DropdownMenuItem>}
10146
+ </DropdownMenuContent>
10147
+ </DropdownMenu>
10148
+ </Flex>
10149
+ }
10150
+ />
10151
+ ))
10152
+ )}
10153
+ </CardContent>
10154
+ </Card>
10155
+
10156
+ {/* CONDITIONAL invitations \u2014 no pending invites \u21D2 render NOTHING (no permanent empty Card).
10157
+ Few pending \u21D2 a compact actionable list. Many/history \u21D2 link to a focused route instead. */}
10158
+ {invitations.length > 0 && (
10159
+ <Card>
10160
+ <CardHeader><CardTitle>\u4FDD\u7559\u4E2D\u306E\u62DB\u5F85</CardTitle></CardHeader>
10161
+ <CardContent flush>
10162
+ {invitations.map((inv) => (
10163
+ <ListRow
10164
+ key={inv.id}
10165
+ title={<span className="font-medium">{inv.orgName}</span>}
10166
+ description={\`\${inv.invitedBy} \u304B\u3089\u306E\u62DB\u5F85\`}
10167
+ trailing={
10168
+ <Flex gap="sm">
10169
+ <Button size="sm" onClick={() => onAccept(inv.id)}>\u53C2\u52A0</Button>
10170
+ <Button variant="ghost" size="sm" onClick={() => onDecline(inv.id)}>\u8F9E\u9000</Button>
10171
+ </Flex>
10172
+ }
10173
+ />
10174
+ ))}
10175
+ </CardContent>
10176
+ </Card>
10177
+ )}
10178
+ </Flex>
10179
+ );
10180
+ }
10181
+ // DON'T: passive rows with no action \xB7 identical decorative icons \xB7 unlabeled timestamps
10182
+ // (label them: "2024\u5E74\u306B\u53C2\u52A0") \xB7 a large empty invitation Card shown permanently.`
9765
10183
  },
9766
10184
  {
9767
10185
  name: "account-recovery-settings",
9768
- tagline: "Signed-in recovery method status rows, separate from password change and signed-out recovery.",
9769
- tags: ["account", "recovery", "security", "backup-codes", "password"],
9770
- code: `// Research basis: Google Account recovery, Microsoft Security info, GitHub recovery codes.
9771
- // Use compact method rows: method + verified/available status + Add/Change/Regenerate action.
9772
- // Do not stack generic Info Alerts for ordinary capability status.
9773
- <RecoveryMethodRow method="email" value={maskedEmail} status="verified" action={<Button>\u5909\u66F4</Button>} />
9774
- {backupCodesSupported && <RecoveryMethodRow method="backup-codes" status={codeStatus} action={<Button>\u518D\u751F\u6210</Button>} />}
9775
- // Password change is a separate destination. Forgot-password is a signed-out journey.`
10186
+ aliases: ["recovery-email", "backup-codes", "security-settings", "recovery-settings"],
10187
+ 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.",
10188
+ tags: ["account", "recovery", "security", "backup-codes", "password", "settings"],
10189
+ code: `// Research basis: Google Account recovery (email/phone + backup codes), Microsoft Security info,
10190
+ // GitHub recovery codes. Signed-IN recovery SETTINGS (manage your methods) is a DIFFERENT surface
10191
+ // from the signed-OUT forgot-password JOURNEY \u2014 do not mix them on one page.
10192
+ import { Card, CardContent, CardHeader, CardTitle, ListRow, Badge } from "@godxjp/ui/data-display";
10193
+ import { Button } from "@godxjp/ui/general";
10194
+ import { Flex } from "@godxjp/ui/layout";
10195
+ import { Mail, Smartphone, KeyRound } from "lucide-react";
10196
+
10197
+ type MethodStatus = "verified" | "unverified" | "unavailable";
10198
+
10199
+ const STATUS: Record<MethodStatus, { tone: "success" | "warning" | "neutral"; label: string }> = {
10200
+ verified: { tone: "success", label: "\u78BA\u8A8D\u6E08\u307F" },
10201
+ unverified: { tone: "warning", label: "\u672A\u78BA\u8A8D" },
10202
+ unavailable: { tone: "neutral", label: "\u672A\u8A2D\u5B9A" },
10203
+ };
10204
+
10205
+ function MethodRow({ icon: Icon, name, value, status, action }: {
10206
+ icon: React.ComponentType<{ className?: string }>; name: string; value?: string;
10207
+ status: MethodStatus; action: React.ReactNode;
10208
+ }) {
10209
+ const s = STATUS[status];
10210
+ return (
10211
+ <ListRow
10212
+ leading={<Icon className="text-muted-foreground size-5" />}
10213
+ title={<span className="font-medium">{name}</span>}
10214
+ description={value ?? "\u672A\u8A2D\u5B9A"}
10215
+ trailing={<Flex gap="sm" align="center"><Badge tone={s.tone} icon={null}>{s.label}</Badge>{action}</Flex>}
10216
+ />
10217
+ );
10218
+ }
10219
+
10220
+ // Compact status/action rows \u2014 one row per method, capability-aware. Do NOT stack full-width
10221
+ // Info Alerts for ordinary status, and do NOT show unavailable features as permanent page content
10222
+ // beyond a single row that offers to set them up.
10223
+ export function AccountRecoverySettings({ email, phone, backupCodesSupported, backupCodesRemaining, onChangeEmail, onChangePhone, onRegenerateCodes, onChangePassword }: {
10224
+ email?: string; phone?: string; backupCodesSupported: boolean; backupCodesRemaining: number;
10225
+ onChangeEmail: () => void; onChangePhone: () => void; onRegenerateCodes: () => void; onChangePassword: () => void;
10226
+ }) {
10227
+ return (
10228
+ <Flex direction="col" gap="lg">
10229
+ <Card>
10230
+ <CardHeader><CardTitle>\u30A2\u30AB\u30A6\u30F3\u30C8\u5FA9\u65E7</CardTitle></CardHeader>
10231
+ <CardContent flush>
10232
+ <MethodRow icon={Mail} name="\u5FA9\u65E7\u7528\u30E1\u30FC\u30EB" value={email} status={email ? "verified" : "unavailable"}
10233
+ action={<Button variant="outline" size="sm" onClick={onChangeEmail}>{email ? "\u5909\u66F4" : "\u8FFD\u52A0"}</Button>} />
10234
+ <MethodRow icon={Smartphone} name="\u5FA9\u65E7\u7528\u96FB\u8A71\u756A\u53F7" value={phone} status={phone ? "verified" : "unavailable"}
10235
+ action={<Button variant="outline" size="sm" onClick={onChangePhone}>{phone ? "\u5909\u66F4" : "\u8FFD\u52A0"}</Button>} />
10236
+ {/* Capability-aware: only render backup codes when the account type supports them. */}
10237
+ {backupCodesSupported && (
10238
+ <MethodRow icon={KeyRound} name="\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u30B3\u30FC\u30C9"
10239
+ value={\`\u6B8B\u308A \${backupCodesRemaining} \u500B\`} status={backupCodesRemaining > 0 ? "verified" : "unverified"}
10240
+ action={<Button variant="outline" size="sm" onClick={onRegenerateCodes}>\u518D\u751F\u6210</Button>} />
10241
+ )}
10242
+ </CardContent>
10243
+ </Card>
10244
+
10245
+ {/* Password change is its OWN destination (risk-appropriate confirmation lives there). */}
10246
+ <Card>
10247
+ <CardHeader><CardTitle>\u30D1\u30B9\u30EF\u30FC\u30C9</CardTitle></CardHeader>
10248
+ <CardContent>
10249
+ <Flex justify="between" align="center">
10250
+ <span className="text-muted-foreground text-sm">\u6700\u7D42\u66F4\u65B0: 90\u65E5\u524D</span>
10251
+ <Button variant="outline" onClick={onChangePassword}>\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u5909\u66F4</Button>
10252
+ </Flex>
10253
+ </CardContent>
10254
+ </Card>
10255
+ </Flex>
10256
+ );
10257
+ }
10258
+ // The signed-OUT recovery journey (forgot password \u2192 email link \u2192 reset) is a separate flow on the
10259
+ // auth screens (AuthShell), NOT part of these signed-in settings. Do not surface it here.`
9776
10260
  }
9777
10261
  ];
9778
10262
  function findPattern(name) {
9779
10263
  const slug = name.trim().toLowerCase();
9780
- return PATTERNS.find((p) => p.name === slug);
10264
+ return PATTERNS.find((p) => p.name === slug || p.aliases?.includes(slug));
9781
10265
  }
9782
10266
  function searchPatterns(query) {
9783
10267
  const q = query.trim().toLowerCase();
9784
10268
  if (q === "") return PATTERNS;
9785
10269
  return PATTERNS.filter(
9786
- (p) => p.name.includes(q) || p.tagline.toLowerCase().includes(q) || p.tags.some((t) => t.includes(q))
10270
+ (p) => p.name.includes(q) || p.aliases?.some((a) => a.includes(q)) || p.tagline.toLowerCase().includes(q) || p.tags.some((t) => t.includes(q))
9787
10271
  );
9788
10272
  }
9789
10273
 
@@ -12060,8 +12544,8 @@ function visualRulesByCategory(category) {
12060
12544
  // package.json
12061
12545
  var package_default = {
12062
12546
  name: "@godxjp/ui-mcp",
12063
- version: "17.0.1",
12064
- godxUiCompatibility: "17.0.x",
12547
+ version: "18.0.1",
12548
+ godxUiCompatibility: "18.0.x",
12065
12549
  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).",
12066
12550
  type: "module",
12067
12551
  main: "./dist/index.js",
@@ -12149,7 +12633,7 @@ var TOOL_DEFINITIONS = [
12149
12633
  },
12150
12634
  {
12151
12635
  name: "list_patterns",
12152
- 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`.",
12636
+ 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`.",
12153
12637
  inputSchema: { type: "object", properties: {} }
12154
12638
  },
12155
12639
  {
@@ -13159,7 +13643,12 @@ function suggestPrimitive(useCase) {
13159
13643
  "Select=discrete options, AutoComplete=free-text+suggestions.",
13160
13644
  3
13161
13645
  );
13162
- check(["filter"], "Form layout='inline' + pattern 'filter-bar'", "Inline form above table.", 4);
13646
+ check(
13647
+ ["filter"],
13648
+ "Toolbar/ToolbarGroup + pattern 'filter-bar' (\u2192 data-table-page)",
13649
+ "Standalone Toolbar filter bar above a table; see the data-table-page pattern.",
13650
+ 4
13651
+ );
13163
13652
  check(
13164
13653
  ["delete", "destructive"],
13165
13654
  "Pattern 'confirm-destructive'",