@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 +613 -124
- 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.",
|
|
@@ -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
|
-
{
|
|
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:
|
|
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: ["
|
|
7834
|
-
notes: "
|
|
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:
|
|
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><
|
|
9321
|
-
// \u274C <Card className="p-4">\u2026fields\u2026</Card>
|
|
9322
|
-
// \u2705 <Card><CardContent><
|
|
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
|
|
9328
|
-
//
|
|
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 <
|
|
9336
|
-
// <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></
|
|
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.
|
|
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>
|
|
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
|
-
//
|
|
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)
|
|
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><
|
|
9361
|
-
// \u2705 <
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
</
|
|
9564
|
+
</Flex>
|
|
9417
9565
|
</form>
|
|
9418
9566
|
</CardContent>
|
|
9419
9567
|
<CardFooter separated>
|
|
9420
|
-
<Button type="submit" form="signup"
|
|
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
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
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
|
-
|
|
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="
|
|
9439
|
-
<
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
9450
|
-
//
|
|
9451
|
-
//
|
|
9452
|
-
export function
|
|
9453
|
-
|
|
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 {
|
|
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
|
-
<
|
|
9688
|
+
<Flex direction="col" gap="md">
|
|
9477
9689
|
<Input value={confirm} onValueChange={(e) => setConfirm(e.target.value)} placeholder={slug} />
|
|
9478
|
-
</
|
|
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 +
|
|
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,
|
|
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 {
|
|
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;
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
9578
|
-
</
|
|
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
|
-
</
|
|
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,
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
import {
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
//
|
|
9738
|
-
//
|
|
9739
|
-
//
|
|
9740
|
-
//
|
|
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
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
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
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
//
|
|
9761
|
-
//
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
{
|
|
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
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
//
|
|
9773
|
-
|
|
9774
|
-
{
|
|
9775
|
-
|
|
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: "
|
|
12064
|
-
godxUiCompatibility: "
|
|
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 (
|
|
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(
|
|
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'",
|