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