@godxjp/ui-mcp 17.0.1 → 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 +607 -123
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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.",
|
|
@@ -2084,7 +2139,7 @@ import { Smartphone } from "lucide-react";
|
|
|
2084
2139
|
{
|
|
2085
2140
|
name: "FormField",
|
|
2086
2141
|
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.",
|
|
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.",
|
|
2088
2143
|
props: [
|
|
2089
2144
|
{
|
|
2090
2145
|
name: "id",
|
|
@@ -2144,7 +2199,10 @@ import { Smartphone } from "lucide-react";
|
|
|
2144
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.",
|
|
2145
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.",
|
|
2146
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.",
|
|
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."
|
|
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`."
|
|
2148
2206
|
],
|
|
2149
2207
|
useCases: [
|
|
2150
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.",
|
|
@@ -2517,6 +2575,52 @@ import { Smartphone } from "lucide-react";
|
|
|
2517
2575
|
description: "Label for the clear row (data-driven combobox mode)."
|
|
2518
2576
|
},
|
|
2519
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
|
+
},
|
|
2520
2624
|
{
|
|
2521
2625
|
name: "name",
|
|
2522
2626
|
type: "string",
|
|
@@ -2542,6 +2646,12 @@ import { Smartphone } from "lucide-react";
|
|
|
2542
2646
|
type: '"sm" | "md"',
|
|
2543
2647
|
defaultValue: '"md"',
|
|
2544
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."
|
|
2545
2655
|
}
|
|
2546
2656
|
],
|
|
2547
2657
|
usage: [
|
|
@@ -2552,7 +2662,12 @@ import { Smartphone } from "lucide-react";
|
|
|
2552
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.",
|
|
2553
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.",
|
|
2554
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.",
|
|
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."
|
|
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."
|
|
2556
2671
|
],
|
|
2557
2672
|
useCases: [
|
|
2558
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.",
|
|
@@ -3579,10 +3694,14 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
3579
3694
|
{
|
|
3580
3695
|
name: "items",
|
|
3581
3696
|
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."
|
|
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)."
|
|
3583
3698
|
},
|
|
3584
3699
|
{ name: "value", type: "string", description: "Controlled active tab key." },
|
|
3585
|
-
{
|
|
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
|
+
},
|
|
3586
3705
|
{
|
|
3587
3706
|
name: "onValueChange",
|
|
3588
3707
|
type: "(value: string) => void",
|
|
@@ -3595,7 +3714,9 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
3595
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.",
|
|
3596
3715
|
"DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",
|
|
3597
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.',
|
|
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."
|
|
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`."
|
|
3599
3720
|
],
|
|
3600
3721
|
useCases: [
|
|
3601
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.",
|
|
@@ -3653,10 +3774,33 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
3653
3774
|
name: "showSizeChanger",
|
|
3654
3775
|
type: "boolean",
|
|
3655
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.`
|
|
3656
3798
|
}
|
|
3657
3799
|
],
|
|
3658
3800
|
usage: [
|
|
3659
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.",
|
|
3660
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.",
|
|
3661
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.",
|
|
3662
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.",
|
|
@@ -7245,7 +7389,7 @@ export default function PasswordBlock() {
|
|
|
7245
7389
|
{
|
|
7246
7390
|
name: "appearance",
|
|
7247
7391
|
type: '"labeled" | "icon"',
|
|
7248
|
-
description:
|
|
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.'
|
|
7249
7393
|
},
|
|
7250
7394
|
{
|
|
7251
7395
|
name: "className",
|
|
@@ -7830,8 +7974,8 @@ var PROP_VOCABULARY = [
|
|
|
7830
7974
|
name: "GapProp",
|
|
7831
7975
|
concept: "Shared layout gap scale.",
|
|
7832
7976
|
values: ["xs", "sm", "md", "lg", "xl"],
|
|
7833
|
-
usedBy: ["
|
|
7834
|
-
notes: "
|
|
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).'
|
|
7835
7979
|
},
|
|
7836
7980
|
{
|
|
7837
7981
|
name: "TitleProp",
|
|
@@ -9260,7 +9404,7 @@ var CARDINAL_RULES = [
|
|
|
9260
9404
|
{
|
|
9261
9405
|
number: 40,
|
|
9262
9406
|
title: "Pages are mobile-first",
|
|
9263
|
-
body:
|
|
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.'
|
|
9264
9408
|
},
|
|
9265
9409
|
{
|
|
9266
9410
|
number: 41,
|
|
@@ -9317,48 +9461,46 @@ var PATTERNS = [
|
|
|
9317
9461
|
// 0) \u2605 MOST COMMON: <Card> body has NO padding (content is flush against the edges)
|
|
9318
9462
|
// Cause: the bare <Card> has ZERO inner padding \u2014 it MUST contain <CardContent>.
|
|
9319
9463
|
// Don't hand-roll padding with className="p-4" on the Card either.
|
|
9320
|
-
// \u274C <Card><
|
|
9321
|
-
// \u274C <Card className="p-4">\u2026fields\u2026</Card>
|
|
9322
|
-
// \u2705 <Card><CardContent><
|
|
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>
|
|
9323
9467
|
// Titles \u2192 <CardHeader><CardTitle>. Only go flush deliberately for a full-bleed table:
|
|
9324
9468
|
// \u2705 <Card><CardContent flush><DataTable/></CardContent></Card>
|
|
9325
9469
|
// GENERAL RULE \u2014 compose godx-ui primitives FULLY; never hand-roll what one ships:
|
|
9326
9470
|
// 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
|
|
9328
|
-
//
|
|
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.
|
|
9329
9473
|
|
|
9330
9474
|
// 1) StatCard shows a DOUBLE border (too thick)
|
|
9331
9475
|
// Cause: StatCard IS already a bordered Card. Don't wrap it.
|
|
9332
9476
|
// \u274C <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>
|
|
9333
9477
|
// \u2705 <ResponsiveGrid columns={4}><StatCard label="x" value="1" /></ResponsiveGrid>
|
|
9334
9478
|
// Need a section title? Use a heading, NOT a Card:
|
|
9335
|
-
// \u2705 <
|
|
9336
|
-
// <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></
|
|
9479
|
+
// \u2705 <Flex direction="col" gap="sm"><div className="text-sm font-medium">KPI</div>
|
|
9480
|
+
// <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Flex>
|
|
9337
9481
|
|
|
9338
9482
|
// 2) Badge renders grey with a \u25CB (no colour) for localized/tier labels
|
|
9339
|
-
// Cause: it auto-maps only English lifecycle keys.
|
|
9483
|
+
// Cause: it auto-maps only English lifecycle keys.
|
|
9340
9484
|
// \u274C <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" />
|
|
9341
9485
|
// \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>
|
|
9486
|
+
// \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
|
|
9343
9487
|
|
|
9344
9488
|
// 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
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.)
|
|
9348
9491
|
|
|
9349
9492
|
// 4) Empty (icon/action) column header shows a blank grey block
|
|
9350
|
-
//
|
|
9351
|
-
// \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.
|
|
9352
9494
|
|
|
9353
9495
|
// 5) DataTable columns are crushed / squeezed
|
|
9354
9496
|
// Cause: the table is nested in a narrow grid column.
|
|
9355
9497
|
// \u274C <ResponsiveGrid columns={3}><div className="lg:col-span-2"><Card><DataTable/></Card></div></ResponsiveGrid>
|
|
9356
9498
|
// \u2705 Table gets its OWN full-width row: <Card><CardContent flush><DataTable/></CardContent></Card>
|
|
9357
9499
|
|
|
9358
|
-
// 6)
|
|
9500
|
+
// 6) Toolbar (filter bar) has no padding (sticks to the edge)
|
|
9359
9501
|
// Cause: it's inside CardContent flush (flush strips padding \u2014 that's for tables).
|
|
9360
|
-
// \u274C <Card><CardContent flush><
|
|
9361
|
-
// \u2705 <
|
|
9502
|
+
// \u274C <Card><CardContent flush><Toolbar/><DataTable/></CardContent></Card>
|
|
9503
|
+
// \u2705 <Toolbar/> then <Card><CardContent flush><DataTable/></CardContent></Card>
|
|
9362
9504
|
|
|
9363
9505
|
// 7) Sidebar footer looks washed-out / off-design
|
|
9364
9506
|
// Cause: raw opacity-*/text-[11px]. Use semantic tokens.
|
|
@@ -9374,7 +9516,7 @@ var PATTERNS = [
|
|
|
9374
9516
|
// Cause: Math.random()/argless new Date() during render (SSR \u2260 client).
|
|
9375
9517
|
// \u2705 seed deterministically by index, or compute in an event handler.
|
|
9376
9518
|
|
|
9377
|
-
// 10) Hide a column on mobile / sign-aware KPI delta
|
|
9519
|
+
// 10) Hide a column on mobile / sign-aware KPI delta
|
|
9378
9520
|
// \u2705 columns: [{ key: "email", header: "\u30E1\u30FC\u30EB", hiddenOnMobile: true }]
|
|
9379
9521
|
// \u2705 <StatCard label="\u58F2\u4E0A" value="\xA58.2M" delta="+12%" /> // + green / - red; inverse flips`
|
|
9380
9522
|
},
|
|
@@ -9388,7 +9530,7 @@ import { z } from "zod";
|
|
|
9388
9530
|
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@godxjp/ui/data-display";
|
|
9389
9531
|
import { FormField, Input } from "@godxjp/ui/data-entry";
|
|
9390
9532
|
import { Button } from "@godxjp/ui/general";
|
|
9391
|
-
import {
|
|
9533
|
+
import { Flex } from "@godxjp/ui/layout";
|
|
9392
9534
|
|
|
9393
9535
|
const schema = z.object({
|
|
9394
9536
|
name: z.string().min(1, "\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059"),
|
|
@@ -9406,18 +9548,19 @@ export function SignUpCard() {
|
|
|
9406
9548
|
<CardHeader><CardTitle>\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210</CardTitle></CardHeader>
|
|
9407
9549
|
<CardContent>
|
|
9408
9550
|
<form id="signup" onSubmit={onSubmit}>
|
|
9409
|
-
|
|
9551
|
+
{/* Vertical rhythm comes from Flex direction="col" gap \u2014 never space-y-*/gap-* utilities. */}
|
|
9552
|
+
<Flex direction="col" gap="md">
|
|
9410
9553
|
<FormField id="name" label="\u6C0F\u540D" required error={errors.name?.message}>
|
|
9411
9554
|
<Input id="name" {...register("name")} />
|
|
9412
9555
|
</FormField>
|
|
9413
9556
|
<FormField id="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required error={errors.email?.message}>
|
|
9414
9557
|
<Input id="email" type="email" {...register("email")} />
|
|
9415
9558
|
</FormField>
|
|
9416
|
-
</
|
|
9559
|
+
</Flex>
|
|
9417
9560
|
</form>
|
|
9418
9561
|
</CardContent>
|
|
9419
9562
|
<CardFooter separated>
|
|
9420
|
-
<Button type="submit" form="signup"
|
|
9563
|
+
<Button type="submit" form="signup" loading={isSubmitting}>\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210</Button>
|
|
9421
9564
|
</CardFooter>
|
|
9422
9565
|
</Card>
|
|
9423
9566
|
);
|
|
@@ -9425,33 +9568,97 @@ export function SignUpCard() {
|
|
|
9425
9568
|
},
|
|
9426
9569
|
{
|
|
9427
9570
|
name: "settings-page-responsive",
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
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";
|
|
9434
9584
|
import { FormField, Input } from "@godxjp/ui/data-entry";
|
|
9585
|
+
import { Button } from "@godxjp/ui/general";
|
|
9435
9586
|
|
|
9436
|
-
|
|
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() {
|
|
9437
9604
|
return (
|
|
9438
|
-
<Flex gap="lg" className="
|
|
9439
|
-
<
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
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
|
+
))}
|
|
9611
|
+
</nav>
|
|
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
|
+
))}
|
|
9443
9617
|
</nav>
|
|
9444
|
-
|
|
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>
|
|
9445
9620
|
</Flex>
|
|
9446
9621
|
);
|
|
9447
9622
|
}
|
|
9448
9623
|
|
|
9449
|
-
//
|
|
9450
|
-
//
|
|
9451
|
-
//
|
|
9452
|
-
export function
|
|
9453
|
-
|
|
9454
|
-
}
|
|
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.`
|
|
9455
9662
|
},
|
|
9456
9663
|
{
|
|
9457
9664
|
name: "confirm-destructive",
|
|
@@ -9461,7 +9668,7 @@ export function GeneralSettingsForm() {
|
|
|
9461
9668
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
|
|
9462
9669
|
import { Input } from "@godxjp/ui/data-entry";
|
|
9463
9670
|
import { Button } from "@godxjp/ui/general";
|
|
9464
|
-
import {
|
|
9671
|
+
import { Flex } from "@godxjp/ui/layout";
|
|
9465
9672
|
import { toast } from "sonner";
|
|
9466
9673
|
|
|
9467
9674
|
export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {
|
|
@@ -9473,9 +9680,9 @@ export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolea
|
|
|
9473
9680
|
<DialogTitle>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664</DialogTitle>
|
|
9474
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>
|
|
9475
9682
|
</DialogHeader>
|
|
9476
|
-
<
|
|
9683
|
+
<Flex direction="col" gap="md">
|
|
9477
9684
|
<Input value={confirm} onValueChange={(e) => setConfirm(e.target.value)} placeholder={slug} />
|
|
9478
|
-
</
|
|
9685
|
+
</Flex>
|
|
9479
9686
|
<DialogFooter>
|
|
9480
9687
|
<Button variant="outline" onClick={() => onOpenChange(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
|
|
9481
9688
|
<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 +9722,14 @@ export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
|
|
|
9515
9722
|
},
|
|
9516
9723
|
{
|
|
9517
9724
|
name: "inertia-list-page",
|
|
9518
|
-
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer +
|
|
9725
|
+
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + Toolbar + DataTable + Badge + Pagination (current primitive API).",
|
|
9519
9726
|
tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
|
|
9520
9727
|
code: `import { Head, router } from "@inertiajs/react"
|
|
9521
9728
|
import { useMemo, useState } from "react"
|
|
9522
|
-
import { PageContainer, ResponsiveGrid,
|
|
9729
|
+
import { PageContainer, ResponsiveGrid, Flex } from "@godxjp/ui/layout"
|
|
9523
9730
|
import { Card, CardContent, StatCard, DataTable, EmptyState, Badge, type ColumnDef } from "@godxjp/ui/data-display"
|
|
9524
9731
|
import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
|
|
9525
|
-
import {
|
|
9732
|
+
import { Toolbar, ToolbarGroup, Pagination } from "@godxjp/ui/navigation"
|
|
9526
9733
|
import { formatDate } from "@godxjp/ui/datetime"
|
|
9527
9734
|
import { withCrmLayout } from "@/layouts/crm-layout" // see "inertia-persistent-layout"
|
|
9528
9735
|
|
|
@@ -9553,19 +9760,20 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
9553
9760
|
return (
|
|
9554
9761
|
<>
|
|
9555
9762
|
<Head title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" />
|
|
9556
|
-
{/* RULE: every page wraps in PageContainer;
|
|
9763
|
+
{/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction="col"/ResponsiveGrid, never p-*/gap-* */}
|
|
9557
9764
|
<PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
|
|
9558
|
-
<
|
|
9765
|
+
<Flex direction="col" gap="lg">
|
|
9559
9766
|
<ResponsiveGrid columns={3}>
|
|
9560
9767
|
<StatCard label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
|
|
9561
9768
|
<StatCard label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
|
|
9562
9769
|
<StatCard label="\u4EF6\u6570" value={coupons.length} />
|
|
9563
9770
|
</ResponsiveGrid>
|
|
9564
9771
|
|
|
9565
|
-
|
|
9772
|
+
{/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}
|
|
9773
|
+
<Toolbar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
|
|
9566
9774
|
{/* SearchInput is value + onSearch(v) \u2014 NOT onChange */}
|
|
9567
9775
|
<SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />
|
|
9568
|
-
<
|
|
9776
|
+
<ToolbarGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
|
|
9569
9777
|
<Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>
|
|
9570
9778
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
9571
9779
|
<SelectContent>
|
|
@@ -9574,8 +9782,8 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
9574
9782
|
<SelectItem value="\u4E0B\u66F8\u304D">\u4E0B\u66F8\u304D</SelectItem>
|
|
9575
9783
|
</SelectContent>
|
|
9576
9784
|
</Select>
|
|
9577
|
-
</
|
|
9578
|
-
</
|
|
9785
|
+
</ToolbarGroup>
|
|
9786
|
+
</Toolbar>
|
|
9579
9787
|
|
|
9580
9788
|
<Card>
|
|
9581
9789
|
<CardContent flush>
|
|
@@ -9585,10 +9793,11 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
9585
9793
|
</CardContent>
|
|
9586
9794
|
</Card>
|
|
9587
9795
|
|
|
9796
|
+
{/* Pagination ONLY for real multi-page data \u2014 hide it while empty or single-page. */}
|
|
9588
9797
|
{filtered.length > PAGE_SIZE && (
|
|
9589
9798
|
<Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />
|
|
9590
9799
|
)}
|
|
9591
|
-
</
|
|
9800
|
+
</Flex>
|
|
9592
9801
|
</PageContainer>
|
|
9593
9802
|
</>
|
|
9594
9803
|
)
|
|
@@ -9602,7 +9811,7 @@ export default Coupons`
|
|
|
9602
9811
|
tagline: "Inertia detail page \u2014 receives {id} prop, Descriptions (compound) + StatCard + EmptyState fallback.",
|
|
9603
9812
|
tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
|
|
9604
9813
|
code: `import { Head, router } from "@inertiajs/react"
|
|
9605
|
-
import { PageContainer, ResponsiveGrid,
|
|
9814
|
+
import { PageContainer, ResponsiveGrid, Flex } from "@godxjp/ui/layout"
|
|
9606
9815
|
import { Card, CardContent, StatCard, EmptyState, Descriptions, Badge } from "@godxjp/ui/data-display"
|
|
9607
9816
|
import { Button } from "@godxjp/ui/general"
|
|
9608
9817
|
import { formatDate } from "@godxjp/ui/datetime"
|
|
@@ -9630,7 +9839,7 @@ function MemberShow({ id }: { id: string }) {
|
|
|
9630
9839
|
<>
|
|
9631
9840
|
<Head title={member.name} />
|
|
9632
9841
|
<PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
|
|
9633
|
-
<
|
|
9842
|
+
<Flex direction="col" gap="lg">
|
|
9634
9843
|
<ResponsiveGrid columns={4}>
|
|
9635
9844
|
<StatCard label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
|
|
9636
9845
|
<StatCard label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
|
|
@@ -9648,7 +9857,7 @@ function MemberShow({ id }: { id: string }) {
|
|
|
9648
9857
|
</Descriptions>
|
|
9649
9858
|
</CardContent>
|
|
9650
9859
|
</Card>
|
|
9651
|
-
</
|
|
9860
|
+
</Flex>
|
|
9652
9861
|
</PageContainer>
|
|
9653
9862
|
</>
|
|
9654
9863
|
)
|
|
@@ -9692,7 +9901,7 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
|
|
|
9692
9901
|
},
|
|
9693
9902
|
{
|
|
9694
9903
|
name: "badge-coloring",
|
|
9695
|
-
tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props
|
|
9904
|
+
tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props).",
|
|
9696
9905
|
tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
|
|
9697
9906
|
code: `import { Badge } from "@godxjp/ui/data-display"
|
|
9698
9907
|
|
|
@@ -9701,7 +9910,7 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
|
|
|
9701
9910
|
// scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
|
|
9702
9911
|
// Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
|
|
9703
9912
|
|
|
9704
|
-
// 1) Lifecycle with localized text \u2014 map to the key, keep JP via
|
|
9913
|
+
// 1) Lifecycle with localized text \u2014 map to the key, keep JP via children (icon stays):
|
|
9705
9914
|
<Badge status="active">\u516C\u958B\u4E2D</Badge> // green \u2713 \u516C\u958B\u4E2D
|
|
9706
9915
|
|
|
9707
9916
|
// 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
|
|
@@ -9712,78 +9921,348 @@ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; retur
|
|
|
9712
9921
|
<Badge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
|
|
9713
9922
|
<Badge status="\u6CD5\u4EBA\u5171\u901A" tone="info" icon={null} />
|
|
9714
9923
|
|
|
9715
|
-
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type
|
|
9924
|
+
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type ToneProp)
|
|
9716
9925
|
// RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
|
|
9717
9926
|
// narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
|
|
9718
9927
|
// import that instead of the raw Badge across pages.`
|
|
9719
9928
|
},
|
|
9720
9929
|
{
|
|
9721
9930
|
name: "async-data-state",
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
import {
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
//
|
|
9738
|
-
//
|
|
9739
|
-
//
|
|
9740
|
-
//
|
|
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).`
|
|
9741
9986
|
},
|
|
9742
9987
|
{
|
|
9743
9988
|
name: "data-table-page",
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
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.`
|
|
9753
10069
|
},
|
|
9754
10070
|
{
|
|
9755
10071
|
name: "organization-memberships",
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
//
|
|
9761
|
-
//
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
{
|
|
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.`
|
|
9765
10178
|
},
|
|
9766
10179
|
{
|
|
9767
10180
|
name: "account-recovery-settings",
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
//
|
|
9773
|
-
|
|
9774
|
-
{
|
|
9775
|
-
|
|
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.`
|
|
9776
10255
|
}
|
|
9777
10256
|
];
|
|
9778
10257
|
function findPattern(name) {
|
|
9779
10258
|
const slug = name.trim().toLowerCase();
|
|
9780
|
-
return PATTERNS.find((p) => p.name === slug);
|
|
10259
|
+
return PATTERNS.find((p) => p.name === slug || p.aliases?.includes(slug));
|
|
9781
10260
|
}
|
|
9782
10261
|
function searchPatterns(query) {
|
|
9783
10262
|
const q = query.trim().toLowerCase();
|
|
9784
10263
|
if (q === "") return PATTERNS;
|
|
9785
10264
|
return PATTERNS.filter(
|
|
9786
|
-
(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))
|
|
9787
10266
|
);
|
|
9788
10267
|
}
|
|
9789
10268
|
|
|
@@ -12060,8 +12539,8 @@ function visualRulesByCategory(category) {
|
|
|
12060
12539
|
// package.json
|
|
12061
12540
|
var package_default = {
|
|
12062
12541
|
name: "@godxjp/ui-mcp",
|
|
12063
|
-
version: "
|
|
12064
|
-
godxUiCompatibility: "
|
|
12542
|
+
version: "18.0.0",
|
|
12543
|
+
godxUiCompatibility: "18.0.x",
|
|
12065
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).",
|
|
12066
12545
|
type: "module",
|
|
12067
12546
|
main: "./dist/index.js",
|
|
@@ -12149,7 +12628,7 @@ var TOOL_DEFINITIONS = [
|
|
|
12149
12628
|
},
|
|
12150
12629
|
{
|
|
12151
12630
|
name: "list_patterns",
|
|
12152
|
-
description: "List every canonical code 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`.",
|
|
12153
12632
|
inputSchema: { type: "object", properties: {} }
|
|
12154
12633
|
},
|
|
12155
12634
|
{
|
|
@@ -13159,7 +13638,12 @@ function suggestPrimitive(useCase) {
|
|
|
13159
13638
|
"Select=discrete options, AutoComplete=free-text+suggestions.",
|
|
13160
13639
|
3
|
|
13161
13640
|
);
|
|
13162
|
-
check(
|
|
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
|
+
);
|
|
13163
13647
|
check(
|
|
13164
13648
|
["delete", "destructive"],
|
|
13165
13649
|
"Pattern 'confirm-destructive'",
|