@godxjp/ui-mcp 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -49
- package/dist/index.js +605 -1522
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -62,26 +62,26 @@ var COMPONENTS = [
|
|
|
62
62
|
usage: [
|
|
63
63
|
"DO: Always wrap every page's content in PageContainer \u2014 it is the mandatory page shell. Pass `title` (required, rendered as `<h1>`) for every page; omitting it leaves the page without an accessible heading.",
|
|
64
64
|
"DO: Use the `extra` prop (not a sibling div, not a wrapper) for action buttons or controls that sit right of the title row \u2014 e.g. `extra={<Button>\u65B0\u898F\u4F5C\u6210</Button>}`. Use the `footer` prop for a pinned action bar below the body (e.g. Save/Cancel on a form page); combine with `stickyFooter` to pin it to the viewport bottom on scroll.",
|
|
65
|
-
"DO: Use `variant='flush'` when the page body contains a full-bleed component like DataTable. Inside a flush container, wrap any padded strips (
|
|
65
|
+
"DO: Use `variant='flush'` when the page body contains a full-bleed component like DataTable. Inside a flush container, wrap any padded strips (Toolbar, intro text) in `<PageContainer.Inset>` to align them with the header. Never add manual `px-*` or `p-*` padding to compensate \u2014 use PageContainer.Inset.",
|
|
66
66
|
"DO: Pass `breadcrumb` as an ordered array of `{ label, to? }` objects from root to current page. The last item is automatically rendered without a link and receives `aria-current='page'`; earlier items with `to` become router `<Link>` elements. Never hand-roll a breadcrumb nav inside a PageContainer.",
|
|
67
67
|
"DON'T: Use `density` to change individual control sizes \u2014 it cascades spacing across the entire page subtree. Set it once per page (e.g. `density='compact'` for data-dense list pages) and let all child components inherit it. Do not apply density classes manually.",
|
|
68
|
-
"DON'T: Confuse PageContainer's prop names with the
|
|
68
|
+
"DON'T: Confuse PageContainer's prop names with the old PageHeader's prop names \u2014 PageContainer uses `subtitle` (not `description`) and `extra` (not `actions`). If you see those legacy names in old code, migrate them to PageContainer."
|
|
69
69
|
],
|
|
70
70
|
useCases: [
|
|
71
|
-
"A master list page (e.g. invoices, journal entries, customers) where the header holds the page title, a 'New Invoice' button in `extra`, a breadcrumb trail, and a full-bleed DataTable as the body \u2014 use `variant='flush'` + `<
|
|
72
|
-
"A detail / edit form page where the footer holds Save and Cancel buttons \u2014 use `footer={<
|
|
71
|
+
"A master list page (e.g. invoices, journal entries, customers) where the header holds the page title, a 'New Invoice' button in `extra`, a breadcrumb trail, and a full-bleed DataTable as the body \u2014 use `variant='flush'` + `<PageContainer.Inset>` for the Toolbar above the table.",
|
|
72
|
+
"A detail / edit form page where the footer holds Save and Cancel buttons \u2014 use `footer={<Flex direction='row'><Button>\u4FDD\u5B58</Button><Button variant='outline'>\u30AD\u30E3\u30F3\u30BB\u30EB</Button></Flex>}` with `stickyFooter` so the actions remain reachable as the form scrolls.",
|
|
73
73
|
"A settings or narrow-form page (e.g. account profile, entity configuration) where `variant='narrow'` constrains content to a readable column width and `stickyFooter` pins the submit bar.",
|
|
74
|
-
"A dashboard page with KPI cards and chart sections \u2014 use `variant='default'` with `children={<
|
|
74
|
+
"A dashboard page with KPI cards and chart sections \u2014 use `variant='default'` with `children={<Flex direction='col' gap='lg'>\u2026</Flex>}` to vertically stack multiple Card/StatCard sections beneath the page title.",
|
|
75
75
|
"Any deep-nav page in a multi-level admin (e.g. Accounting > Ledger > Journal Entry #42) where a 3-segment breadcrumb trail provides back-navigation without browser history dependence.",
|
|
76
|
-
"A high-density data reconciliation page where an analyst needs to see maximum rows \u2014 use `density='compact'` to tighten all spacing across the DataTable,
|
|
76
|
+
"A high-density data reconciliation page where an analyst needs to see maximum rows \u2014 use `density='compact'` to tighten all spacing across the DataTable, Toolbar, and controls in a single prop."
|
|
77
77
|
],
|
|
78
78
|
related: [
|
|
79
|
-
"
|
|
80
|
-
"
|
|
79
|
+
"PageContainer.Inset \u2014 use INSIDE a `variant='flush'` PageContainer to re-introduce horizontal padding for strips like Toolbar or intro text that should align with the page header, while the surrounding DataTable stays full-bleed. Not a standalone page shell.",
|
|
80
|
+
"PageContainer \u2014 always use PageContainer for new pages; it supports `children`, `footer`, `variant`, `density`, and `stickyFooter`. Legacy code using the old prop names (`description` \u2192 `subtitle`, `actions` \u2192 `extra`) should be migrated to PageContainer.",
|
|
81
81
|
"AppShell \u2014 the outer shell that owns the sidebar/topbar layout grid; PageContainer lives inside AppShell's `children` slot. Do not put AppShell inside PageContainer \u2014 the nesting order is AppShell \u2192 PageContainer.",
|
|
82
82
|
"SplitPane \u2014 use instead of PageContainer when the page body needs a fixed-width aside panel alongside main content (e.g. a detail drawer next to a list). PageContainer has no aside slot; SplitPane fills that gap and can itself be placed inside PageContainer's children."
|
|
83
83
|
],
|
|
84
|
-
example: `import { PageContainer,
|
|
84
|
+
example: `import { PageContainer, Flex } from "@godxjp/ui/layout";
|
|
85
85
|
import { Button } from "@godxjp/ui/general";
|
|
86
86
|
|
|
87
87
|
export default function OrdersPage() {
|
|
@@ -92,105 +92,13 @@ export default function OrdersPage() {
|
|
|
92
92
|
breadcrumb={[{ label: "\u30DB\u30FC\u30E0", to: "/" }, { label: "\u6CE8\u6587\u4E00\u89A7" }]}
|
|
93
93
|
extra={<Button>\u65B0\u898F\u6CE8\u6587</Button>}
|
|
94
94
|
>
|
|
95
|
-
<
|
|
95
|
+
<Flex direction="col" gap="lg">{/* page content */}</Flex>
|
|
96
96
|
</PageContainer>
|
|
97
97
|
);
|
|
98
98
|
}`,
|
|
99
99
|
storyPath: "layout/PageContainer.stories.tsx",
|
|
100
100
|
rules: [23]
|
|
101
101
|
},
|
|
102
|
-
{
|
|
103
|
-
name: "Stack",
|
|
104
|
-
group: "layout",
|
|
105
|
-
tagline: "Vertical flex column with token gap \u2014 the default block-stacking primitive (use instead of space-y-*).",
|
|
106
|
-
props: [
|
|
107
|
-
{
|
|
108
|
-
name: "gap",
|
|
109
|
-
type: '"xs" | "sm" | "md" | "lg" | "xl"',
|
|
110
|
-
defaultValue: '"md"',
|
|
111
|
-
description: "Vertical space between children (design tokens)."
|
|
112
|
-
},
|
|
113
|
-
{ name: "className", type: "string", description: "Extra classes merged via cn()." },
|
|
114
|
-
{ name: "children", type: "ReactNode", description: "Block-level children to stack." }
|
|
115
|
-
],
|
|
116
|
-
usage: [
|
|
117
|
-
'DO use `<Stack gap="lg">` (or sm/md/xl/xs) to separate major page sections \u2014 KPI row, FilterBar, table card \u2014 instead of writing `space-y-*` or `flex flex-col gap-*` utilities on a raw div. Token gap values (`xs`|`sm`|`md`|`lg`|`xl`) map to CSS custom properties; raw Tailwind spacing utilities (`space-y-4`) are forbidden on page layouts (rule 40).',
|
|
118
|
-
"DO pass `className` only for structural overrides (e.g., `w-full`, `min-h-0`) \u2014 NEVER to smuggle spacing utilities like `p-4` or `gap-6` that duplicate what the `gap` prop already does. Every spacing value must trace back to a design token.",
|
|
119
|
-
'DO NOT use Stack for horizontal arrangements \u2014 that is `Inline`. Stack is vertical only (`flex-col`). Mixing `className="flex-row"` on a Stack to force horizontal is wrong; use `Inline` instead.',
|
|
120
|
-
'DO NOT nest multiple bare Stack wrappers just to get different gaps \u2014 compose them: a top-level `<Stack gap="xl">` for page sections, an inner `<Stack gap="sm">` for tightly-related fields. Each level should correspond to a real semantic grouping.',
|
|
121
|
-
'DO import from `@godxjp/ui/layout` (not the root `@godxjp/ui` barrel) \u2014 `import { Stack } from "@godxjp/ui/layout"`.',
|
|
122
|
-
"Stack is a plain `<div>` with `React.HTMLAttributes<HTMLDivElement>` \u2014 all standard HTML div props (`data-*`, `id`, `aria-*`, `role`) pass through. There is no controlled/uncontrolled state and no form submission role; use `FormField` for form layout concerns."
|
|
123
|
-
],
|
|
124
|
-
useCases: [
|
|
125
|
-
'Top-level page body: wrap KPI `ResponsiveGrid`, standalone `FilterBar`, and a table `Card` in a `<Stack gap="lg">` \u2014 this is the canonical inertia-list-page structure (rule 38 + rule 40).',
|
|
126
|
-
"Detail/form pages: stack a `PageHeader` (or `PageContainer` header), a facts `Card` with `Descriptions`, and an action `Card` with `FormField` rows, each separated by a semantically meaningful gap level.",
|
|
127
|
-
'Card body with multiple related fields: use `<Stack gap="sm">` inside `<CardContent>` to separate labelled input rows without resorting to `space-y-2` utilities.',
|
|
128
|
-
'Dashboard shells: `<Stack gap="xl">` as the page root, containing a `<ResponsiveGrid columns={4}>` of `StatCard` items followed by chart cards and a table card \u2014 each row is a Stack child.',
|
|
129
|
-
'Modal/Sheet interiors: `<Stack gap="md">` inside `<Dialog>` or `<Sheet>` content area to separate sections (description, form, preview) with consistent token spacing.',
|
|
130
|
-
'Empty/loading placeholder layout: wrap `<SkeletonTable>` or `<DataState>` in a `<Stack gap="md">` alongside a header block so the skeleton occupies the same vertical rhythm as the loaded page.'
|
|
131
|
-
],
|
|
132
|
-
related: [
|
|
133
|
-
"Inline \u2014 horizontal counterpart; use for button groups, icon+label rows, badge clusters. When children should sit side-by-side, use Inline. When they should stack top-to-bottom, use Stack. Never use Stack with a flex-row className override.",
|
|
134
|
-
"ResponsiveGrid \u2014 use when children are card-shaped items that should tile into 2/3/4 columns on desktop and collapse to 1 column on mobile (e.g., StatCard KPI rows). Stack does not do multi-column layouts.",
|
|
135
|
-
"PageContainer \u2014 outer page wrapper that provides title, breadcrumb, and padding context. Stack is the layout primitive used INSIDE PageContainer's children area, not a replacement for it.",
|
|
136
|
-
"CardContent \u2014 for spacing inside a Card, always use CardContent (which adds consistent padding); don't wrap Card body in a bare Stack with padding classes. A Stack inside CardContent is correct when you need multiple vertically spaced sections within the card body."
|
|
137
|
-
],
|
|
138
|
-
example: `import { Stack } from "@godxjp/ui/layout";
|
|
139
|
-
|
|
140
|
-
<Stack gap="lg">
|
|
141
|
-
<KpiRow />
|
|
142
|
-
<FilterBarBlock />
|
|
143
|
-
<TableCard />
|
|
144
|
-
</Stack>`,
|
|
145
|
-
storyPath: "layout/Stack.stories.tsx",
|
|
146
|
-
rules: [2, 40]
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: "Inline",
|
|
150
|
-
group: "layout",
|
|
151
|
-
tagline: "Horizontal flex row with token gap \u2014 the default inline/row arrangement (use instead of gap-* on a flex div).",
|
|
152
|
-
props: [
|
|
153
|
-
{
|
|
154
|
-
name: "gap",
|
|
155
|
-
type: '"xs" | "sm" | "md" | "lg"',
|
|
156
|
-
defaultValue: '"sm"',
|
|
157
|
-
description: "Horizontal space between children."
|
|
158
|
-
},
|
|
159
|
-
{ name: "className", type: "string", description: "Extra classes merged via cn()." },
|
|
160
|
-
{ name: "children", type: "ReactNode", description: "Inline children in a row." }
|
|
161
|
-
],
|
|
162
|
-
usage: [
|
|
163
|
-
'DO use `<Inline gap="sm">` instead of hand-rolling `flex gap-2 items-center` \u2014 the component bakes in `display:flex; flex-wrap:wrap; align-items:center` via the token class `ui-inline-*`, so you get consistent spacing and wrapping behavior without raw Tailwind utilities (rule 2).',
|
|
164
|
-
'DO pass extra layout modifiers through `className` (e.g. `className="justify-between"` to push items to opposite ends, or `className="flex-nowrap"` to prevent wrapping when overflow must be clipped) \u2014 `Inline` spreads all HTML div attributes, so className is merged via `cn()`.',
|
|
165
|
-
"DON'T use `Inline` for vertical stacking \u2014 it is always a row. For vertical spacing between block elements use `Stack` instead. The most common mistake is reaching for `Inline` when the children should stack, or reaching for `Stack` when the children should sit side-by-side.",
|
|
166
|
-
"DON'T nest an `Inline` just to group icon + label inside a `Button` \u2014 Button already handles its own internal row layout. Use `Inline` at the call-site level to space multiple sibling Buttons or controls apart.",
|
|
167
|
-
'DO use `gap="xs"` for tight icon+label pairs (e.g. flag + country name in CountrySelect) and `gap="sm"` (default) for button groups and toolbar-level groupings. `gap="md"` / `gap="lg"` suit section-header clusters with more breathing room.',
|
|
168
|
-
"NOTE: `Inline` has no alignment, justify, or wrap props beyond `gap` \u2014 any extra layout intent must come through `className`. There is no controlled/form-submission aspect; it is a pure layout shell with no ARIA role of its own."
|
|
169
|
-
],
|
|
170
|
-
useCases: [
|
|
171
|
-
"Action toolbar: grouping a primary Button and a secondary outline Button side-by-side at the bottom of a form or dialog (the catalog example shows exactly this).",
|
|
172
|
-
"Table/list toolbar: placing a `SearchInput` next to a `FilterBar` trigger or a `DateRangePicker` at the top of a DataTable page, so controls sit in a wrapping row with consistent gap.",
|
|
173
|
-
"Status chip row: rendering a cluster of `Badge` or `Badge` components inline (e.g. invoice tags: Paid + Overdue + Draft) without writing ad-hoc flex classes.",
|
|
174
|
-
'Icon + text label pair: wrapping a flag icon and country name in `CountrySelect`, or a lucide icon and a span, keeping them vertically centered with a tight `gap="xs"`.',
|
|
175
|
-
'Card header actions: grouping a `Button variant="ghost"` edit action and a `DropdownMenu` trigger on the right side of a `CardHeader`, using `className="justify-end"` on the Inline.',
|
|
176
|
-
'Alert action row: pairing two action links/buttons inside an `Alert` body (the godx-ui Alert component itself uses `<Inline gap="xs">` internally for its action cluster).'
|
|
177
|
-
],
|
|
178
|
-
related: [
|
|
179
|
-
"Stack \u2014 use Stack when children should be arranged vertically (column direction). Inline is horizontal/row; Stack is vertical/column. They are the two axis-specific layout primitives and are often composed together (Stack of Inlines).",
|
|
180
|
-
"PageInset \u2014 use PageInset when you need a padded horizontal strip inside a flush PageContainer (e.g. for FilterBar or intro text above a full-bleed DataTable). PageInset adds top/side padding to a section; Inline only arranges children in a row with a gap.",
|
|
181
|
-
"FilterBar \u2014 use FilterBar (with FilterGroup) when the horizontal group of controls is a page-level filter row that semantically belongs together and may include responsive collapse behaviour. Use Inline for ad-hoc groupings of buttons or badges that don't need filter semantics.",
|
|
182
|
-
"ResponsiveGrid \u2014 use ResponsiveGrid when you need a multi-column grid of cards that collapses on mobile. Use Inline when children must stay in a single wrapping row at all breakpoints without explicit column counts."
|
|
183
|
-
],
|
|
184
|
-
example: `import { Inline } from "@godxjp/ui/layout";
|
|
185
|
-
import { Button } from "@godxjp/ui/general";
|
|
186
|
-
|
|
187
|
-
<Inline gap="sm">
|
|
188
|
-
<Button>\u4FDD\u5B58</Button>
|
|
189
|
-
<Button variant="outline">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
|
|
190
|
-
</Inline>`,
|
|
191
|
-
storyPath: "layout/Inline.stories.tsx",
|
|
192
|
-
rules: [2]
|
|
193
|
-
},
|
|
194
102
|
{
|
|
195
103
|
name: "Flex",
|
|
196
104
|
group: "layout",
|
|
@@ -206,7 +114,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
206
114
|
name: "gap",
|
|
207
115
|
type: '"xs" | "sm" | "md" | "lg" | "xl"',
|
|
208
116
|
defaultValue: '"md"',
|
|
209
|
-
description: "Token gap between children, shared with
|
|
117
|
+
description: "Token gap between children, shared with other layout primitives."
|
|
210
118
|
},
|
|
211
119
|
{
|
|
212
120
|
name: "align",
|
|
@@ -227,9 +135,9 @@ import { Button } from "@godxjp/ui/general";
|
|
|
227
135
|
],
|
|
228
136
|
usage: [
|
|
229
137
|
'DO import from `@godxjp/ui/layout` and reach for Flex when the axis, alignment, justification, or wrap behavior is part of the component contract: `import { Flex } from "@godxjp/ui/layout"`.',
|
|
230
|
-
"DO keep spacing on the `gap` prop instead of raw `gap-*`, `space-*`, or padding utilities. Flex uses the same token scale as
|
|
231
|
-
'DO use `direction="row"` with `wrap` for responsive control rows, chip clusters, and action groups that need more control than
|
|
232
|
-
'DO use `direction="col"` for vertical groupings that need explicit `align` or `justify` behavior.
|
|
138
|
+
"DO keep spacing on the `gap` prop instead of raw `gap-*`, `space-*`, or padding utilities. Flex uses the same token scale as other layout primitives, so spacing remains tied to the design system.",
|
|
139
|
+
'DO use `direction="row"` with `wrap` for responsive control rows, chip clusters, and action groups that need more control than simple row composition.',
|
|
140
|
+
'DO use `direction="col"` for vertical groupings that need explicit `align` or `justify` behavior. For pure vertical stacking without alignment control, `direction="col"` is sufficient.',
|
|
233
141
|
"DON'T override the axis with `className` after choosing a direction prop. Keep the layout intent in props so catalog guidance and data attributes stay accurate.",
|
|
234
142
|
"Flex is a plain div with React.HTMLAttributes<HTMLDivElement>; pass `id`, `role`, `aria-*`, `data-*`, and structural className values as needed, but do not use it as a semantic form or button wrapper."
|
|
235
143
|
],
|
|
@@ -237,13 +145,13 @@ import { Button } from "@godxjp/ui/general";
|
|
|
237
145
|
"Toolbar internals where controls should sit in a row, wrap on narrow widths, and stay vertically centered.",
|
|
238
146
|
"Card headers that need title content on the left and actions on the right via `justify='between'` without hand-rolling flex utility classes.",
|
|
239
147
|
"Empty-state or loading blocks that center content on both axes using `align='center'` and `justify='center'`.",
|
|
240
|
-
"Form sub-sections where a vertical group needs stretched children or centered helper content beyond what
|
|
241
|
-
"Badge, chip, or tag clusters where wrapping is required but the caller also needs
|
|
242
|
-
"Low-level layout composition inside custom components where
|
|
148
|
+
"Form sub-sections where a vertical group needs stretched children or centered helper content beyond what a plain column Flex provides.",
|
|
149
|
+
"Badge, chip, or tag clusters where wrapping is required but the caller also needs explicit gap control.",
|
|
150
|
+
"Low-level layout composition inside custom components where raw flex classes would duplicate the primitive."
|
|
243
151
|
],
|
|
244
152
|
related: [
|
|
245
|
-
"
|
|
246
|
-
"
|
|
153
|
+
"Flex `direction='col'` \u2014 the standard pattern for ordinary vertical block spacing; use explicit `align`, `justify`, or `wrap` props when you need more control.",
|
|
154
|
+
"Flex `direction='row'` \u2014 the standard pattern for simple horizontal groups; add `wrap` and `align='center'` for the typical row with wrapped centered items.",
|
|
247
155
|
"ResponsiveGrid \u2014 use for equal-width, multi-column tile layouts. Flex arranges children on one flex axis and does not provide column-count behavior.",
|
|
248
156
|
"PageContainer \u2014 page scaffold and padding context. Flex is an inner layout primitive used inside page sections, cards, dialogs, and toolbars."
|
|
249
157
|
],
|
|
@@ -283,22 +191,22 @@ import { Button } from "@godxjp/ui/general";
|
|
|
283
191
|
"DO use columns={2|3|4} to declare the target desktop column count \u2014 the grid collapses automatically to 1 column on narrow containers (mobile-first via CSS container queries), via 2-column intermediate at \u2265640px, then full target count at \u22651024px. There is no 'columns={1}' \u2014 omit the grid for single-column flows.",
|
|
284
192
|
"DO NOT place a DataTable inside a ResponsiveGrid column beside a card or chart. DataTable must occupy its own full-width row in a Card with CardContent flush. Nesting a multi-column table in a grid column squeezes CJK text to one character per line (see rule 37).",
|
|
285
193
|
"DO use ResponsiveGrid for page-level spacing \u2014 it applies the correct gap token (--space-stack-md) automatically. Never add raw gap-* / p-* / space-* utilities to the page layout around tiles; compose spacing through this component instead (rule 40).",
|
|
286
|
-
"DO render
|
|
194
|
+
"DO render SkeletonStat children in place of StatCard tiles while KPIs are loading \u2014 same columns prop, same count as the real tiles. Switch to real StatCard once data resolves.",
|
|
287
195
|
"The grid uses CSS container queries, not viewport media queries \u2014 it responds to its containing block width, not the window. Ensure the container is not artificially constrained (e.g. inside a narrow SplitPane column) or column expansion will never trigger."
|
|
288
196
|
],
|
|
289
197
|
useCases: [
|
|
290
198
|
"Dashboard KPI row: rendering 3\u20134 StatCard tiles (revenue, member count, active invoices, overdue amount) that reflow to a 2-column stacked grid on tablet and a single column on mobile.",
|
|
291
|
-
"Summary header above a list page: a 2-column grid of two StatCard totals (e.g. total payable vs total paid) sitting above a
|
|
199
|
+
"Summary header above a list page: a 2-column grid of two StatCard totals (e.g. total payable vs total paid) sitting above a Toolbar and DataTable.",
|
|
292
200
|
"Accounting period overview: 4 StatCard tiles (opening balance, total debits, total credits, closing balance) that collapse gracefully on narrow viewports without any custom CSS.",
|
|
293
|
-
"Loading state for a KPI row: identical <ResponsiveGrid columns={4}> wrapping four <
|
|
201
|
+
"Loading state for a KPI row: identical <ResponsiveGrid columns={4}> wrapping four <SkeletonStat /> placeholders rendered while async data is in flight, swapped for real StatCard tiles once resolved.",
|
|
294
202
|
"Settings or profile summary cards: 2- or 3-column grid of Card+CardContent blocks (not StatCard) showing categorized read-only data groups before a detail form below.",
|
|
295
203
|
"Entity comparison panel: a columns={3} grid comparing three legal entities side-by-side with a Card+CardContent per entity, which collapses to 2-up on tablet and stacks on mobile."
|
|
296
204
|
],
|
|
297
205
|
related: [
|
|
298
|
-
"
|
|
206
|
+
"Flex \u2014 use Flex (direction col or row) for sequential blocks of mixed-width content (forms, description lists, button rows). Use ResponsiveGrid only when you want equal-width, auto-reflowing tile columns.",
|
|
299
207
|
"SplitPane \u2014 use SplitPane for a fixed two-panel side-by-side layout with a defined primary/secondary ratio that does NOT collapse to stacked tiles. Use ResponsiveGrid when you want automatic column count collapse on narrow screens.",
|
|
300
208
|
"StatCard \u2014 the canonical direct child of ResponsiveGrid for KPI tiles. StatCard is self-contained (draws its own bordered card); never wrap it in Card/CardContent when placing it inside ResponsiveGrid.",
|
|
301
|
-
"
|
|
209
|
+
"SkeletonStat \u2014 the loading-state sibling of StatCard, used as a drop-in placeholder child of ResponsiveGrid with the same columns count while KPI data is in flight."
|
|
302
210
|
],
|
|
303
211
|
example: `import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
304
212
|
import { StatCard } from "@godxjp/ui/data-display";
|
|
@@ -367,7 +275,7 @@ import { StatCard } from "@godxjp/ui/data-display";
|
|
|
367
275
|
"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.",
|
|
368
276
|
"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>.",
|
|
369
277
|
"DO NOT nest a second AppShell or AppShell inside AppShell's children \u2014 AppShell renders the root `app-root` div; nesting shells breaks the CSS grid layout.",
|
|
370
|
-
"DO NOT add padding directly to children expecting it to reach the viewport edge \u2014 AppShell's `<main>` is a scroll container; use <PageContainer> (or <
|
|
278
|
+
"DO NOT add padding directly to children expecting it to reach the viewport edge \u2014 AppShell's `<main>` is a scroll container; use <PageContainer> (or <PageContainer.Inset> inside a flush PageContainer) inside children to get standard page padding."
|
|
371
279
|
],
|
|
372
280
|
useCases: [
|
|
373
281
|
"Full admin SPA shell: AppShell wraps a <Sidebar> nav rail and a <Topbar> (with productMenu entity-switcher, onSearchOpen, onNotificationsOpen, user avatar) and every Inertia page renders as children inside a <PageContainer>.",
|
|
@@ -704,49 +612,6 @@ function MyShell({ children }: { content: React.ReactNode }) {
|
|
|
704
612
|
storyPath: "layout/Topbar.stories.tsx",
|
|
705
613
|
rules: [2, 3, 5, 6]
|
|
706
614
|
},
|
|
707
|
-
{
|
|
708
|
-
name: "PageInset",
|
|
709
|
-
group: "layout",
|
|
710
|
-
tagline: 'Padded horizontal strip aligned with the page header \u2014 use inside variant="flush" for filter bars / intros.',
|
|
711
|
-
props: [
|
|
712
|
-
{
|
|
713
|
-
name: "children",
|
|
714
|
-
type: "ReactNode",
|
|
715
|
-
description: "Content rendered with standard page horizontal padding."
|
|
716
|
-
},
|
|
717
|
-
{ name: "className", type: "string", description: "Extra classes." }
|
|
718
|
-
],
|
|
719
|
-
usage: [
|
|
720
|
-
'DO use PageInset exclusively inside a `<PageContainer variant="flush">` \u2014 flush strips the left/right padding from the container body, and PageInset re-applies the exact same `--space-page-active-x` token so content aligns pixel-for-pixel with the page header and footer. Outside of a flush container it doubles the padding.',
|
|
721
|
-
"DO NOT use PageInset to add padding inside a standard (non-flush) PageContainer \u2014 the container body already has `--space-page-active-x` padding on both sides; wrapping content in PageInset produces doubled inset.",
|
|
722
|
-
'DO NOT hand-roll the padding with `className="px-6"` or `className="px-page"` \u2014 those values are not guaranteed to match the design token. PageInset is the only correct way to reproduce the page-header alignment inside a flush body.',
|
|
723
|
-
"DO place PageInset as a direct child of PageContainer's body (before or between full-bleed children such as DataTable or Table) \u2014 it is a plain `<div>` that wraps one cohesive strip (e.g., a FilterBar, a summary intro, or a quick-action toolbar) and does not provide vertical spacing itself.",
|
|
724
|
-
"DO accept all standard HTMLDivElement attributes (id, aria-*, data-*, style) via the spread \u2014 PageInsetProp extends React.HTMLAttributes<HTMLDivElement>, so accessibility attributes and test hooks pass through without extra wrappers.",
|
|
725
|
-
"COMMON MISTAKE: adding a PageInset around a DataTable inside a flush PageContainer \u2014 DataTable must remain full-bleed (no PageInset) so its borders reach the edges; only the filter/intro strips above or below the table need PageInset."
|
|
726
|
-
],
|
|
727
|
-
useCases: [
|
|
728
|
-
"Flush list page with a FilterBar above a full-bleed DataTable: wrap the FilterBar (or FilterBar + FilterGroup) in PageInset so it aligns with the page title, while DataTable sits edge-to-edge beneath it.",
|
|
729
|
-
"Invoice or transaction index where a SearchInput + date range picker toolbar sits above a borderless table \u2014 PageInset keeps the toolbar inset-aligned without breaking the table's flush left/right edges.",
|
|
730
|
-
"Dashboard section inside a flush PageContainer that shows a brief intro paragraph or status summary strip before a full-width chart or DataTable.",
|
|
731
|
-
"Multi-section flush page where two or more padded action bars (bulk-action toolbar, pagination row) appear between full-bleed tables \u2014 each strip gets its own PageInset.",
|
|
732
|
-
'Settings or form pages using variant="flush" where a prominent alert or MutationFeedback banner must align with the form fields rendered in a padded section below.',
|
|
733
|
-
"Accounting detail pages (e.g., journal entry list) where a summary Descriptions strip needs the same left-edge as the page header while the entry rows below are full-bleed."
|
|
734
|
-
],
|
|
735
|
-
related: [
|
|
736
|
-
'PageContainer \u2014 the required parent; PageInset only makes sense as a child of PageContainer variant="flush". Use PageContainer for all other padding needs (default variant already provides horizontal padding everywhere).',
|
|
737
|
-
"FilterBar \u2014 the most common direct child of PageInset; FilterBar itself has no page-level padding, so always wrap it in PageInset when inside a flush PageContainer.",
|
|
738
|
-
'CardContent \u2014 serves a similar "add-the-missing-padding" role but inside a Card, not a flush PageContainer. Use CardContent to pad content inside a Card; use PageInset to pad content inside a flush page body.',
|
|
739
|
-
"Stack \u2014 a vertical-spacing primitive that can wrap multiple PageInset strips but has no padding of its own; Stack and PageInset compose orthogonally (Stack for gaps, PageInset for horizontal alignment)."
|
|
740
|
-
],
|
|
741
|
-
example: `import { PageContainer, PageInset } from "@godxjp/ui/layout";
|
|
742
|
-
|
|
743
|
-
<PageContainer title="\u5546\u54C1\u4E00\u89A7" variant="flush">
|
|
744
|
-
<PageInset><FilterBarBlock /></PageInset>
|
|
745
|
-
{/* full-bleed table below */}
|
|
746
|
-
</PageContainer>`,
|
|
747
|
-
storyPath: "layout/PageInset.stories.tsx",
|
|
748
|
-
rules: []
|
|
749
|
-
},
|
|
750
615
|
{
|
|
751
616
|
name: "SplitPane",
|
|
752
617
|
group: "layout",
|
|
@@ -769,7 +634,7 @@ function MyShell({ children }: { content: React.ReactNode }) {
|
|
|
769
634
|
usage: [
|
|
770
635
|
"DO: pass all right-panel content via the `aside` prop \u2014 it renders inside a semantic `<aside>` element at a fixed rem width (sm=20rem, md=22rem). The `children` prop fills the main `1fr` column. Both accept any ReactNode.",
|
|
771
636
|
'DO: choose `asideWidth="sm"` for compact detail panels (filters, quick stats, key-value summaries) and the default `asideWidth="md"` for richer panels (forms, timelines, long metadata lists).',
|
|
772
|
-
"DO: wrap SplitPane inside `PageContainer` or `
|
|
637
|
+
"DO: wrap SplitPane inside `PageContainer` or `PageContainer.Inset` \u2014 SplitPane provides no page padding of its own. It is a grid primitive, not a page scaffold.",
|
|
773
638
|
"DON'T: expect two columns below 1080px. Below that breakpoint SplitPane stacks to a single column (main on top, aside below). Never use it for layouts that must remain side-by-side on tablet or mobile \u2014 use CSS Grid or `ResponsiveGrid` instead.",
|
|
774
639
|
"DON'T: add a CSS `overflow: hidden` or fixed height on the SplitPane wrapper; both columns carry `min-width: 0` to handle overflow correctly, and the grid uses `minmax(0, 1fr)` \u2014 adding external constraints will break the overflow contract.",
|
|
775
640
|
"DON'T: hand-roll a two-column div layout with flexbox or CSS Grid when SplitPane already ships \u2014 that duplicates the responsive breakpoint logic and the semantic `<aside>` element."
|
|
@@ -785,7 +650,7 @@ function MyShell({ children }: { content: React.ReactNode }) {
|
|
|
785
650
|
"ResponsiveGrid \u2014 use when you need more than two columns, or when both columns must have equal or percentage-based widths rather than a fixed-rem aside. SplitPane always gives main a `1fr` and aside a fixed rem width.",
|
|
786
651
|
"PageContainer \u2014 use as the outer scaffold that provides page padding and vertical rhythm; nest SplitPane inside PageContainer, not the other way around.",
|
|
787
652
|
"Sheet \u2014 use when the detail/context panel should slide in as an overlay (drawer) rather than sitting permanently beside the main content. Prefer Sheet on mobile or when the aside content is secondary and on-demand.",
|
|
788
|
-
"
|
|
653
|
+
"Flex direction='col' \u2014 use when content is purely vertical (single column, sequential sections). SplitPane is the right pick only when a persistent side panel is needed at the same hierarchy level as the main content."
|
|
789
654
|
],
|
|
790
655
|
example: `import { SplitPane } from "@godxjp/ui/layout";
|
|
791
656
|
|
|
@@ -883,13 +748,13 @@ function MyShell({ children }: { content: React.ReactNode }) {
|
|
|
883
748
|
'Destructive confirmation inside a Dialog \u2014 pair `tone="destructive"` Button as the confirm action and `variant="outline"` as Cancel; never use `variant="default"` for a delete action.',
|
|
884
749
|
'Icon-only toolbar actions in a DataTable column (edit, delete, copy) using `size="icon-sm"` + `variant="ghost"` + a Lucide icon child \u2014 gives equal-width square targets that don\'t distort the row.',
|
|
885
750
|
"Navigation links styled as buttons (e.g. 'New Invoice', 'Back to list') using `asChild` + Inertia `<Link>` \u2014 preserves SPA navigation while using the button's visual treatment.",
|
|
886
|
-
"Async mutation trigger in an accounting workflow (e.g. 'Sync from MF', 'Export CSV') \u2014 disable on processing state; pair with `
|
|
887
|
-
"Refetch / retry trigger when NOT using TanStack Query \u2014 for manual cache refresh inside a TanStack Query context use `
|
|
751
|
+
"Async mutation trigger in an accounting workflow (e.g. 'Sync from MF', 'Export CSV') \u2014 disable on processing state; pair with `AlertMutationFeedback` for error/retry UI rather than inline `try/catch` alerts.",
|
|
752
|
+
"Refetch / retry trigger when NOT using TanStack Query \u2014 for manual cache refresh inside a TanStack Query context use `ButtonRefetch` instead, which owns its own `disabled`/`onClick` lifecycle."
|
|
888
753
|
],
|
|
889
754
|
related: [
|
|
890
755
|
"DropdownMenu \u2014 when a button needs to reveal a list of actions (e.g. 'Actions \u25BE' in a DataTable row), wrap the Button as a `DropdownMenuTrigger` inside a `DropdownMenu` compound; don't open a Sheet/Dialog just to show a list of options.",
|
|
891
|
-
"
|
|
892
|
-
"
|
|
756
|
+
"ButtonRefetch \u2014 a pre-wired Button variant from `@godxjp/ui/query` that binds directly to a TanStack Query result (shows spinner, auto-disables while fetching, retries on click). Use it instead of a raw Button whenever the action is a query refetch \u2014 do not pass `onClick`/`disabled` to it manually.",
|
|
757
|
+
"AlertMutationFeedback \u2014 for surfacing mutation errors and a retry action; it renders its own retry Button internally. Do not add a separate Button alongside AlertMutationFeedback for the same mutation.",
|
|
893
758
|
"PrefetchLink \u2014 use when the goal is purely navigation with hover-prefetch (Inertia v3 prefetch); it renders as an `<a>` not a button. Only reach for `Button asChild + Link` when the navigation control must look like a button (primary CTA style)."
|
|
894
759
|
],
|
|
895
760
|
example: `import { Button } from "@godxjp/ui/general";
|
|
@@ -1103,8 +968,8 @@ export default function InvoiceList({
|
|
|
1103
968
|
},
|
|
1104
969
|
{
|
|
1105
970
|
name: "size",
|
|
1106
|
-
type: '"
|
|
1107
|
-
defaultValue: '"
|
|
971
|
+
type: '"md" | "compact"',
|
|
972
|
+
defaultValue: '"md"',
|
|
1108
973
|
description: "Card size preset."
|
|
1109
974
|
},
|
|
1110
975
|
{
|
|
@@ -1147,7 +1012,7 @@ export default function InvoiceList({
|
|
|
1147
1012
|
{
|
|
1148
1013
|
name: "CardContent",
|
|
1149
1014
|
group: "data-display",
|
|
1150
|
-
tagline: "Card body. flush = edge-to-edge (for DataTable/tabs); tight = no top gap; solo = no header above. NEVER put a
|
|
1015
|
+
tagline: "Card body. flush = edge-to-edge (for DataTable/tabs); tight = no top gap; solo = no header above. NEVER put a Toolbar inside flush (it loses padding).",
|
|
1151
1016
|
props: [
|
|
1152
1017
|
{
|
|
1153
1018
|
name: "flush",
|
|
@@ -1170,7 +1035,7 @@ export default function InvoiceList({
|
|
|
1170
1035
|
"DO: Use <CardContent flush> for DataTable, Table, or Tabs \u2014 the flush prop removes horizontal padding so the content spans edge-to-edge inside the card border. Never add manual p-0 on the Card itself instead.",
|
|
1171
1036
|
"DO: Use <CardContent tight> when placing a flush toolbar or a Tabs list directly below a CardHeader \u2014 tight removes the top gap so the header and the body connect without an awkward spacing gap.",
|
|
1172
1037
|
"DO: Use <CardContent solo> when the card has no CardHeader above it \u2014 solo gives the top padding that matches the card shell, ensuring visual balance.",
|
|
1173
|
-
"DON'T: Nest a
|
|
1038
|
+
"DON'T: Nest a Toolbar inside <CardContent flush> \u2014 flush strips horizontal padding and Toolbar will lose its own padding. Put Toolbar outside the flush CardContent or in a separate non-flush CardContent above it.",
|
|
1174
1039
|
"DON'T: Wrap a StatCard inside <Card><CardContent> \u2014 StatCard already renders its own Card border; double-wrapping produces a double border. Render StatCard directly in a ResponsiveGrid."
|
|
1175
1040
|
],
|
|
1176
1041
|
useCases: [
|
|
@@ -1185,7 +1050,7 @@ export default function InvoiceList({
|
|
|
1185
1050
|
"Card \u2014 the parent container; CardContent is always a direct child of Card. Card itself has zero internal padding; every visible body padding comes from CardContent (or CardHeader/CardFooter). Never put content directly inside Card.",
|
|
1186
1051
|
"StatCard \u2014 a self-contained KPI tile that IS already a Card; do not wrap it in <Card><CardContent>. Use StatCard directly inside a ResponsiveGrid.",
|
|
1187
1052
|
"ScrollArea \u2014 place ScrollArea inside CardContent (non-flush) when the card body needs to scroll; do not put ScrollArea outside CardContent or you lose the card's internal padding.",
|
|
1188
|
-
"
|
|
1053
|
+
"SkeletonStat \u2014 the loading placeholder for a StatCard tile; swap in SkeletonStat while KPI data is loading. For general Card loading shapes, use SkeletonTable or Skeleton primitives."
|
|
1189
1054
|
],
|
|
1190
1055
|
example: `import { Card, CardContent, DataTable } from "@godxjp/ui/data-display";
|
|
1191
1056
|
|
|
@@ -1229,7 +1094,7 @@ export default function InvoiceList({
|
|
|
1229
1094
|
"DO use `hint` for secondary context (e.g. '\u5148\u6708\u6BD4 +3%', 'last 30 days'). In the default `stacked` layout hint renders below the value; in `inline` layout it renders beside the label.",
|
|
1230
1095
|
"DO NOT add an `accent` prop \u2014 accent is a Card prop and StatCard does not expose it. Passing accent has no effect and creates a false expectation.",
|
|
1231
1096
|
"DO NOT hand-roll a KPI tile using a plain <Card><CardContent>. StatCard is the correct primitive and token-aligns the label/value/hint/delta slots automatically.",
|
|
1232
|
-
"WHILE data is loading, replace each StatCard with a <
|
|
1097
|
+
"WHILE data is loading, replace each StatCard with a <SkeletonStat /> at the same grid position \u2014 never render an empty value string or a spinner inside StatCard itself."
|
|
1233
1098
|
],
|
|
1234
1099
|
useCases: [
|
|
1235
1100
|
"Dashboard KPI row: monthly revenue, invoice count, overdue balance, and collection rate displayed side-by-side in a ResponsiveGrid with delta trend vs previous period.",
|
|
@@ -1237,11 +1102,11 @@ export default function InvoiceList({
|
|
|
1237
1102
|
"Coupon/membership admin overview: active members, live coupons, monthly redemptions, and total discount amount \u2014 the canonical example in the catalog.",
|
|
1238
1103
|
"Inline variant for a narrow sidebar or detail panel where space is constrained: label on the left, large value on the right (layout='inline'), e.g. contract value next to a deal record.",
|
|
1239
1104
|
"Cost or error-rate metrics where a falling number is positive: pass `inverse` so a '-15%' delta shows green, preventing misleading red-for-good UI.",
|
|
1240
|
-
"Loading state for any KPI grid: render the same ResponsiveGrid columns filled with <
|
|
1105
|
+
"Loading state for any KPI grid: render the same ResponsiveGrid columns filled with <SkeletonStat /> components while the query is in-flight, then replace with StatCard tiles once data resolves."
|
|
1241
1106
|
],
|
|
1242
1107
|
related: [
|
|
1243
1108
|
"ResponsiveGrid \u2014 required layout wrapper for StatCard grids; controls column count and responsive breakpoints. Always pair them together.",
|
|
1244
|
-
"
|
|
1109
|
+
"SkeletonStat \u2014 exact loading placeholder shaped like a StatCard tile; swap in while KPI data is fetching, then replace with the real StatCard.",
|
|
1245
1110
|
"Descriptions \u2014 use instead when displaying multiple label/value metadata pairs on a detail page (not headline KPIs); Descriptions is not card-bordered and does not show delta/hint slots.",
|
|
1246
1111
|
"Card + CardContent \u2014 use when you need a general-purpose content container with a header, footer, or arbitrary body; do NOT wrap StatCard inside these."
|
|
1247
1112
|
],
|
|
@@ -1351,7 +1216,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1351
1216
|
"Card / CardContent \u2014 Descriptions provides the internal grid layout; Card/CardContent provides the outer container, padding, and border. Always wrap Descriptions in CardContent (never add p-4 directly on Descriptions). Use Card when you need the visual surface; use Descriptions inside it for the label/value structure.",
|
|
1352
1217
|
"DataTable \u2014 use DataTable when you have multiple rows of the same entity type that need sorting, filtering, or pagination. Use Descriptions when you have a single entity's fields laid out as labelled metadata (one row per field, not one row per record).",
|
|
1353
1218
|
"Table \u2014 use Table (the lower-level primitive) for tabular data with explicit column headers and multiple data rows. Use Descriptions when the data is inherently label\u2192value (no column headers needed, each field is its own row/cell).",
|
|
1354
|
-
"
|
|
1219
|
+
"Flex \u2014 use Flex for arbitrary vertical/horizontal layout of heterogeneous UI elements. Use Descriptions when every item follows the label-on-top / value-below pattern and you want responsive multi-column alignment for free."
|
|
1355
1220
|
],
|
|
1356
1221
|
example: `import { Descriptions } from "@godxjp/ui/data-display";
|
|
1357
1222
|
|
|
@@ -1387,7 +1252,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1387
1252
|
"First-run onboarding screens where no data has been created yet \u2014 e.g. 'No entities added yet' with an action button to create the first legal entity.",
|
|
1388
1253
|
"Passed as the `empty=` prop inside `DataState` or `InfiniteQueryState` to satisfy the TanStack Query lifecycle widget's zero-items slot without hand-rolling markup.",
|
|
1389
1254
|
"Standalone section within a `CardContent` to indicate a sub-section (e.g. attachments, comments, related records) has no entries yet, separate from the page-level list.",
|
|
1390
|
-
"Error-adjacent zero states where the page loaded successfully but the filtered result set is empty \u2014 distinct from an error state handled by `DataState`/`
|
|
1255
|
+
"Error-adjacent zero states where the page loaded successfully but the filtered result set is empty \u2014 distinct from an error state handled by `DataState`/`AlertMutationFeedback`."
|
|
1391
1256
|
],
|
|
1392
1257
|
related: [
|
|
1393
1258
|
"DataTable \u2014 already embeds an EmptyState automatically when `data` is empty; customise via the `empty=` prop. Do NOT wrap DataTable in a `data.length === 0` guard that renders EmptyState separately.",
|
|
@@ -1558,7 +1423,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1558
1423
|
],
|
|
1559
1424
|
usage: [
|
|
1560
1425
|
"DO: pass a `UseQueryResult<T>` directly from `useQuery` \u2014 DataState reads `isPending`, `isError`, `isFetching`, `data`, and `error` off it; never destructure those fields manually and branch yourself.",
|
|
1561
|
-
"DO: always provide a `skeleton` \u2014 it renders during both the initial pending phase and during a re-fetch after an error; pass `<SkeletonTable />` for tabular data or `<
|
|
1426
|
+
"DO: always provide a `skeleton` \u2014 it renders during both the initial pending phase and during a re-fetch after an error; pass `<SkeletonTable />` for tabular data or `<SkeletonStat />` for stat card lists \u2014 never `null` or a spinner div.",
|
|
1562
1427
|
'DO: provide `empty` + `isEmpty` together when the data can legitimately return 0 items \u2014 e.g. `isEmpty={(d) => d.items.length === 0}` paired with `empty={<EmptyState title="\u2026" />}`. Omitting `empty` means an empty array still falls through to `children`, silently rendering a blank table.',
|
|
1563
1428
|
"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.",
|
|
1564
1429
|
"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.",
|
|
@@ -1573,9 +1438,9 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1573
1438
|
],
|
|
1574
1439
|
related: [
|
|
1575
1440
|
"InfiniteQueryState \u2014 use instead of DataState when the query is `useInfiniteQuery`; it accepts a `flatten` function to reduce pages and adds a load-more footer. DataState cannot accept `UseInfiniteQueryResult`.",
|
|
1576
|
-
"SkeletonTable /
|
|
1441
|
+
"SkeletonTable / SkeletonStat \u2014 pass as the `skeleton` prop of DataState; they are not standalone replacements for DataState, only the loading slot inside it.",
|
|
1577
1442
|
"EmptyState \u2014 pass as the `empty` prop of DataState alongside a matching `isEmpty` predicate; do not hand-roll an empty-check outside DataState by inspecting `query.data` yourself.",
|
|
1578
|
-
"
|
|
1443
|
+
"AlertMutationFeedback \u2014 sibling widget for mutation (not query) lifecycle; use it below a form submit button to surface `useMutation` errors, not DataState which only handles `useQuery`."
|
|
1579
1444
|
],
|
|
1580
1445
|
example: `import { DataState } from "@godxjp/ui/query";
|
|
1581
1446
|
|
|
@@ -1617,7 +1482,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1617
1482
|
],
|
|
1618
1483
|
usage: [
|
|
1619
1484
|
"DO: Import from `@godxjp/ui/query` (not `@godxjp/ui`). Use the bundled `flattenItemPages` helper for any API that returns `{ items: T[] }` pages \u2014 it handles `undefined` data safely. Custom page shapes require a custom `flatten` function.",
|
|
1620
|
-
"DO: Always pass `skeleton` (e.g. `<SkeletonTable />` or `<
|
|
1485
|
+
"DO: Always pass `skeleton` (e.g. `<SkeletonTable />` or `<SkeletonStat />`). It shows on initial `isPending`, on refetch-after-error, and whenever `data` is absent. Never show a blank area while loading.",
|
|
1621
1486
|
"DO: Pass `empty` (an `<EmptyState>` node) to handle the zero-results case \u2014 without it the children render-prop is called with an empty array and you get a silent blank screen. Provide a custom `isEmpty` only when `TFlat` is not an array.",
|
|
1622
1487
|
"DON'T: Hand-roll a load-more button. The component renders a default centered outline Button when `hasNextPage` is true. Override only via `loadMore` (custom node) or `showLoadMore={false}` (hide entirely). Never call `query.fetchNextPage()` outside the component for pagination.",
|
|
1623
1488
|
"DON'T: Use `InfiniteQueryState` for a `useQuery` result \u2014 it expects `UseInfiniteQueryResult` shape (`pages`, `hasNextPage`, `fetchNextPage`, `isFetchingNextPage`). For regular `useQuery` use `DataState` instead.",
|
|
@@ -1634,8 +1499,8 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1634
1499
|
related: [
|
|
1635
1500
|
"DataState \u2014 use instead when the query is a plain `useQuery` (not infinite). Identical lifecycle surface (skeleton/empty/error/children) but expects a single page of data, not accumulated pages. Pick DataState for any paginated table where only one page is visible at a time.",
|
|
1636
1501
|
"DataTable \u2014 use for tabular data with server-side pagination where pages are swapped, not appended. DataTable manages its own pagination UI (cursor buttons); InfiniteQueryState is for append-only / infinite-scroll patterns.",
|
|
1637
|
-
"SkeletonTable /
|
|
1638
|
-
"
|
|
1502
|
+
"SkeletonTable / SkeletonStat \u2014 pass as the `skeleton` prop to InfiniteQueryState; do not render them manually alongside InfiniteQueryState since the component controls when skeleton is visible.",
|
|
1503
|
+
"ButtonRefetch \u2014 companion component for the page header refresh action wired to `query.refetch()`. Use alongside InfiniteQueryState when you want an explicit refresh control in addition to the built-in load-more footer."
|
|
1639
1504
|
],
|
|
1640
1505
|
example: `import { InfiniteQueryState, flattenItemPages } from "@godxjp/ui/query";
|
|
1641
1506
|
|
|
@@ -1645,47 +1510,6 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1645
1510
|
storyPath: "query/InfiniteQueryState.stories.tsx",
|
|
1646
1511
|
rules: []
|
|
1647
1512
|
},
|
|
1648
|
-
{
|
|
1649
|
-
name: "MutationFeedback",
|
|
1650
|
-
group: "data-display",
|
|
1651
|
-
tagline: "Inline mutation error \u2014 renders nothing when idle/successful. Import from @godxjp/ui/query.",
|
|
1652
|
-
props: [
|
|
1653
|
-
{
|
|
1654
|
-
name: "mutation",
|
|
1655
|
-
type: "{ isError, error, isPending }",
|
|
1656
|
-
required: true,
|
|
1657
|
-
description: "useMutation result."
|
|
1658
|
-
},
|
|
1659
|
-
{ name: "onRetry", type: "() => void", description: "Retry handler." }
|
|
1660
|
-
],
|
|
1661
|
-
usage: [
|
|
1662
|
-
"DO: Import exclusively from `@godxjp/ui/query` \u2014 it is NOT exported from the main `@godxjp/ui` barrel.",
|
|
1663
|
-
"DO: Pass the full `useMutation` result object as `mutation`; the component reads `.isError`, `.error`, and `.isPending` \u2014 only those three fields are consumed, so a plain object mock works in tests.",
|
|
1664
|
-
"DO: Supply `onRetry` when the user can meaningfully re-trigger the mutation (e.g. re-submit a form, re-run a simulation). The Retry button is rendered by `AlertQueryError` only when `onRetry` is provided AND `showRetry` is not `false`. Set `showRetry={false}` to hide the button when retry is semantically wrong (e.g. a destructive delete that must not auto-repeat).",
|
|
1665
|
-
"DO: Use the `pending` prop to render an inline loading slot while `mutation.isPending` is true \u2014 it replaces the error area with arbitrary JSX (spinner, skeleton row) so the layout does not shift when the mutation transitions idle \u2192 pending \u2192 error.",
|
|
1666
|
-
"DON'T: Render `MutationFeedback` for query (fetch) errors \u2014 use `DataState` for those. `MutationFeedback` is scoped to write operations (`useMutation`), not read operations (`useQuery`).",
|
|
1667
|
-
"DON'T: Hand-roll an inline error alert for mutation failures \u2014 this component already wraps `Alert variant='destructive'` with i18n title, `humanError()` message formatting, and an accessible Retry button. Duplicating that logic breaks consistency and bypasses localisation."
|
|
1668
|
-
],
|
|
1669
|
-
useCases: [
|
|
1670
|
-
"Form save failures \u2014 place `<MutationFeedback mutation={saveMutation} onRetry={saveMutation.mutate} />` directly below a submit button so the error appears inline next to the control that triggered it, keeping the user in context without a page-level toast.",
|
|
1671
|
-
"Blocking simulator / calculation runs \u2014 when a long-running mutation (e.g. tax computation, invoice generation) fails and the page must stay on the form until the user corrects and retries, use `MutationFeedback` with `pending={<SkeletonTable />}` to show a skeleton while running and flip to an error banner on failure.",
|
|
1672
|
-
"Multi-step wizard step submissions \u2014 show per-step mutation errors inline inside each `<Steps>` panel so the user sees exactly which step failed without scrolling to a page-level alert.",
|
|
1673
|
-
"Admin destructive actions (delete, void, archive) \u2014 pass `showRetry={false}` so no Retry button appears after a failed irreversible operation, preventing accidental double-execution.",
|
|
1674
|
-
"Accounting record mutations (journal entry save, invoice approval) \u2014 inline error keeps audit-sensitive feedback close to the triggering form rather than relying on a transient toast that the user might miss.",
|
|
1675
|
-
"Background job kicks that surface an immediate API error \u2014 when a mutation POSTs to a job-dispatch endpoint and the server rejects synchronously, `MutationFeedback` surfaces the error inline with a retry CTA without requiring a separate alert component."
|
|
1676
|
-
],
|
|
1677
|
-
related: [
|
|
1678
|
-
"Toaster \u2014 use `toast.error()` (sonner) via `Toaster` for transient, non-blocking save confirmations where the user does not need to retry in-place; prefer `MutationFeedback` when the error is blocking and must stay visible until the user acts.",
|
|
1679
|
-
"DataState \u2014 use `DataState` for the full TanStack Query read lifecycle (pending skeleton / error / empty / data); `MutationFeedback` is the write-side counterpart for `useMutation` errors only.",
|
|
1680
|
-
"Alert \u2014 the raw destructive alert primitive; use `Alert variant='destructive'` directly only when the error is not from a `useMutation` result and you need full compositional control; `MutationFeedback` is the correct abstraction when you have a mutation object.",
|
|
1681
|
-
"QueryRefetchButton \u2014 for surfacing a manual refetch action on a `useQuery` result in a page header; `MutationFeedback` handles the equivalent retry on the mutation side and must not be conflated with query refetch patterns."
|
|
1682
|
-
],
|
|
1683
|
-
example: `import { MutationFeedback } from "@godxjp/ui/query";
|
|
1684
|
-
|
|
1685
|
-
<MutationFeedback mutation={saveMutation} />`,
|
|
1686
|
-
storyPath: "query/MutationFeedback.stories.tsx",
|
|
1687
|
-
rules: []
|
|
1688
|
-
},
|
|
1689
1513
|
// ─── data-entry ─────────────────────────────────────────────────────────
|
|
1690
1514
|
{
|
|
1691
1515
|
name: "FormField",
|
|
@@ -1728,21 +1552,21 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1728
1552
|
"DO pass a SINGLE React element as `children`. FormField calls `React.cloneElement` on it to inject `aria-describedby`, `aria-required`, and `aria-invalid` \u2014 if you pass a fragment or multiple nodes, cloneElement silently skips the injection and a11y attributes are lost.",
|
|
1729
1553
|
"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.",
|
|
1730
1554
|
"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.",
|
|
1731
|
-
"DON'T wrap `Switch` in FormField \u2014 use `
|
|
1732
|
-
"DON'T use FormField for checkbox-beside-label or radio-beside-label patterns \u2014 use `
|
|
1555
|
+
"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.",
|
|
1556
|
+
"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."
|
|
1733
1557
|
],
|
|
1734
1558
|
useCases: [
|
|
1735
1559
|
"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.",
|
|
1736
1560
|
"Wrapping a `Select` or `DatePicker` inside a multi-field filter panel where each control needs a visible label, helper hint (e.g. 'YYYY/MM/DD'), and inline error state.",
|
|
1737
1561
|
"Adding a `labelAddon` tooltip button next to a 'Tax rate' label in an accounting form to explain when different rates apply, without breaking the label\u2013control association.",
|
|
1738
1562
|
"Enclosing a `DateRangePicker` or `TimePicker` in an admin settings page where the field needs a label, a muted hint ('Inclusive of start and end date'), and conditional error display.",
|
|
1739
|
-
"Wrapping a `SearchSelect` or `
|
|
1563
|
+
"Wrapping a `SearchSelect` or `Select` (with `showSearch`) control for vendor/account lookup in a journal-entry form where the `id` must be kept consistent for programmatic focus management.",
|
|
1740
1564
|
"Providing structured error feedback for a `Cascader` or `TreeSelect` in a multi-level category assignment screen, replacing ad-hoc error rendering with the standardised `role='alert'` pattern."
|
|
1741
1565
|
],
|
|
1742
1566
|
related: [
|
|
1743
1567
|
"Label \u2014 the bare Radix label component. Use directly only when you are building a fully custom layout that cannot accept FormField's stack wrapper, and you will manage aria-describedby/aria-invalid yourself. FormField is always preferred for standard form controls.",
|
|
1744
|
-
"
|
|
1745
|
-
"
|
|
1568
|
+
"Field \u2014 a self-contained field for boolean toggles: it already includes its own label, hidden `<input name>` for HTML form submission, helper, and error. Never wrap a bare `Switch` in FormField.",
|
|
1569
|
+
"Field \u2014 pairs a single checkbox or radio with a label and optional description in a horizontal layout (control beside text). Use Field instead of FormField when the control and its label sit side-by-side rather than stacked.",
|
|
1746
1570
|
"CheckboxGroup / RadioGroup \u2014 for groups of options where FormField is not needed per-item; the group component handles its own legend/label and option layout."
|
|
1747
1571
|
],
|
|
1748
1572
|
example: `import { FormField, Input } from "@godxjp/ui/data-entry";
|
|
@@ -1827,10 +1651,10 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1827
1651
|
"DO: supply an `ariaLabel` (or visible `label`) when no adjacent label exists. Without either prop, SearchInput falls back to the i18n key `common.search` rendered as a visually-hidden `<Label>` \u2014 still accessible, but providing a context-specific string (e.g. `ariaLabel='\u8ACB\u6C42\u66F8\u3092\u691C\u7D22'`) is more descriptive for screen readers.",
|
|
1828
1652
|
"DON'T: use SearchInput inside a `<form>` expecting native form submission. The component has no `name` prop and does not emit a form field value \u2014 it is a filter-trigger widget. For a form search field, use a plain `Input` inside `FormField`.",
|
|
1829
1653
|
"DON'T: hand-roll a debounced input when you need a search box. SearchInput ships the debounce, clear button (\xD7), search icon, and accessible label \u2014 recreating these with a raw `<Input>` adds code and misses the UX contract.",
|
|
1830
|
-
"DON'T: place SearchInput inside a `
|
|
1654
|
+
"DON'T: place SearchInput inside a `ToolbarGroup` wrapper \u2014 `ToolbarGroup` is for Select/DatePicker controls with a label chip. SearchInput goes directly as a child of `Toolbar` (or standalone above a table), not wrapped in `ToolbarGroup`."
|
|
1831
1655
|
],
|
|
1832
1656
|
useCases: [
|
|
1833
|
-
"List-page filter bar: placed as the first child of `
|
|
1657
|
+
"List-page filter bar: placed as the first child of `Toolbar` (before any `ToolbarGroup` children) to drive text-based filtering of a `DataTable`. The `onSearch` callback updates a query param or state variable that the table's data fetch reads.",
|
|
1834
1658
|
"Inline client-side search over a small in-memory list (e.g. a sidebar nav list, a transfer panel, a settings category list) where results narrow immediately as the user types without a server round-trip \u2014 use uncontrolled mode (`defaultValue`) so no state is needed in the parent.",
|
|
1835
1659
|
"URL-synced search: controlled mode where `value` comes from `useSearchParams()` and `onSearch` pushes to the URL, enabling deep-linkable, bookmarkable filtered views on invoice/transaction/customer index pages.",
|
|
1836
1660
|
"Panel or dialog search: filtering a long dropdown list, a tree, or a multi-item selection panel that does not use the built-in `Command` palette \u2014 SearchInput provides the search box while the parent renders the filtered result set.",
|
|
@@ -1838,7 +1662,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1838
1662
|
],
|
|
1839
1663
|
related: [
|
|
1840
1664
|
"Input \u2014 use `Input` (inside `FormField`) when the search field is part of a submitted form and needs a `name` attribute, or when you need full `onChange` control without any debounce or clear button. SearchInput is the right pick when the field only triggers filtering, not form submission.",
|
|
1841
|
-
"
|
|
1665
|
+
"Toolbar \u2014 SearchInput is almost always placed as a direct child of `Toolbar`, which provides the surrounding strip, clear-all button, and active-filter state. Do not use SearchInput as a standalone header widget when a full filter strip (with selects etc.) already exists \u2014 compose them together.",
|
|
1842
1666
|
"Command \u2014 use `Command` + `CommandInput` when you need a keyboard-navigable command palette or combobox list with grouped items and keyboard selection. `Command` is only meaningful when paired with `CommandList`; SearchInput is the right pick for a plain filter box with no item-selection behavior.",
|
|
1843
1667
|
"Select (with showSearch) \u2014 when users must pick a value from a list AND search to narrow it, use `<Select options={...} showSearch>` (which has its own built-in search input). SearchInput is for filtering an external data set, not for value selection from an option list."
|
|
1844
1668
|
],
|
|
@@ -1944,8 +1768,8 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1944
1768
|
},
|
|
1945
1769
|
{
|
|
1946
1770
|
name: "SelectTrigger size",
|
|
1947
|
-
type: '"sm" | "
|
|
1948
|
-
defaultValue: '"
|
|
1771
|
+
type: '"sm" | "md"',
|
|
1772
|
+
defaultValue: '"md"',
|
|
1949
1773
|
description: "Compound API only. Size variant on the SelectTrigger sub-component."
|
|
1950
1774
|
}
|
|
1951
1775
|
],
|
|
@@ -1969,7 +1793,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1969
1793
|
related: [
|
|
1970
1794
|
"SearchSelect \u2014 the combobox engine Select delegates to when showSearch=true or loadOptions is set. Prefer Select with showSearch instead of reaching for SearchSelect directly (SearchSelect is now deprecated as a public API).",
|
|
1971
1795
|
"TreeSelect \u2014 use when options are hierarchical (parent/child tree). Not a drop-in for Select; has expand/collapse and a separate treeData prop.",
|
|
1972
|
-
"
|
|
1796
|
+
"Select with showSearch \u2014 use Select (with the `showSearch` prop) for typeahead/autocomplete lookup patterns instead of the removed Autocomplete component.",
|
|
1973
1797
|
"RadioGroup \u2014 use instead of Select when there are 2-4 mutually exclusive choices that must all be visible at once without opening a popover.",
|
|
1974
1798
|
"Combobox (if present) \u2014 compound cmdk-powered combobox for free-text + suggestion; Select is for strict value lists only."
|
|
1975
1799
|
],
|
|
@@ -2074,7 +1898,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2074
1898
|
{
|
|
2075
1899
|
name: "Switch",
|
|
2076
1900
|
group: "data-entry",
|
|
2077
|
-
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use
|
|
1901
|
+
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use Field.",
|
|
2078
1902
|
props: [
|
|
2079
1903
|
{ name: "checked", type: "boolean", description: "Controlled checked state." },
|
|
2080
1904
|
{
|
|
@@ -2082,6 +1906,12 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2082
1906
|
type: "(checked: boolean) => void",
|
|
2083
1907
|
description: "Fires when toggled."
|
|
2084
1908
|
},
|
|
1909
|
+
{
|
|
1910
|
+
name: "size",
|
|
1911
|
+
type: '"sm" | "md"',
|
|
1912
|
+
defaultValue: '"md"',
|
|
1913
|
+
description: "Thumb size \u2014 'sm' for dense rows."
|
|
1914
|
+
},
|
|
2085
1915
|
{ name: "id", type: "string", description: "Links to a <Label htmlFor>." },
|
|
2086
1916
|
{
|
|
2087
1917
|
name: "disabled",
|
|
@@ -2092,23 +1922,23 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2092
1922
|
],
|
|
2093
1923
|
usage: [
|
|
2094
1924
|
"DO use Switch (bare) only when you are building a custom inline toggle without a visible label \u2014 e.g., a DataTable row action column. Always pair it with a <Label htmlFor={id}> placed adjacent in the DOM; never leave it label-less for screen readers.",
|
|
2095
|
-
"DO NOT pass a `name` prop to bare Switch expecting HTML form submission \u2014 Radix Switch renders no hidden input, so the value is silently dropped on submit. Use
|
|
2096
|
-
"DO use the `size` prop ('sm' | '
|
|
2097
|
-
"DO wire controlled state: pass both `checked` (boolean) and `onCheckedChange` together. Passing only one causes a React controlled/uncontrolled warning. For uncontrolled use, pass neither \u2014 but bare Switch has no `defaultChecked` state management built in (
|
|
2098
|
-
"DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field \u2014 that is exactly what
|
|
1925
|
+
"DO NOT pass a `name` prop to bare Switch expecting HTML form submission \u2014 Radix Switch renders no hidden input, so the value is silently dropped on submit. Use Field (which mirrors a hidden `0`/`1` input) for any field that must submit inside an HTML <form>.",
|
|
1926
|
+
"DO use the `size` prop ('sm' | 'md') to control thumb size. 'sm' is appropriate in dense DataTable rows or filter bars; omit it (defaults to 'md') everywhere else.",
|
|
1927
|
+
"DO wire controlled state: pass both `checked` (boolean) and `onCheckedChange` together. Passing only one causes a React controlled/uncontrolled warning. For uncontrolled use, pass neither \u2014 but bare Switch has no `defaultChecked` state management built in (Field handles that internally).",
|
|
1928
|
+
"DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field \u2014 that is exactly what Field provides, including aria-describedby, aria-invalid, error/helper text, and the hidden input. Reach for Field instead.",
|
|
2099
1929
|
"DO link the switch to its label via matching `id` on Switch and `htmlFor` on Label. Without this pairing, clicking the label text does not toggle the switch and the a11y association is broken."
|
|
2100
1930
|
],
|
|
2101
1931
|
useCases: [
|
|
2102
1932
|
"Inline toggle in a DataTable action cell (e.g., 'Active' column) where the label is already provided by the column header and no form submission is involved.",
|
|
2103
|
-
"Settings panel where a React state boolean is toggled immediately via an optimistic API call \u2014 no <form> submit, so
|
|
2104
|
-
"Custom compound component where you compose Switch + Label yourself and need direct access to the Radix Root props (e.g., adding aria-controls or data-attributes not supported by
|
|
1933
|
+
"Settings panel where a React state boolean is toggled immediately via an optimistic API call \u2014 no <form> submit, so Field's hidden input is unnecessary.",
|
|
1934
|
+
"Custom compound component where you compose Switch + Label yourself and need direct access to the Radix Root props (e.g., adding aria-controls or data-attributes not supported by Field).",
|
|
2105
1935
|
"Filter toolbar toggle (e.g., 'Show archived') rendered inline next to other filter controls, using size='sm' for density parity with adjacent inputs.",
|
|
2106
1936
|
"Preview/demo UI where the switch controls a local display state (dark-mode preview, feature flag preview) with no server persistence."
|
|
2107
1937
|
],
|
|
2108
1938
|
related: [
|
|
2109
|
-
"
|
|
1939
|
+
"Field \u2014 use this instead of bare Switch whenever the toggle needs a visible label, helper text, error message, or must submit its value inside an HTML <form>. Field composes Label + Switch + hidden input automatically.",
|
|
2110
1940
|
"Checkbox \u2014 use Checkbox (or CheckboxGroup) when the user is selecting one or more items from a set, or when the binary choice semantically means 'agree/select' rather than 'enable/disable'. Switch implies an immediate, persistent state change; Checkbox implies a form choice.",
|
|
2111
|
-
"
|
|
1941
|
+
"Field \u2014 use for a binary or small-set choice rendered as radio-style cards with rich descriptions, when the visual weight of a toggle is insufficient for the decision importance.",
|
|
2112
1942
|
"RadioGroup \u2014 use when the user must choose exactly one option from 2\u20134 mutually exclusive values; Switch is only appropriate for a single on/off boolean."
|
|
2113
1943
|
],
|
|
2114
1944
|
example: `import { Switch, Label } from "@godxjp/ui/data-entry";
|
|
@@ -2153,7 +1983,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2153
1983
|
"Input \u2014 use Input for single-line values (names, amounts, codes). Use Textarea only when the expected value spans multiple lines or could be longer than ~80 characters.",
|
|
2154
1984
|
"FormField \u2014 always the parent wrapper for Textarea in forms; provides label, helper text, error message, and injects all required aria attributes onto the Textarea child automatically.",
|
|
2155
1985
|
"Select \u2014 when the user must pick from a finite set of multi-line-looking options (e.g. template choices) use Select, not a Textarea presenting options as free text.",
|
|
2156
|
-
"
|
|
1986
|
+
"Select with showSearch or SearchSelect \u2014 if the multi-line field is actually a tag/token input or a constrained lookup, prefer Select (showSearch) or SearchSelect over a Textarea that the user types into freely."
|
|
2157
1987
|
],
|
|
2158
1988
|
example: `import { Textarea } from "@godxjp/ui/data-entry";
|
|
2159
1989
|
|
|
@@ -2173,23 +2003,23 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2173
2003
|
"DO: always pass `htmlFor` matching the `id` of the associated control \u2014 this is the entire purpose of the component. Without it, clicking the label text does NOT focus or toggle the control, breaking a11y and UX.",
|
|
2174
2004
|
'DO: import from `@godxjp/ui/data-entry` (not shadcn or Radix directly). The godx-ui Label extends Radix\'s LabelPrimitive with `data-slot="label"`, `select-none`, and `group-data-[disabled]` opacity-50 \u2014 hand-rolling a `<label>` loses all of these.',
|
|
2175
2005
|
"DON'T: use Label as a standalone visible heading or section title. It is a form-control association primitive. For page/section headings use semantic HTML (`<h2>`, etc.) or a typography class instead.",
|
|
2176
|
-
"DON'T: wrap Label around a control that is already labelled internally. FormField,
|
|
2177
|
-
"DO: pair Label with Checkbox or Switch when NOT using the compound
|
|
2006
|
+
"DON'T: wrap Label around a control that is already labelled internally. FormField, Field, and CheckboxGroup all render Label internally \u2014 adding a second Label creates a duplicate association and redundant screen-reader announcement.",
|
|
2007
|
+
"DO: pair Label with Checkbox or Switch when NOT using the compound wrapper (Field). In that case generate the shared id with `React.useId()` and pass it to both `id` on the control and `htmlFor` on Label.",
|
|
2178
2008
|
"PREFER FormField over a bare Label + control pair whenever you also need helper text, error messages, or `required` asterisk. FormField injects `aria-describedby` and `aria-invalid` automatically; a bare Label does not."
|
|
2179
2009
|
],
|
|
2180
2010
|
useCases: [
|
|
2181
|
-
"Pairing with a standalone Checkbox when
|
|
2182
|
-
"Labelling a bare Switch (not
|
|
2183
|
-
"Adding a visible label to a custom or third-party control that accepts an `id` prop but isn't wrapped by FormField or
|
|
2011
|
+
"Pairing with a standalone Checkbox when Field's two-line layout is unnecessary \u2014 e.g. a single 'Remember me' option in a login form.",
|
|
2012
|
+
"Labelling a bare Switch (not Field) in a settings row where the switch is controlled by parent state and no HTML form name attribute is needed.",
|
|
2013
|
+
"Adding a visible label to a custom or third-party control that accepts an `id` prop but isn't wrapped by FormField or Field.",
|
|
2184
2014
|
"Labelling a Textarea in a free-text form field when FormField's helper/error slots aren't needed, keeping the markup minimal.",
|
|
2185
2015
|
"Rendering an accessible label inside a table row where a FormField's block layout would break the inline/grid structure.",
|
|
2186
2016
|
"Adding a label to a DatePicker, TimePicker, or ColorPicker inside a simple layout that doesn't need the full FormField wrapper."
|
|
2187
2017
|
],
|
|
2188
2018
|
related: [
|
|
2189
2019
|
"FormField \u2014 prefer this over a bare Label whenever the field needs helper text, an error message, or a required marker; FormField renders Label internally and wires aria-describedby/aria-invalid automatically.",
|
|
2190
|
-
"
|
|
2191
|
-
"
|
|
2192
|
-
"Checkbox \u2014 the most common bare-Label partner; pair with Label via shared useId() id/htmlFor when
|
|
2020
|
+
"Field \u2014 use for a Checkbox or Radio.Item that needs a visible label and optional description line; it renders Label internally \u2014 do NOT add a second Label around it.",
|
|
2021
|
+
"Field \u2014 use instead of a bare Switch + Label pair when the control must submit a value via an HTML form name; Field owns the Label + hidden input composition.",
|
|
2022
|
+
"Checkbox \u2014 the most common bare-Label partner; pair with Label via shared useId() id/htmlFor when Field's layout is too heavy."
|
|
2193
2023
|
],
|
|
2194
2024
|
example: `import { Label } from "@godxjp/ui/data-entry";
|
|
2195
2025
|
|
|
@@ -2217,24 +2047,24 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2217
2047
|
usage: [
|
|
2218
2048
|
"DO pair every standalone Checkbox with a `<Label htmlFor={id}>` \u2014 the id prop on Checkbox must match the htmlFor on Label so screen readers announce the label on focus. Without this pairing the control is inaccessible.",
|
|
2219
2049
|
"DO use the controlled pattern (`checked` + `onCheckedChange`) for any form-bound checkbox. `onCheckedChange` receives `boolean | 'indeterminate'` \u2014 always coerce with `!!v` or an explicit guard before storing in state.",
|
|
2220
|
-
"DO use `Checkbox.Group` (alias for CheckboxGroup) with the `options` prop when you have \u22652 choices from an array \u2014 it renders each item inside a `
|
|
2050
|
+
"DO use `Checkbox.Group` (alias for CheckboxGroup) with the `options` prop when you have \u22652 choices from an array \u2014 it renders each item inside a `Field` (label + optional description), generates stable ids automatically, and manages the `string[]` value array. NEVER hand-roll a loop of bare `<Checkbox>` elements for a multi-select list.",
|
|
2221
2051
|
"DO pass `name` on `Checkbox.Group` (not on individual checkboxes) when the group must submit as form fields \u2014 the group propagates the name to each internal checkbox so the browser serialises all checked values under that key.",
|
|
2222
2052
|
"DON'T use `checked='indeterminate'` on `Checkbox.Group` children \u2014 indeterminate is only meaningful on a parent 'select-all' control you wire manually; the group itself does not auto-compute it.",
|
|
2223
|
-
"DON'T wrap a standalone Checkbox in `
|
|
2053
|
+
"DON'T wrap a standalone Checkbox in `Field` manually \u2014 `Field` is the internal composition primitive that `Checkbox.Group` uses. For a single boolean with a label, use `<div className='flex items-center gap-2'><Checkbox id='x' .../><Label htmlFor='x'>...</Label></div>` as shown in the catalog example; for a full labelled-checkbox with description, use `Field` directly only if you need a one-off item outside a group."
|
|
2224
2054
|
],
|
|
2225
2055
|
useCases: [
|
|
2226
2056
|
"A 'Select all' / bulk-action row above a DataTable \u2014 standalone Checkbox with `checked='indeterminate'` when some (not all) rows are selected, toggling between all-selected and none-selected.",
|
|
2227
|
-
"A multi-step filter panel (e.g. filter invoices by payment status: Paid, Unpaid, Overdue) \u2014 `Checkbox.Group` with `options` prop and `orientation='vertical'`, controlled value wired to
|
|
2057
|
+
"A multi-step filter panel (e.g. filter invoices by payment status: Paid, Unpaid, Overdue) \u2014 `Checkbox.Group` with `options` prop and `orientation='vertical'`, controlled value wired to Toolbar state.",
|
|
2228
2058
|
"Confirmation or consent acknowledgement before a destructive action in a Dialog \u2014 standalone Checkbox with controlled state used to enable/disable the confirm Button.",
|
|
2229
|
-
"Settings panel where each feature flag is a boolean toggle with a description line \u2014 `Checkbox.Group` with options carrying a `description` field so each row renders label + subtext via
|
|
2059
|
+
"Settings panel where each feature flag is a boolean toggle with a description line \u2014 `Checkbox.Group` with options carrying a `description` field so each row renders label + subtext via Field.",
|
|
2230
2060
|
"Bulk-edit form row in an accounting ledger (e.g. 'Apply to all selected entries') \u2014 standalone Checkbox with name + value inside a `<form>` for native HTML form submission.",
|
|
2231
2061
|
"Onboarding checklist (e.g. 'I have read the terms', 'I consent to data processing') with multiple distinct items whose values are independent \u2014 two separate standalone Checkboxes, each with their own id/state, not a Checkbox.Group (since each item maps to a different boolean field)."
|
|
2232
2062
|
],
|
|
2233
2063
|
related: [
|
|
2234
|
-
"CheckboxGroup \u2014 use instead of bare Checkbox when you have a list of 2+ options from an array; it handles id generation,
|
|
2235
|
-
"Switch /
|
|
2064
|
+
"CheckboxGroup \u2014 use instead of bare Checkbox when you have a list of 2+ options from an array; it handles id generation, Field wrapping, value array management, and the `name` prop for form submission. Checkbox is for a single boolean; CheckboxGroup is for multi-select.",
|
|
2065
|
+
"Switch / Field \u2014 use Switch when the action takes immediate effect (enable/disable a feature in settings) rather than selecting an option to be submitted later. Checkbox implies 'will be submitted as part of a form'; Switch implies 'applies now'. Field adds a hidden input for HTML form compatibility.",
|
|
2236
2066
|
"RadioGroup \u2014 use when only one option in a group may be selected at a time (mutually exclusive). CheckboxGroup = multiple selections allowed; RadioGroup = single selection only.",
|
|
2237
|
-
"
|
|
2067
|
+
"Field \u2014 the internal layout primitive (control slot + Label + description) that Checkbox.Group renders per item. Use it directly only when you need a one-off labelled checkbox or radio item outside of a group, and you want the consistent indent/description layout without the group's value-management overhead."
|
|
2238
2068
|
],
|
|
2239
2069
|
example: `import { Checkbox, Label } from "@godxjp/ui/data-entry";
|
|
2240
2070
|
|
|
@@ -2269,10 +2099,10 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2269
2099
|
}
|
|
2270
2100
|
],
|
|
2271
2101
|
usage: [
|
|
2272
|
-
"DO use the `options` prop for the data-driven path: pass `{ label, value, disabled?, description? }[]` and RadioGroup renders every option as a correctly labelled
|
|
2102
|
+
"DO use the `options` prop for the data-driven path: pass `{ label, value, disabled?, description? }[]` and RadioGroup renders every option as a correctly labelled Field automatically \u2014 never hand-roll Radio.Item + Label pairs in a loop yourself.",
|
|
2273
2103
|
"DO provide `name` whenever the group lives inside an HTML form: Radix renders a hidden `<input name={name}>` carrying the selected string value, making the field natively form-submittable without a separate hidden input.",
|
|
2274
2104
|
"DO use controlled mode (`value` + `onValueChange`) for any form managed by useForm or a state manager. Use `defaultValue` only for truly uncontrolled UI where you never need to read the value in code.",
|
|
2275
|
-
"DO NOT reach for children / manual composition unless the options list is dynamic-JSX (e.g. each item needs a custom rendered label with an icon). When you do compose children manually, wrap each Radio.Item in a
|
|
2105
|
+
"DO NOT reach for children / manual composition unless the options list is dynamic-JSX (e.g. each item needs a custom rendered label with an icon). When you do compose children manually, wrap each Radio.Item in a Field \u2014 rendering a bare Radio.Item without Field skips the label and breaks a11y.",
|
|
2276
2106
|
"DO NOT use RadioGroup when the user may select zero or multiple items \u2014 that is CheckboxGroup. RadioGroup enforces exactly one selection at all times (or none before first interaction when uncontrolled).",
|
|
2277
2107
|
"A11y: the Radix root emits `role=radiogroup`; each item gets `role=radio` and is keyboard-navigable with arrow keys. Never suppress `name` on the Root when inside a form \u2014 without it the hidden input is unnamed and won't submit."
|
|
2278
2108
|
],
|
|
@@ -2287,8 +2117,8 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2287
2117
|
related: [
|
|
2288
2118
|
"CheckboxGroup \u2014 use when the user may select zero or more values simultaneously (multi-select); RadioGroup enforces exactly one selection. Both share the same options array shape and orientation prop.",
|
|
2289
2119
|
"Select \u2014 use when there are 5 or more options or the option list is dynamic/searchable; RadioGroup is preferred for 2-4 fixed visible choices where scanning all options at once matters.",
|
|
2290
|
-
"
|
|
2291
|
-
"
|
|
2120
|
+
"Field \u2014 use when there are exactly two states that map to on/off (boolean); RadioGroup is the right pick when the two-or-more options are semantically distinct named values, not a toggle.",
|
|
2121
|
+
"Field \u2014 the low-level label+description wrapper that RadioGroup uses internally for each item. Use it directly only when manually composing Radio.Item children inside Radio.Group; never hand-roll a label alongside a bare Radio.Item without it."
|
|
2292
2122
|
],
|
|
2293
2123
|
example: `import { RadioGroup } from "@godxjp/ui/data-entry";
|
|
2294
2124
|
|
|
@@ -2430,7 +2260,7 @@ export function InvoiceDueDateField() {
|
|
|
2430
2260
|
"Sheet \u2014 use Sheet instead of Dialog when the content is a slide-in panel (filters, detail sidebar, settings drawer). Sheet uses `side` prop and is better suited for wide filter forms or contextual detail panels that don't demand full focus interruption.",
|
|
2431
2261
|
"Alert \u2014 use Alert for inline, non-modal status messages (validation errors, success banners on the page). Dialog is modal and focus-trapping; Alert is inline and never blocks interaction.",
|
|
2432
2262
|
"Popover \u2014 use Popover for lightweight non-modal overlays anchored to a trigger (quick-edit a single field, tooltip-style confirmation for low-stakes actions). Dialog is full-modal; Popover stays near its trigger and doesn't dim the page.",
|
|
2433
|
-
"
|
|
2263
|
+
"AlertMutationFeedback \u2014 use AlertMutationFeedback for toast/inline feedback after the Dialog closes, not inside it. Putting a success toast inside a Dialog that is about to unmount causes it to disappear immediately; emit the feedback after `onOpenChange(false)` resolves."
|
|
2434
2264
|
],
|
|
2435
2265
|
example: `import { useState } from "react";
|
|
2436
2266
|
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
|
|
@@ -2572,7 +2402,7 @@ function CreateDialog() {
|
|
|
2572
2402
|
],
|
|
2573
2403
|
related: [
|
|
2574
2404
|
"Dialog \u2014 use Dialog (centered modal) when the action is destructive, requires full user focus, or needs a confirm/alertdialog (mode='confirm'). Use Sheet when the user benefits from seeing the page content behind the slide-over (filters, detail peek, quick-edit).",
|
|
2575
|
-
"
|
|
2405
|
+
"Toolbar/ToolbarGroup \u2014 use Toolbar for inline persistent filter controls above a DataTable (no overlay). Use Sheet when the filter set is large (>4 controls) or on mobile where inline controls collapse poorly.",
|
|
2576
2406
|
"Popover \u2014 use Popover for lightweight, anchor-positioned context menus or single-control overlays (date picker, color picker). Use Sheet when the panel has a header, multiple fields, or footer actions that need a dedicated panel.",
|
|
2577
2407
|
"SplitPane \u2014 use SplitPane for a persistent side-by-side layout where both panes are always visible. Use Sheet when the secondary panel is transient and should overlay the primary content."
|
|
2578
2408
|
],
|
|
@@ -2629,7 +2459,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2629
2459
|
],
|
|
2630
2460
|
related: [
|
|
2631
2461
|
"Toaster \u2014 use for transient, auto-dismissing feedback ('Record saved', 'Deleted'). Alert is for persistent page-scoped banners; Toaster is for fire-and-forget notifications triggered by toast() from sonner.",
|
|
2632
|
-
"
|
|
2462
|
+
"AlertMutationFeedback \u2014 use when you want inline success/error feedback tightly coupled to a form mutation's state (renders inline below the submit button). Alert requires you to manage show/hide state yourself.",
|
|
2633
2463
|
"DataState \u2014 use for full query lifecycle (loading skeleton + empty state + error) inside a data-fetching section. Alert.QueryError is the error sub-component DataState uses internally; prefer DataState when you also need the loading/empty states.",
|
|
2634
2464
|
"EmptyState \u2014 use for the zero-data case inside a list or table section, not for errors or warnings. Alert is for status messages; EmptyState is for the absence of data."
|
|
2635
2465
|
],
|
|
@@ -2674,7 +2504,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2674
2504
|
related: [
|
|
2675
2505
|
"DataTable \u2014 sibling component that SkeletonTable precedes. Once DataTable mounts, use its `loading` prop (renders an in-table loading row) for subsequent refetches rather than swapping back to SkeletonTable. Pick SkeletonTable only for the pre-mount gap.",
|
|
2676
2506
|
"DataState \u2014 query lifecycle widget from `@godxjp/ui/query`; accepts SkeletonTable as its `skeleton` prop and handles loading/empty/error transitions automatically. Prefer DataState + SkeletonTable over a hand-rolled ternary when the data comes from a useQuery hook.",
|
|
2677
|
-
"
|
|
2507
|
+
"SkeletonStat \u2014 sibling skeleton shaped like a StatCard tile; use inside a ResponsiveGrid to placeholder KPI dashboard cards, not tabular data.",
|
|
2678
2508
|
"DataTable \u2014 when data is already mounted but re-fetching (e.g. pagination, filter change), set `loading={true}` on DataTable directly instead of unmounting it and swapping in SkeletonTable; avoids layout shift and preserves scroll position."
|
|
2679
2509
|
],
|
|
2680
2510
|
example: `import { SkeletonTable } from "@godxjp/ui/feedback";
|
|
@@ -2683,39 +2513,6 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2683
2513
|
storyPath: "feedback/Skeleton.stories.tsx",
|
|
2684
2514
|
rules: []
|
|
2685
2515
|
},
|
|
2686
|
-
{
|
|
2687
|
-
name: "SkeletonCard",
|
|
2688
|
-
group: "feedback",
|
|
2689
|
-
tagline: "Loading placeholder shaped like a StatCard tile. Use inside a ResponsiveGrid while KPIs load.",
|
|
2690
|
-
props: [],
|
|
2691
|
-
usage: [
|
|
2692
|
-
"DO render SkeletonCard directly inside a ResponsiveGrid (no Card wrapper needed) \u2014 it is a self-contained block with its own padding and shape, matching the three-layer anatomy of a StatCard tile (label line \u2192 value line \u2192 hint line).",
|
|
2693
|
-
"DO render one SkeletonCard per expected StatCard tile so the grid dimensions stay stable during load. Four KPI tiles loading \u2192 four SkeletonCard siblings in the same ResponsiveGrid.",
|
|
2694
|
-
"DON'T wrap SkeletonCard in <Card> or <CardContent> \u2014 it already owns its box layout. Double-wrapping adds unwanted padding and borders, identical to the StatCard double-border mistake.",
|
|
2695
|
-
"DON'T use SkeletonCard for non-stat shapes: row lists \u2192 SkeletonTable or SkeletonRows; a single record detail page \u2192 SkeletonDetail. SkeletonCard is only correct when the loaded state is a stat/KPI tile.",
|
|
2696
|
-
`DON'T add aria-busy or aria-live yourself \u2014 the component sets aria-busy="true" on its root automatically. Adding them again duplicates announcements for screen readers.`,
|
|
2697
|
-
"DON'T pass any props \u2014 SkeletonCard takes none. If you need a different height or layout, check whether SkeletonDetail or a custom SkeletonBlock composition is the right tool instead."
|
|
2698
|
-
],
|
|
2699
|
-
useCases: [
|
|
2700
|
-
"Dashboard KPI row loading: while a deferred prop or async query fetches aggregate figures (total revenue, open invoices, overdue count, cash balance), render four SkeletonCards in a ResponsiveGrid columns={4} so the page shell holds its layout without a spinner overlay.",
|
|
2701
|
-
"Accounting summary header: the top-of-page stat strip on an invoice list or ledger view needs a stable layout before period-filtered totals resolve \u2014 SkeletonCard keeps each column's width locked.",
|
|
2702
|
-
"Per-entity KPI switcher: when the user switches the active legal entity and a new round of stats is re-fetched, swap the StatCard tiles back to SkeletonCard during the transition to prevent stale values from flashing.",
|
|
2703
|
-
"Report page initialisation: a profit-and-loss or balance-sheet summary card row uses SkeletonCard as the placeholder while the report query runs, then replaces each tile with a StatCard once data arrives.",
|
|
2704
|
-
"Deferred prop shell (Inertia v3): pair SkeletonCard tiles with Inertia's Deferred component so the page shell renders instantly and the stat row hydrates when the deferred payload resolves."
|
|
2705
|
-
],
|
|
2706
|
-
related: [
|
|
2707
|
-
"StatCard \u2014 the loaded counterpart. Replace SkeletonCard with StatCard (inside the same ResponsiveGrid slot) once data is available. StatCard also draws its own bordered card, so wrapping rules are identical \u2014 never add an extra Card around either.",
|
|
2708
|
-
"SkeletonTable \u2014 use instead of SkeletonCard when the loading area will become a DataTable or row-list. SkeletonTable renders a header row plus N body rows; SkeletonCard renders a three-line KPI block.",
|
|
2709
|
-
"SkeletonDetail \u2014 use instead of SkeletonCard when the loading area will become a single-record detail view (title + metadata key-value rows). Wrong pick: using SkeletonCard on a detail page leaves a mismatched shape.",
|
|
2710
|
-
"DataState \u2014 use instead of SkeletonCard when the loading area is driven by a TanStack Query result; DataState handles the skeleton/empty/error lifecycle automatically and does not require manual SkeletonCard/SkeletonTable placement."
|
|
2711
|
-
],
|
|
2712
|
-
example: `import { SkeletonCard } from "@godxjp/ui/feedback";
|
|
2713
|
-
import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
2714
|
-
|
|
2715
|
-
<ResponsiveGrid columns={4}><SkeletonCard /><SkeletonCard /><SkeletonCard /><SkeletonCard /></ResponsiveGrid>`,
|
|
2716
|
-
storyPath: "feedback/Skeleton.stories.tsx",
|
|
2717
|
-
rules: []
|
|
2718
|
-
},
|
|
2719
2516
|
{
|
|
2720
2517
|
name: "Toaster",
|
|
2721
2518
|
group: "feedback",
|
|
@@ -2740,13 +2537,13 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
2740
2537
|
useCases: [
|
|
2741
2538
|
'After a successful form save (invoice, journal entry, vendor record) \u2014 show `toast.success("\u4FDD\u5B58\u3057\u307E\u3057\u305F")` to confirm without blocking navigation.',
|
|
2742
2539
|
'After a background job is enqueued (e.g. bulk sync or export) \u2014 show `toast.info("\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3092\u958B\u59CB\u3057\u307E\u3057\u305F")` then later update with `toast.promise()` to track completion.',
|
|
2743
|
-
"Mutation error fallback when the error is transient and retrying is the right UX \u2014 show `toast.error(message)` instead of replacing page content; reserve `
|
|
2540
|
+
"Mutation error fallback when the error is transient and retrying is the right UX \u2014 show `toast.error(message)` instead of replacing page content; reserve `AlertMutationFeedback` for inline, persistent error display inside a form.",
|
|
2744
2541
|
'Soft destructive action confirmation outcome \u2014 e.g. "\u524A\u9664\u3057\u307E\u3057\u305F" after an item is removed, paired with an undo action via `toast("\u2026", { action: { label: \'\u5143\u306B\u623B\u3059\', onClick: undo } })`.',
|
|
2745
2542
|
'OAuth / session expiry warnings \u2014 surface a brief `toast.warning("\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u8FD1\u3065\u3044\u3066\u3044\u307E\u3059")` without interrupting the user\'s current form state.'
|
|
2746
2543
|
],
|
|
2747
2544
|
related: [
|
|
2748
2545
|
"Alert \u2014 use for persistent, inline feedback that must stay visible (validation summaries, page-level warnings, destructive notices). Unlike Toaster, Alert does not auto-dismiss and lives in the document flow.",
|
|
2749
|
-
"
|
|
2546
|
+
"AlertMutationFeedback \u2014 use when you have a TanStack `useMutation` result and want an inline error + retry UI inside a form or card. Renders nothing on success/idle; pairs naturally with a `toast.success` in `onSuccess`.",
|
|
2750
2547
|
"Dialog \u2014 use when the user must make a conscious decision (confirm delete, resolve conflict) before proceeding. Toaster toasts are fire-and-forget; Dialog blocks until the user responds."
|
|
2751
2548
|
],
|
|
2752
2549
|
example: `// app root \u2014 mount once
|
|
@@ -2796,7 +2593,7 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
2796
2593
|
],
|
|
2797
2594
|
related: [
|
|
2798
2595
|
"Steps (@godxjp/ui/navigation) \u2014 sequential wizard/progress indicator. Use Steps when order and completion state matter (multi-step forms, onboarding flows); use Tabs when panels are non-sequential and any tab can be visited freely.",
|
|
2799
|
-
"
|
|
2596
|
+
"Toolbar / ToolbarGroup (@godxjp/ui/navigation) \u2014 horizontal filter chip row. Visually resembles `line`-variant tabs but is semantically different: Toolbar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.",
|
|
2800
2597
|
"DropdownMenu (@godxjp/ui/navigation) \u2014 use for space-constrained contexts where showing all tab triggers at once is impractical (e.g. mobile overflow menu). If only 2-3 options exist and screen space is tight, a DropdownSidebar is a lighter alternative to a full tab strip."
|
|
2801
2598
|
],
|
|
2802
2599
|
example: `import { Tabs } from "@godxjp/ui/navigation";
|
|
@@ -2811,111 +2608,13 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
2811
2608
|
storyPath: "navigation/Tabs.stories.tsx",
|
|
2812
2609
|
rules: []
|
|
2813
2610
|
},
|
|
2814
|
-
{
|
|
2815
|
-
name: "FilterBar",
|
|
2816
|
-
group: "navigation",
|
|
2817
|
-
tagline: "Standard list-page filter strip. Place ABOVE the table Card \u2014 NEVER inside CardContent flush (it strips padding). Compose with FilterGroup + SearchInput + Select.",
|
|
2818
|
-
props: [
|
|
2819
|
-
{
|
|
2820
|
-
name: "children",
|
|
2821
|
-
type: "ReactNode",
|
|
2822
|
-
required: true,
|
|
2823
|
-
description: "Filter controls + FilterGroup wrappers."
|
|
2824
|
-
},
|
|
2825
|
-
{
|
|
2826
|
-
name: "hasActiveFilters",
|
|
2827
|
-
type: "boolean",
|
|
2828
|
-
description: "Shows a clear-all button when true."
|
|
2829
|
-
},
|
|
2830
|
-
{ name: "onClear", type: "() => void", description: "Clear-all handler." }
|
|
2831
|
-
],
|
|
2832
|
-
usage: [
|
|
2833
|
-
"DO place FilterBar ABOVE the table Card in the page layout \u2014 never inside CardContent or CardContent flush. It carries its own padding-block via CSS tokens; nesting it inside a flush card strips that spacing and breaks the visual rhythm. The correct pattern is: <PageInset><FilterBar \u2026/></PageInset> then a sibling <Card><CardContent flush><DataTable /></CardContent></Card>.",
|
|
2834
|
-
"DO wrap every labelled filter control (Select, DatePicker, DateRangePicker, etc.) in a FilterGroup with a descriptive label prop. A bare Select dropped directly in FilterBar has no label anchor and breaks the stacked-column layout on mobile. SearchInput is the one exception \u2014 it does NOT need a FilterGroup wrapper because it carries its own visible placeholder.",
|
|
2835
|
-
"DO manage filter state yourself (controlled). FilterBar has no internal state \u2014 it is a layout shell. Pass your state values into the filter controls as value/onValueChange, derive hasActiveFilters from whether any filter value differs from its empty/default state, and clear all state in onClear. Do NOT rely on form name/submission; FilterBar filters are instant-apply, not form-submitted.",
|
|
2836
|
-
"DO pass both hasActiveFilters AND onClear to show the clear-all button. The button only renders when BOTH props are truthy \u2014 passing onClear alone with hasActiveFilters defaulting to true shows the button even when no filters are active. Compute hasActiveFilters as a boolean expression over your state: hasActiveFilters={search !== '' || status !== 'all'}.",
|
|
2837
|
-
"DON'T hand-roll a clear button inside children. FilterBar renders its own Button variant='ghost' size='sm' with the localised 'clear filters' label (via useTranslation). Adding a second clear button inside children causes duplication and i18n inconsistency.",
|
|
2838
|
-
"DON'T use FilterBar for tab-like navigation between content panels. It is semantically a filter strip \u2014 switching dataset predicates, not rendering different pages or sections. For panel switching use Tabs / Tabs. The two look similar in line-variant style but FilterBar does not use role='tablist' and has no active-panel concept."
|
|
2839
|
-
],
|
|
2840
|
-
useCases: [
|
|
2841
|
-
"Invoice / journal-entry list page: SearchInput for free-text search, FilterGroup wrapping a Select for status (draft/posted/void), FilterGroup wrapping a DateRangePicker for fiscal-period, all above a DataTable card \u2014 hasActiveFilters derived from all three states.",
|
|
2842
|
-
"Partner / customer ledger list: FilterGroup for account type (AR/AP), FilterGroup for legal entity, SearchInput for partner name \u2014 clear-all resets the entity switcher back to 'all' as well as local filter state.",
|
|
2843
|
-
"Transaction history with multi-dimension filtering: date range + account Select + currency Select \u2014 FilterBar's responsive flex-wrap layout automatically stacks filters vertically on mobile and flows them into a row on \u2265640px without any custom CSS.",
|
|
2844
|
-
"Admin user / permission list: SearchInput for email search + FilterGroup wrapping a Select for role \u2014 the clear button becomes visible only when either control departs from its default, so the toolbar stays clean on first load.",
|
|
2845
|
-
"Report parameter bar above a read-only DataTable: two DatePickers inside FilterGroups set the report start/end dates, a Select picks the reporting entity \u2014 hasActiveFilters is always true once the user first applies the report, giving a one-click reset.",
|
|
2846
|
-
"Any list page migrating away from hand-rolled Tailwind flex rows with inline labels \u2014 drop existing label+control pairs into FilterGroup to get consistent label colour (muted-foreground), gap, and the responsive stacking behaviour for free."
|
|
2847
|
-
],
|
|
2848
|
-
related: [
|
|
2849
|
-
"FilterGroup (@godxjp/ui/navigation) \u2014 the required child wrapper for each labelled filter control inside FilterBar. Use FilterGroup for every Select/DatePicker/DateRangePicker slot; omit it only for SearchInput which does not need a visible label.",
|
|
2850
|
-
"SearchInput (@godxjp/ui/data-entry) \u2014 the free-text search control placed directly as a child of FilterBar (no FilterGroup wrapper needed). SearchInput handles debounce and the clear-X icon internally; do not wrap it in a FilterGroup or compose it manually from Input.",
|
|
2851
|
-
"Tabs / Tabs (@godxjp/ui/navigation) \u2014 for switching between content panels, not filtering a dataset. If the 'filter' is really changing which rendered section is visible (not which rows pass a predicate), use Tabs instead of FilterBar.",
|
|
2852
|
-
"PageInset (@godxjp/ui/layout) \u2014 the layout wrapper that gives FilterBar its horizontal padding when the parent PageContainer is flush. Always wrap FilterBar in PageInset inside a flush container; in a non-flush container FilterBar's own padding-block tokens are sufficient."
|
|
2853
|
-
],
|
|
2854
|
-
example: `import { FilterBar, FilterGroup } from "@godxjp/ui/navigation";
|
|
2855
|
-
import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@godxjp/ui/data-entry";
|
|
2856
|
-
|
|
2857
|
-
<FilterBar hasActiveFilters={search !== ""} onClear={() => setSearch("")}>
|
|
2858
|
-
<SearchInput placeholder="\u540D\u524D\u3067\u691C\u7D22" value={search} onSearch={setSearch} />
|
|
2859
|
-
<FilterGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
|
|
2860
|
-
<Select value={status} onValueChange={setStatus}>
|
|
2861
|
-
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
2862
|
-
<SelectContent>
|
|
2863
|
-
<SelectItem value="all">\u3059\u3079\u3066</SelectItem>
|
|
2864
|
-
<SelectItem value="active">\u6709\u52B9</SelectItem>
|
|
2865
|
-
</SelectContent>
|
|
2866
|
-
</Select>
|
|
2867
|
-
</FilterGroup>
|
|
2868
|
-
</FilterBar>`,
|
|
2869
|
-
storyPath: "navigation/FilterBar.stories.tsx",
|
|
2870
|
-
rules: [38, 40]
|
|
2871
|
-
},
|
|
2872
|
-
{
|
|
2873
|
-
name: "FilterGroup",
|
|
2874
|
-
group: "navigation",
|
|
2875
|
-
tagline: "Labelled filter slot inside FilterBar \u2014 wraps a single Select/DatePicker.",
|
|
2876
|
-
props: [
|
|
2877
|
-
{
|
|
2878
|
-
name: "label",
|
|
2879
|
-
type: "ReactNode",
|
|
2880
|
-
required: true,
|
|
2881
|
-
description: "Label shown with the child control."
|
|
2882
|
-
},
|
|
2883
|
-
{ name: "children", type: "ReactNode", required: true, description: "The filter control." }
|
|
2884
|
-
],
|
|
2885
|
-
usage: [
|
|
2886
|
-
"DO: Always nest FilterGroup directly inside FilterBar \u2014 it renders a vertical label+control stack (ui-stack-xs) that aligns with FilterBar's flex-wrap row layout. Never use FilterGroup as a standalone wrapper outside FilterBar.",
|
|
2887
|
-
"DO: Pass exactly ONE filter control as children (Select, DatePicker, DateRangePicker, SearchSelect, etc.). FilterGroup is a single-control labeled slot, not a multi-control panel \u2014 it renders the label div above the control and sizes the slot to full-width mobile / auto desktop.",
|
|
2888
|
-
"DON'T: Pass a raw label string that contains complex JSX beyond text \u2014 label is ReactNode but is styled as muted small-size text (ui-filter-label). Do not add Tooltip or interactive elements inside the label as it is purely presentational.",
|
|
2889
|
-
"DON'T: Place FilterGroup inside CardContent (including flush variant) \u2014 this breaks horizontal padding. FilterBar (and thus all its FilterGroups) must sit above the table Card as a standalone block. Page order: KPIs \u2192 FilterBar (containing FilterGroups) \u2192 Card wrapping DataTable.",
|
|
2890
|
-
"DO: Use FilterGroup for every labelled filter slot; do NOT hand-roll a label+control pair with a div. The label placement, color (muted-foreground), and font-size are token-driven through ui-filter-label \u2014 duplicating this with raw Tailwind will drift from the design.",
|
|
2891
|
-
"A11y: label is rendered as a visible div, not a form <label> element \u2014 it is not wired to the child control's id. If the child control (e.g. Select) has its own accessible label, that is sufficient. Do not add htmlFor on the FilterGroup label; instead ensure the inner control has aria-label or its own Label via the control's API."
|
|
2892
|
-
],
|
|
2893
|
-
useCases: [
|
|
2894
|
-
"A list page for invoices/journal entries where each column value can be filtered: wrap each Select (status, period, entity) in its own FilterGroup inside a FilterBar placed above the DataTable card.",
|
|
2895
|
-
"An admin transaction log page with a date range picker and a partner Select \u2014 each gets its own FilterGroup label so users can scan 'Period' / 'Partner' labels horizontally before interacting.",
|
|
2896
|
-
"A report filter panel that shows 'Fiscal Year', 'Department', and 'Account Type' dropdowns \u2014 FilterGroups communicate the semantic name of each filter without requiring the controls themselves to have visible labels (where Select placeholder alone is ambiguous).",
|
|
2897
|
-
"Mobile-first responsive filter strip: FilterBar + FilterGroups stack vertically on narrow screens (full-width per FilterGroup) and wrap into a horizontal row at sm:, making them safe for 320 px viewports without extra breakpoint overrides.",
|
|
2898
|
-
"When a filter control (e.g. DateRangePicker) has no intrinsic visible label of its own \u2014 wrapping it in FilterGroup provides the visible label without hand-rolling a div/label pattern that would differ from design tokens."
|
|
2899
|
-
],
|
|
2900
|
-
related: [
|
|
2901
|
-
"FilterBar \u2014 the required parent container. FilterGroup has no useful meaning outside FilterBar; FilterBar also owns the clear-all button (onClear + hasActiveFilters). Always compose FilterGroup inside FilterBar, never standalone.",
|
|
2902
|
-
"SearchInput \u2014 sits directly inside FilterBar as a sibling to FilterGroup (not wrapped in FilterGroup) because it is self-labelling via its placeholder and search icon. Use FilterGroup only for controls that need a separate visible label.",
|
|
2903
|
-
"Select \u2014 the canonical child for FilterGroup when filtering by a categorical value (status, type, entity). Pair with FilterGroup for the label; do not hand-roll a label+Select div.",
|
|
2904
|
-
"Tabs \u2014 an alternative filtering/scoping pattern when the filter has exactly 2-4 mutually exclusive states and you want them visible as persistent tabs rather than a dropdown. Use Tabs+TabsList above the table instead of FilterBar+FilterGroup+Select when the values are few and well-known."
|
|
2905
|
-
],
|
|
2906
|
-
example: `import { FilterGroup } from "@godxjp/ui/navigation";
|
|
2907
|
-
|
|
2908
|
-
<FilterGroup label="\u30B9\u30B3\u30FC\u30D7"><Select>{/* ... */}</Select></FilterGroup>`,
|
|
2909
|
-
storyPath: "navigation/FilterBar.stories.tsx",
|
|
2910
|
-
rules: [38]
|
|
2911
|
-
},
|
|
2912
2611
|
{
|
|
2913
2612
|
name: "Pagination",
|
|
2914
2613
|
group: "navigation",
|
|
2915
2614
|
tagline: "Offset/page-based pagination bar. Sits below a table card.",
|
|
2916
2615
|
props: [
|
|
2917
2616
|
{
|
|
2918
|
-
name: "
|
|
2617
|
+
name: "value",
|
|
2919
2618
|
type: "number",
|
|
2920
2619
|
defaultValue: "1",
|
|
2921
2620
|
description: "Current page (1-indexed)."
|
|
@@ -2928,22 +2627,22 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
2928
2627
|
description: "Show total count, or a custom label fn."
|
|
2929
2628
|
},
|
|
2930
2629
|
{
|
|
2931
|
-
name: "
|
|
2630
|
+
name: "onValueChange",
|
|
2932
2631
|
type: "(page: number, pageSize: number) => void",
|
|
2933
2632
|
description: "Page / page-size change handler."
|
|
2934
2633
|
}
|
|
2935
2634
|
],
|
|
2936
2635
|
usage: [
|
|
2937
|
-
"DO always control Pagination externally: store `
|
|
2636
|
+
"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.",
|
|
2938
2637
|
"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.",
|
|
2939
2638
|
"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.",
|
|
2940
2639
|
"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.",
|
|
2941
2640
|
"DO use `showTotal` to surface item counts: pass `true` for the built-in i18n label, or a function `(total, [from, to]) => ReactNode` for a custom range label like '1\u201310 of 342 invoices'. Never hard-code a total string beside the component.",
|
|
2942
|
-
"DON'T use Pagination for cursor- or infinite-scroll-based lists. Pagination is strictly offset/page-based (`
|
|
2641
|
+
"DON'T use Pagination for cursor- or infinite-scroll-based lists. Pagination is strictly offset/page-based (`value` is a page number). For cursor pagination inside a DataTable use `DataTable.Pagination`; for infinite scroll use `InfiniteQueryState`."
|
|
2943
2642
|
],
|
|
2944
2643
|
useCases: [
|
|
2945
2644
|
"Standalone offset-paginated admin list pages (e.g. invoice list, customer list, transaction history) rendered outside DataTable \u2014 place Pagination below the table card, outside the card border, with `showTotal` and optionally `showSizeChanger`.",
|
|
2946
|
-
"Search results pages where the backend accepts `page` + `per_page` query parameters and returns a total count \u2014 wire `
|
|
2645
|
+
"Search results pages where the backend accepts `page` + `per_page` query parameters and returns a total count \u2014 wire `value` and `pageSize` to URL search params so the URL is shareable and browser-back works.",
|
|
2947
2646
|
"Reports and filtered data grids where the user needs to export 'all selected pages': `showTotal` with a custom function lets you show '1\u201350 of 1 200 rows' so the user understands the scope before exporting.",
|
|
2948
2647
|
"Compact modal or sheet footers with a long list (e.g. selecting from a product catalog inside a dialog) \u2014 use `simple` mode to save horizontal space while keeping navigation accessible.",
|
|
2949
2648
|
"DataTable instances where the server returns an offset-based total and `DataTable.Pagination` is not being used: attach a standalone Pagination below the card and pass the same `page` / `pageSize` state to both the DataTable `data` prop and the API fetch."
|
|
@@ -2952,11 +2651,11 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
2952
2651
|
"DataTable.Pagination \u2014 use instead of standalone Pagination when the list is rendered inside a DataTable compound and uses cursor-based navigation (cursor + hasMore + onChange). DataTable.Pagination handles First/Next without page arithmetic; standalone Pagination requires a known total.",
|
|
2953
2652
|
"InfiniteQueryState \u2014 use for infinite-scroll / load-more lists driven by useInfiniteQuery. It auto-manages skeleton, empty, and error states; Pagination is inappropriate here because there is no discrete page number.",
|
|
2954
2653
|
"DataTable \u2014 when offset pagination is needed inside DataTable, prefer composing DataTable with a standalone Pagination below the card rather than DataTable.Pagination if the API is offset-based and returns a total count. DataTable itself does not paginate; you supply `data` for the current page.",
|
|
2955
|
-
"SearchInput \u2014 often placed in the same toolbar as Pagination. Resetting `
|
|
2654
|
+
"SearchInput \u2014 often placed in the same toolbar as Pagination. Resetting `value` to page 1 inside the search `onSearchChange` handler is mandatory; forgetting this is the most common bug when combining search and Pagination."
|
|
2956
2655
|
],
|
|
2957
2656
|
example: `import { Pagination } from "@godxjp/ui/navigation";
|
|
2958
2657
|
|
|
2959
|
-
<Pagination
|
|
2658
|
+
<Pagination value={page} total={filtered.length} pageSize={10} showTotal onValueChange={(p) => setPage(p)} />`,
|
|
2960
2659
|
storyPath: "navigation/Pagination.stories.tsx",
|
|
2961
2660
|
rules: [40]
|
|
2962
2661
|
},
|
|
@@ -3016,14 +2715,20 @@ import { Button } from "@godxjp/ui/general";
|
|
|
3016
2715
|
{
|
|
3017
2716
|
name: "items",
|
|
3018
2717
|
type: "StepItemProp[]",
|
|
3019
|
-
description: "Array of { title,
|
|
2718
|
+
description: "Array of { title, subtitle?, description?, icon?, status? }."
|
|
3020
2719
|
},
|
|
3021
2720
|
{
|
|
3022
|
-
name: "
|
|
2721
|
+
name: "value",
|
|
3023
2722
|
type: "number",
|
|
3024
2723
|
defaultValue: "0",
|
|
3025
2724
|
description: "Active step index (0-based)."
|
|
3026
2725
|
},
|
|
2726
|
+
{
|
|
2727
|
+
name: "defaultValue",
|
|
2728
|
+
type: "number",
|
|
2729
|
+
defaultValue: "0",
|
|
2730
|
+
description: "Base offset for the first rendered step index."
|
|
2731
|
+
},
|
|
3027
2732
|
{
|
|
3028
2733
|
name: "orientation",
|
|
3029
2734
|
type: '"horizontal" | "vertical"',
|
|
@@ -3032,19 +2737,19 @@ import { Button } from "@godxjp/ui/general";
|
|
|
3032
2737
|
}
|
|
3033
2738
|
],
|
|
3034
2739
|
usage: [
|
|
3035
|
-
"DO: Pass all steps via the `items` array (each `{ title,
|
|
3036
|
-
"DO: Control the active step with `
|
|
3037
|
-
"DO: Use per-item `status` to pin individual steps independently of `
|
|
2740
|
+
"DO: Pass all steps via the `items` array (each `{ title, subtitle?, description?, icon?, status?, disabled? }`) \u2014 Steps is a single-component API with no child sub-components to compose manually.",
|
|
2741
|
+
"DO: Control the active step with `value` (0-based index). For async operations, set the top-level `status` prop (`'process'|'error'|'finish'`) to override the current step's icon \u2014 e.g. `status='error'` turns the active step red without touching `items`.",
|
|
2742
|
+
"DO: Use per-item `status` to pin individual steps independently of `value` (e.g. a skipped or already-errored step). Per-item `status` takes precedence over the derived status from `value`.",
|
|
3038
2743
|
"DON'T: Use Steps for navigation that needs URL routing or tab-switching \u2014 it has no built-in panel rendering. Pair it with your own conditional panel or a `Tabs`/`Tabs` body; Steps only renders the indicator bar.",
|
|
3039
|
-
"DON'T: Wire `
|
|
3040
|
-
"A11y: The `<ol>` is given `aria-label='Progress'` automatically. Individual steps render as `<button type='button'>` when `
|
|
2744
|
+
"DON'T: Wire `onValueChange` unless you actually support non-linear navigation. `onValueChange` makes every non-disabled step clickable (rendered as `<button>`); omitting it makes all steps non-interactive (`cursor-default`). Never set `disabled` on an item without also providing `onValueChange`, or the prop is meaningless.",
|
|
2745
|
+
"A11y: The `<ol>` is given `aria-label='Progress'` automatically. Individual steps render as `<button type='button'>` when `onValueChange` is present \u2014 ensure each `item.title` is descriptive enough to serve as the button label; avoid icon-only steps without a visible title."
|
|
3041
2746
|
],
|
|
3042
2747
|
useCases: [
|
|
3043
2748
|
"Multi-step form wizard (entity onboarding, invoice creation): render Steps above a form, drive `current` from local state, advance on validated submit \u2014 use `status='error'` on the current step when server validation fails.",
|
|
3044
2749
|
"Async background job tracker: display steps for a long-running import/export pipeline; poll job status and map job phases to `StepStatusProp` values (`'process'` with spinner for in-flight, `'finish'` for done, `'error'` for failed).",
|
|
3045
2750
|
"Document approval workflow (accounting, contracts): map approval stages (Draft \u2192 Review \u2192 Approved \u2192 Archived) to `items` with per-item `status` reflecting the real state from the server \u2014 use `orientation='vertical'` for a sidebar timeline feel.",
|
|
3046
|
-
"Onboarding checklist sidebar: `orientation='vertical'` + `type='dot'` + `size='
|
|
3047
|
-
"Non-linear step navigation (e.g. revisit a previous step to correct data): provide `
|
|
2751
|
+
"Onboarding checklist sidebar: `orientation='vertical'` + `type='dot'` + `size='sm'` for a compact sidebar progress guide alongside a multi-section settings page.",
|
|
2752
|
+
"Non-linear step navigation (e.g. revisit a previous step to correct data): provide `onValueChange` and leave only future steps `disabled`; past and current steps become clickable buttons."
|
|
3048
2753
|
],
|
|
3049
2754
|
related: [
|
|
3050
2755
|
"Timeline \u2014 use Timeline (from @godxjp/ui) when you need a chronological event log with timestamps and variable content per entry; use Steps when the number of stages is fixed and forward-progress is the semantic.",
|
|
@@ -3054,7 +2759,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
3054
2759
|
],
|
|
3055
2760
|
example: `import { Steps } from "@godxjp/ui/navigation";
|
|
3056
2761
|
|
|
3057
|
-
<Steps
|
|
2762
|
+
<Steps value={1} items={[{ title: "\u7533\u8ACB" }, { title: "\u5BE9\u67FB\u4E2D" }, { title: "\u5B8C\u4E86" }]} />`,
|
|
3058
2763
|
storyPath: "navigation/Steps.stories.tsx",
|
|
3059
2764
|
rules: []
|
|
3060
2765
|
},
|
|
@@ -4075,7 +3780,7 @@ export function DocumentUploadDropzone() {
|
|
|
4075
3780
|
"Upload (variant='picture-card') \u2014 multi-image grid upload without crop."
|
|
4076
3781
|
],
|
|
4077
3782
|
example: `{\`import { useState } from "react";
|
|
4078
|
-
import { UploadCropDialog } from "@godxjp/ui/
|
|
3783
|
+
import { UploadCropDialog } from "@godxjp/ui/upload"; // internal \u2014 prefer Upload variant="avatar-crop" instead
|
|
4079
3784
|
|
|
4080
3785
|
export function AvatarField() {
|
|
4081
3786
|
const [cropFile, setCropFile] = useState<File | null>(null);
|
|
@@ -4629,211 +4334,6 @@ export function ReportRangeFilter() {
|
|
|
4629
4334
|
storyPath: "data-entry/Calendar.stories.tsx",
|
|
4630
4335
|
rules: [3, 5, 6, 23]
|
|
4631
4336
|
},
|
|
4632
|
-
{
|
|
4633
|
-
name: "CountrySelect",
|
|
4634
|
-
group: "data-entry",
|
|
4635
|
-
tagline: "Flag-and-name country picker built on Select; always uncontrolled \u2014 pass `name` for form submission and `defaultValue` to pre-select, not `value`/`onChange`.",
|
|
4636
|
-
props: [
|
|
4637
|
-
{
|
|
4638
|
-
name: "id",
|
|
4639
|
-
type: "string",
|
|
4640
|
-
required: true,
|
|
4641
|
-
description: "HTML id forwarded to the SelectTrigger for label association and a11y."
|
|
4642
|
-
},
|
|
4643
|
-
{
|
|
4644
|
-
name: "name",
|
|
4645
|
-
type: "string",
|
|
4646
|
-
required: true,
|
|
4647
|
-
description: "Form field name. The selected country code (value) is submitted under this key via the hidden native select that Select wraps."
|
|
4648
|
-
},
|
|
4649
|
-
{
|
|
4650
|
-
name: "options",
|
|
4651
|
-
type: "CountryOptionProp[]",
|
|
4652
|
-
required: true,
|
|
4653
|
-
description: "List of country options. Each entry must have at least `name` and either `value` or `code` (the country ISO code). Optionally include `nativeName`, `flagSvgPath`, and `label`."
|
|
4654
|
-
},
|
|
4655
|
-
{
|
|
4656
|
-
name: "defaultValue",
|
|
4657
|
-
type: "string | null",
|
|
4658
|
-
description: "Pre-selected country code. When omitted (and allowEmpty is false), defaults to the first option's value. Pass null or empty string to show the placeholder."
|
|
4659
|
-
},
|
|
4660
|
-
{
|
|
4661
|
-
name: "required",
|
|
4662
|
-
type: "boolean",
|
|
4663
|
-
defaultValue: "false",
|
|
4664
|
-
description: "Marks the field as required via aria-required on the trigger."
|
|
4665
|
-
},
|
|
4666
|
-
{
|
|
4667
|
-
name: "allowEmpty",
|
|
4668
|
-
type: "boolean",
|
|
4669
|
-
defaultValue: "false",
|
|
4670
|
-
description: "When true, prepends an empty sentinel option (value '0') rendered as emptyLabel. Lets the user submit no country. Without this, the picker always has a real country selected."
|
|
4671
|
-
},
|
|
4672
|
-
{
|
|
4673
|
-
name: "emptyLabel",
|
|
4674
|
-
type: "string",
|
|
4675
|
-
defaultValue: "\u2014",
|
|
4676
|
-
description: "Label shown for the empty option when allowEmpty is true."
|
|
4677
|
-
},
|
|
4678
|
-
{
|
|
4679
|
-
name: "placeholder",
|
|
4680
|
-
type: "string",
|
|
4681
|
-
description: "Placeholder text shown inside the trigger when no value is selected (relevant when defaultValue is null/empty and allowEmpty is true)."
|
|
4682
|
-
},
|
|
4683
|
-
{
|
|
4684
|
-
name: "invalid",
|
|
4685
|
-
type: "boolean",
|
|
4686
|
-
defaultValue: "false",
|
|
4687
|
-
description: "Sets aria-invalid on the SelectTrigger to indicate a validation error."
|
|
4688
|
-
}
|
|
4689
|
-
],
|
|
4690
|
-
usage: [
|
|
4691
|
-
"DO: Always pass both `id` and `name` \u2014 `id` wires up a `<label htmlFor>`, `name` is the form submission key for the selected country code.",
|
|
4692
|
-
"DO NOT: Pass `value`/`onChange` \u2014 CountrySelect is UNCONTROLLED only. It uses `defaultValue` (passed to the underlying Select). For controlled scenarios wrap the underlying `Select` primitive directly.",
|
|
4693
|
-
"DO: Populate `options` with objects satisfying CountryOptionProp \u2014 each needs `name` plus either `value` (preferred) or `code` as the ISO code. Add `flagSvgPath` for flag images and `nativeName` for bilingual display.",
|
|
4694
|
-
"DO: Use `allowEmpty` when the country field is optional \u2014 it inserts a sentinel '0' entry. Without it, a country is always pre-selected (falls back to options[0] if defaultValue is absent), so the form will always submit a real code.",
|
|
4695
|
-
"DO: Set `invalid={true}` in error state to surface aria-invalid to assistive technology; pair it with a visible error message adjacent to the control.",
|
|
4696
|
-
"DO NOT: Hand-roll a flag+name row or a custom country dropdown \u2014 use CountrySelect (for form submission) or CountryOptionLabel standalone (for display-only read-only rows)."
|
|
4697
|
-
],
|
|
4698
|
-
useCases: [
|
|
4699
|
-
"Billing / shipping address forms where the user must pick a country before proceeding and the code is submitted to the server.",
|
|
4700
|
-
"Account settings pages where a user sets their home country or tax residency \u2014 use `defaultValue` with the stored ISO code to pre-populate.",
|
|
4701
|
-
"Invoice creation forms in an accounting app that require a supplier or customer country, with `allowEmpty={false}` to guarantee a code is always present.",
|
|
4702
|
-
"Optional 'country of origin' filter fields \u2014 use `allowEmpty={true}` so users can clear the selection back to 'no filter'.",
|
|
4703
|
-
"Multi-step onboarding flows that must pre-select a country inferred from the user's locale, then let them correct it.",
|
|
4704
|
-
"Read-only display of a country name + flag in a Descriptions or DataTable cell \u2014 use `CountryOptionLabel` directly (not CountrySelect) for non-interactive display."
|
|
4705
|
-
],
|
|
4706
|
-
related: [
|
|
4707
|
-
"Select \u2014 the generic primitive CountrySelect is built on; use Select directly when you need a controlled picker or options that are not country objects.",
|
|
4708
|
-
"CountryOptionLabel \u2014 the flag + name row exported from the same module; use it standalone in read-only contexts (table cells, detail views) where no picker interaction is needed.",
|
|
4709
|
-
"DatePicker \u2014 another uncontrolled data-entry primitive that submits via a hidden form value; same pattern but for dates."
|
|
4710
|
-
],
|
|
4711
|
-
example: `import { CountrySelect } from "@godxjp/ui/data-entry";
|
|
4712
|
-
|
|
4713
|
-
const countries = [
|
|
4714
|
-
{ value: "JP", name: "Japan", nativeName: "\u65E5\u672C", flagSvgPath: "/flags/jp.svg" },
|
|
4715
|
-
{ value: "US", name: "United States", nativeName: "United States", flagSvgPath: "/flags/us.svg" },
|
|
4716
|
-
{ value: "VN", name: "Vietnam", nativeName: "Vi\u1EC7t Nam", flagSvgPath: "/flags/vn.svg" },
|
|
4717
|
-
];
|
|
4718
|
-
|
|
4719
|
-
// Required country field \u2014 pre-select Japan
|
|
4720
|
-
<CountrySelect
|
|
4721
|
-
id="billing-country"
|
|
4722
|
-
name="billingCountry"
|
|
4723
|
-
options={countries}
|
|
4724
|
-
defaultValue="JP"
|
|
4725
|
-
required
|
|
4726
|
-
invalid={!!errors.billingCountry}
|
|
4727
|
-
/>
|
|
4728
|
-
|
|
4729
|
-
// Optional country field \u2014 allow clearing
|
|
4730
|
-
<CountrySelect
|
|
4731
|
-
id="filter-country"
|
|
4732
|
-
name="filterCountry"
|
|
4733
|
-
options={countries}
|
|
4734
|
-
allowEmpty
|
|
4735
|
-
emptyLabel="All countries"
|
|
4736
|
-
placeholder="Select a country"
|
|
4737
|
-
/>`,
|
|
4738
|
-
storyPath: "data-entry/CountrySelect.stories.tsx",
|
|
4739
|
-
rules: [3, 6, 23, 31]
|
|
4740
|
-
},
|
|
4741
|
-
{
|
|
4742
|
-
name: "ChoiceField",
|
|
4743
|
-
group: "data-entry",
|
|
4744
|
-
tagline: "Layout wrapper that pairs a checkbox/radio control with a Label and optional description \u2014 the `id` prop MUST match the control's `id` for the label click to work.",
|
|
4745
|
-
props: [
|
|
4746
|
-
{
|
|
4747
|
-
name: "id",
|
|
4748
|
-
type: "string",
|
|
4749
|
-
required: true,
|
|
4750
|
-
description: "ID shared with the inner control (checkbox/radio). Used as `htmlFor` on the Label so clicking the label toggles the control. Must be unique per page."
|
|
4751
|
-
},
|
|
4752
|
-
{
|
|
4753
|
-
name: "label",
|
|
4754
|
-
type: "React.ReactNode",
|
|
4755
|
-
required: true,
|
|
4756
|
-
description: "The visible label text rendered beside the control. Accepts rich content (string, JSX, badges, etc.)."
|
|
4757
|
-
},
|
|
4758
|
-
{
|
|
4759
|
-
name: "description",
|
|
4760
|
-
type: "React.ReactNode",
|
|
4761
|
-
description: "Optional secondary line rendered below the label as a <p> element. Use for help text, hints, or elaborating on the choice."
|
|
4762
|
-
},
|
|
4763
|
-
{
|
|
4764
|
-
name: "className",
|
|
4765
|
-
type: "string",
|
|
4766
|
-
description: "Extra CSS classes merged onto the outer wrapper div (ui-choice-field)."
|
|
4767
|
-
},
|
|
4768
|
-
{
|
|
4769
|
-
name: "children",
|
|
4770
|
-
type: "React.ReactNode",
|
|
4771
|
-
required: true,
|
|
4772
|
-
description: "The interactive control to render \u2014 must be a single Checkbox or Radio.Item element with an `id` matching the `id` prop."
|
|
4773
|
-
}
|
|
4774
|
-
],
|
|
4775
|
-
usage: [
|
|
4776
|
-
"DO: always pass the same value to both `id` on ChoiceField and `id` on the inner control (Checkbox / Radio.Item). The Label uses `htmlFor={id}` \u2014 mismatching IDs breaks click-to-toggle.",
|
|
4777
|
-
"DO: use `React.useId()` to generate unique IDs when rendering ChoiceField inside a list or map, as the parent Radio.Group and CheckboxGroup already do internally.",
|
|
4778
|
-
"DON'T: use ChoiceField to wrap a raw `<input type='checkbox'>` or `<input type='radio'>` \u2014 always use the godx-ui Checkbox or Radio.Item primitives as children.",
|
|
4779
|
-
"DON'T: hand-roll this layout (flex + label + description paragraph) yourself. ChoiceField already provides the correct ui-choice-field / ui-choice-control / ui-choice-content / ui-choice-label / ui-choice-description class structure.",
|
|
4780
|
-
"PREFER: Radio.Group or Checkbox.Group with the `options` prop when you have a list of choices \u2014 they create ChoiceField internally with auto-generated IDs. Only reach for bare ChoiceField for custom compositions.",
|
|
4781
|
-
"SKIP ChoiceField for standalone checkboxes that need NO label or description \u2014 use Checkbox directly in that case."
|
|
4782
|
-
],
|
|
4783
|
-
useCases: [
|
|
4784
|
-
"Rendering a single opt-in checkbox with a descriptive sub-line, e.g. 'Receive email notifications / You can unsubscribe at any time'.",
|
|
4785
|
-
"Custom radio group where each option needs a rich label (e.g. icon + text + badge) that the standard options API cannot express.",
|
|
4786
|
-
"Building an agreement / terms-of-service checkbox where the label is a React node with a link inside.",
|
|
4787
|
-
"Composing a vertically or horizontally oriented list of choices when you need full control over each item's checked state and ID.",
|
|
4788
|
-
"Wrapping a Radix-based Radio.Item or Checkbox inside a settings panel where each row needs a two-line label + description layout.",
|
|
4789
|
-
"Accessibility-correct label pairing when a third-party or custom control must appear beside descriptive text and clicking the text must activate the control."
|
|
4790
|
-
],
|
|
4791
|
-
related: [
|
|
4792
|
-
"Radio.Group / RadioGroup \u2014 use this (not bare ChoiceField) for a full group of radio options; it creates ChoiceField internally with correct IDs and orientation.",
|
|
4793
|
-
"Checkbox.Group / CheckboxGroup \u2014 same as Radio.Group but for multi-select; wraps ChoiceField automatically when you pass the `options` prop.",
|
|
4794
|
-
"Checkbox \u2014 use standalone (no ChoiceField) when you need a bare control with no label or when the label is already provided by a FormField wrapper.",
|
|
4795
|
-
"Radio / Radio.Item \u2014 the inner control that goes inside ChoiceField as children in a custom radio composition.",
|
|
4796
|
-
"Label \u2014 ChoiceField renders Label internally; do NOT add a second Label around ChoiceField or you will double-label the control."
|
|
4797
|
-
],
|
|
4798
|
-
example: `{\`import { Checkbox, Radio, ChoiceField } from "@godxjp/ui/data-entry";
|
|
4799
|
-
import * as React from "react";
|
|
4800
|
-
|
|
4801
|
-
// Example 1: Custom checkbox with description
|
|
4802
|
-
function NotificationToggle() {
|
|
4803
|
-
const id = React.useId();
|
|
4804
|
-
const [checked, setChecked] = React.useState(false);
|
|
4805
|
-
|
|
4806
|
-
return (
|
|
4807
|
-
<ChoiceField
|
|
4808
|
-
id={id}
|
|
4809
|
-
label="Receive email notifications"
|
|
4810
|
-
description="We will send updates about your account activity."
|
|
4811
|
-
>
|
|
4812
|
-
<Checkbox
|
|
4813
|
-
id={id}
|
|
4814
|
-
checked={checked}
|
|
4815
|
-
onCheckedChange={(v) => setChecked(Boolean(v))}
|
|
4816
|
-
/>
|
|
4817
|
-
</ChoiceField>
|
|
4818
|
-
);
|
|
4819
|
-
}
|
|
4820
|
-
|
|
4821
|
-
// Example 2: Custom radio item with rich label
|
|
4822
|
-
function PlanOption({ planId, name, price }: { planId: string; name: string; price: string }) {
|
|
4823
|
-
const id = \\\`plan-\\\${planId}\\\`;
|
|
4824
|
-
return (
|
|
4825
|
-
<ChoiceField
|
|
4826
|
-
id={id}
|
|
4827
|
-
label={<span className="font-semibold">{name}</span>}
|
|
4828
|
-
description={\\\`\\\${price} / month\\\`}
|
|
4829
|
-
>
|
|
4830
|
-
<Radio.Item id={id} value={planId} />
|
|
4831
|
-
</ChoiceField>
|
|
4832
|
-
);
|
|
4833
|
-
}\`}`,
|
|
4834
|
-
storyPath: "data-entry/ChoiceField.stories.tsx",
|
|
4835
|
-
rules: [6, 13, 23, 31]
|
|
4836
|
-
},
|
|
4837
4337
|
{
|
|
4838
4338
|
name: "Command",
|
|
4839
4339
|
group: "data-entry",
|
|
@@ -5066,14 +4566,14 @@ function AccountQuickPick({ onSelect }: { onSelect: (id: string) => void }) {
|
|
|
5066
4566
|
{
|
|
5067
4567
|
name: "children",
|
|
5068
4568
|
type: "React.ReactNode",
|
|
5069
|
-
description: "Manual children mode: used when `options` is omitted or empty. Render Checkbox items directly as children. You are responsible for composing each Checkbox with a
|
|
4569
|
+
description: "Manual children mode: used when `options` is omitted or empty. Render Checkbox items directly as children. You are responsible for composing each Checkbox with a Field for correct label/description layout."
|
|
5070
4570
|
}
|
|
5071
4571
|
],
|
|
5072
4572
|
usage: [
|
|
5073
|
-
"DO use the `options` prop for any data-driven list \u2014 it auto-generates IDs, handles checked state, and wires up
|
|
4573
|
+
"DO use the `options` prop for any data-driven list \u2014 it auto-generates IDs, handles checked state, and wires up Field (label + description) for each item. NEVER hand-roll individual `<Checkbox>` elements inside a loop when you have an options array.",
|
|
5074
4574
|
"DO pass `name` when inside an HTML form so each checkbox submits its value under the same field name, giving the server a multi-value array. Without `name`, native form submission silently drops all values.",
|
|
5075
4575
|
"Controlled vs uncontrolled: pass `value` + `onChange` together for controlled usage (e.g. react-hook-form). Pass `defaultValue` alone for uncontrolled usage. Do NOT mix both \u2014 if `value` is provided, `defaultValue` is ignored and onChange must update value externally or the UI freezes.",
|
|
5076
|
-
"Each option's `description` renders as a secondary line below its label via
|
|
4576
|
+
"Each option's `description` renders as a secondary line below its label via Field \u2014 use it for help text or sub-copy; keep `label` short.",
|
|
5077
4577
|
"Group-level `disabled` disables all checkboxes. Individual `options[n].disabled` disables only that item. Both can coexist.",
|
|
5078
4578
|
'DO NOT wrap this inside another ARIA group or fieldset without removing the built-in `role="group"` \u2014 it already provides the correct grouping semantics. Pair the group with a `<legend>` or visible heading for a11y.'
|
|
5079
4579
|
],
|
|
@@ -5089,7 +4589,7 @@ function AccountQuickPick({ onSelect }: { onSelect: (id: string) => void }) {
|
|
|
5089
4589
|
"RadioGroup \u2014 use when only ONE selection is allowed at a time (mutually exclusive). CheckboxGroup = multiple, RadioGroup = single.",
|
|
5090
4590
|
"Checkbox (standalone) \u2014 use a bare `Checkbox` for a single boolean toggle (e.g. 'I agree to terms'). Use CheckboxGroup when you have 2+ related choices.",
|
|
5091
4591
|
"Checkbox.Group \u2014 alias; the same component is also accessible as `Checkbox.Group` (the Checkbox export attaches CheckboxGroup as `.Group`). Both are equivalent \u2014 prefer the named `CheckboxGroup` import for clarity in larger files.",
|
|
5092
|
-
"Switch /
|
|
4592
|
+
"Switch / Field \u2014 for a single binary on/off toggle with immediate effect (not a form submission value). Do not use CheckboxGroup to fake toggle rows.",
|
|
5093
4593
|
"Select (multi) \u2014 for a long list (10+ items) where space is limited; CheckboxGroup is better for \u226410 visible options that benefit from scanning all at once."
|
|
5094
4594
|
],
|
|
5095
4595
|
example: `import { CheckboxGroup } from "@godxjp/ui/data-entry";
|
|
@@ -5153,7 +4653,7 @@ export function ControlledExample() {
|
|
|
5153
4653
|
{
|
|
5154
4654
|
name: "options",
|
|
5155
4655
|
type: "ChoiceOptionProp[]",
|
|
5156
|
-
description: "Declarative option list: { label: ReactNode; value: string; disabled?: boolean; description?: ReactNode }[]. When provided, Radio.Group renders each option as a labelled
|
|
4656
|
+
description: "Declarative option list: { label: ReactNode; value: string; disabled?: boolean; description?: ReactNode }[]. When provided, Radio.Group renders each option as a labelled Field automatically. Omit to compose children manually."
|
|
5157
4657
|
},
|
|
5158
4658
|
{
|
|
5159
4659
|
name: "orientation",
|
|
@@ -5179,15 +4679,15 @@ export function ControlledExample() {
|
|
|
5179
4679
|
{
|
|
5180
4680
|
name: "children",
|
|
5181
4681
|
type: "React.ReactNode",
|
|
5182
|
-
description: "Manual composition fallback \u2014 used only when options is not provided. Render Radio.Item (+
|
|
4682
|
+
description: "Manual composition fallback \u2014 used only when options is not provided. Render Radio.Item (+ Field wrapper) children directly inside Radio.Group."
|
|
5183
4683
|
}
|
|
5184
4684
|
],
|
|
5185
4685
|
usage: [
|
|
5186
4686
|
"DO use Radio.Group (not the bare Radio export) as the root \u2014 it wires up Radix context, keyboard navigation, and the hidden form input. A lone Radio.Item outside a Radio.Group has no context and will not function.",
|
|
5187
|
-
"DO prefer the options array API for static/data-driven option lists: pass options={[{ label, value, description?, disabled? }]} and Radio.Group renders each as a correctly-labelled
|
|
4687
|
+
"DO prefer the options array API for static/data-driven option lists: pass options={[{ label, value, description?, disabled? }]} and Radio.Group renders each as a correctly-labelled Field automatically \u2014 no manual id/label wiring needed.",
|
|
5188
4688
|
"DO pass name to Radio.Group when the selection must be submitted via a native HTML form. Radix injects a hidden <input name={name} value={selected}> so the value is picked up by FormData/fetch without extra wiring.",
|
|
5189
4689
|
"DO use controlled mode (value + onValueChange) when the selection drives other UI (conditional fields, preview panels). Use defaultValue for fire-and-forget uncontrolled forms.",
|
|
5190
|
-
"DON'T hand-roll a label-plus-radio row with raw <input type='radio'> \u2014 use Radio.Group with options or compose Radio.Item inside
|
|
4690
|
+
"DON'T hand-roll a label-plus-radio row with raw <input type='radio'> \u2014 use Radio.Group with options or compose Radio.Item inside Field for custom markup. Every option must be wrapped in Field (or equivalent) for the label htmlFor/id linkage.",
|
|
5191
4691
|
"DON'T disable individual options inside the options array and ALSO set disabled on the group \u2014 group-level disabled wins and overrides all per-item disabled states."
|
|
5192
4692
|
],
|
|
5193
4693
|
useCases: [
|
|
@@ -5200,7 +4700,7 @@ export function ControlledExample() {
|
|
|
5200
4700
|
],
|
|
5201
4701
|
related: [
|
|
5202
4702
|
"Checkbox.Group \u2014 use when users may select multiple options simultaneously; Radio.Group enforces single-selection only.",
|
|
5203
|
-
"Switch /
|
|
4703
|
+
"Switch / Field \u2014 use for a single boolean on/off toggle (e.g. enable notifications); Radio.Group is for choosing one among three or more named options.",
|
|
5204
4704
|
"Select \u2014 use when there are many options (5+) and vertical screen space is limited; Radio.Group is preferable for 2-4 short options where all choices should be visible at a glance."
|
|
5205
4705
|
],
|
|
5206
4706
|
example: `{\`import { Radio } from "@godxjp/ui/data-entry";
|
|
@@ -5231,7 +4731,7 @@ function CustomRadioGroup() {
|
|
|
5231
4731
|
return (
|
|
5232
4732
|
<Radio.Group name="account_type" defaultValue="asset">
|
|
5233
4733
|
<Radio.Item id="opt-asset" value="asset" />
|
|
5234
|
-
{/* wrap each item in
|
|
4734
|
+
{/* wrap each item in Field for label + description */}
|
|
5235
4735
|
</Radio.Group>
|
|
5236
4736
|
);
|
|
5237
4737
|
}\`}`,
|
|
@@ -5239,282 +4739,20 @@ function CustomRadioGroup() {
|
|
|
5239
4739
|
rules: [3, 6, 13, 23]
|
|
5240
4740
|
},
|
|
5241
4741
|
{
|
|
5242
|
-
name: "
|
|
5243
|
-
group: "data-
|
|
5244
|
-
|
|
5245
|
-
tagline: "DEPRECATED searchable single-select combobox (static or async) \u2014 use <Select options showSearch> instead; SearchSelect stays exported but Select is now the single data-driven entry point.",
|
|
4742
|
+
name: "Popover",
|
|
4743
|
+
group: "data-display",
|
|
4744
|
+
tagline: "Radix-backed floating panel anchored to a trigger \u2014 always compose with PopoverTrigger + PopoverContent; never use a raw div overlay.",
|
|
5246
4745
|
props: [
|
|
5247
4746
|
{
|
|
5248
|
-
name: "
|
|
5249
|
-
type: "
|
|
5250
|
-
|
|
5251
|
-
description: "Controlled selected value. Empty string means nothing selected."
|
|
5252
|
-
},
|
|
5253
|
-
{
|
|
5254
|
-
name: "onChange",
|
|
5255
|
-
type: "(value: string, option?: SearchSelectOptionProp) => void",
|
|
5256
|
-
description: "Called with the new value string and the full option object on selection, or with ('', undefined) when cleared."
|
|
4747
|
+
name: "open",
|
|
4748
|
+
type: "boolean",
|
|
4749
|
+
description: "Controls open state in controlled mode. Pair with onOpenChange."
|
|
5257
4750
|
},
|
|
5258
4751
|
{
|
|
5259
|
-
name: "
|
|
5260
|
-
type: "
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
{
|
|
5264
|
-
name: "loadOptions",
|
|
5265
|
-
type: "(params: SearchSelectLoadParamsProp) => Promise<SearchSelectLoadResultProp>",
|
|
5266
|
-
description: "Async fetcher called with { query, page } \u2014 supports debounced search and infinite-scroll pagination. Provide this OR options, not both."
|
|
5267
|
-
},
|
|
5268
|
-
{
|
|
5269
|
-
name: "renderOption",
|
|
5270
|
-
type: "(option: SearchSelectOptionProp) => React.ReactNode",
|
|
5271
|
-
description: "Custom per-option renderer (Ant-Design style). Defaults to label + optional sublabel layout."
|
|
5272
|
-
},
|
|
5273
|
-
{
|
|
5274
|
-
name: "selectedLabel",
|
|
5275
|
-
type: "string",
|
|
5276
|
-
description: "Label to display for the current value when its option is not in the currently loaded page (prevents a flash of the raw ID string)."
|
|
5277
|
-
},
|
|
5278
|
-
{
|
|
5279
|
-
name: "placeholder",
|
|
5280
|
-
type: "string",
|
|
5281
|
-
description: "Trigger button placeholder when no value is selected. Defaults to i18n key dataEntry.searchSelect.placeholder."
|
|
5282
|
-
},
|
|
5283
|
-
{
|
|
5284
|
-
name: "searchPlaceholder",
|
|
5285
|
-
type: "string",
|
|
5286
|
-
description: "Placeholder inside the search input inside the popover. Defaults to i18n key dataEntry.searchSelect.search."
|
|
5287
|
-
},
|
|
5288
|
-
{
|
|
5289
|
-
name: "emptyMessage",
|
|
5290
|
-
type: "string",
|
|
5291
|
-
description: "Message shown when no options match the search query. Defaults to i18n key dataEntry.searchSelect.empty."
|
|
5292
|
-
},
|
|
5293
|
-
{
|
|
5294
|
-
name: "loadingMessage",
|
|
5295
|
-
type: "string",
|
|
5296
|
-
description: "Message shown during async fetch. Defaults to i18n key dataEntry.searchSelect.loading."
|
|
5297
|
-
},
|
|
5298
|
-
{
|
|
5299
|
-
name: "clearLabel",
|
|
5300
|
-
type: "string",
|
|
5301
|
-
description: "Label for the clear row that appears at the top when a value is selected and clearable is true."
|
|
5302
|
-
},
|
|
5303
|
-
{
|
|
5304
|
-
name: "clearable",
|
|
5305
|
-
type: "boolean",
|
|
5306
|
-
defaultValue: "true",
|
|
5307
|
-
description: "Show a clear row at the top of the list when a value is selected."
|
|
5308
|
-
},
|
|
5309
|
-
{
|
|
5310
|
-
name: "disabled",
|
|
5311
|
-
type: "boolean",
|
|
5312
|
-
defaultValue: "false",
|
|
5313
|
-
description: "Disables the trigger button and prevents opening the popover."
|
|
5314
|
-
},
|
|
5315
|
-
{
|
|
5316
|
-
name: "name",
|
|
5317
|
-
type: "string",
|
|
5318
|
-
description: "HTML form field name \u2014 injects a hidden <input> so the selected value submits with native forms."
|
|
5319
|
-
},
|
|
5320
|
-
{
|
|
5321
|
-
name: "id",
|
|
5322
|
-
type: "string",
|
|
5323
|
-
description: "ID forwarded to the trigger button, used to associate a <label htmlFor>."
|
|
5324
|
-
},
|
|
5325
|
-
{
|
|
5326
|
-
name: "className",
|
|
5327
|
-
type: "string",
|
|
5328
|
-
description: "Additional Tailwind classes applied to the trigger button (w-full by default)."
|
|
5329
|
-
},
|
|
5330
|
-
{
|
|
5331
|
-
name: "data-testid",
|
|
5332
|
-
type: "string",
|
|
5333
|
-
description: "Test ID on the trigger button. Each option gets a derived ID: ${data-testid}-option-${value}. The clear row gets ${data-testid}-option-none."
|
|
5334
|
-
}
|
|
5335
|
-
],
|
|
5336
|
-
usage: [
|
|
5337
|
-
"DEPRECATED \u2014 prefer <Select options={...} showSearch /> or <Select loadOptions={...} showSearch /> which uses the same engine internally. SearchSelect remains exported for backwards compatibility only.",
|
|
5338
|
-
"Provide exactly ONE of `options` (static, client-side filtered) or `loadOptions` (async, debounced, paginated). Passing both is unsupported; loadOptions takes precedence.",
|
|
5339
|
-
"Always pass `name` when used inside a native <form> so the hidden input submits the value correctly. Without `name` the selection does not participate in FormData.",
|
|
5340
|
-
"When using `loadOptions` with a paginated endpoint, return `hasMore: true` in the result to enable infinite scroll \u2014 the component appends the next page when scrolled within 48px of the bottom.",
|
|
5341
|
-
"Pass `selectedLabel` when `value` might not appear in the first loaded page (e.g. an edit form pre-populated from the server) \u2014 otherwise the trigger shows the placeholder text instead of the selected item's label.",
|
|
5342
|
-
"Do NOT render a SearchSelect inside a form and also pass a compound <Select> \u2014 choose one API. For new code always prefer <Select options showSearch> from @godxjp/ui/data-entry to avoid the deprecated path."
|
|
5343
|
-
],
|
|
5344
|
-
useCases: [
|
|
5345
|
-
"Legacy code that already uses SearchSelect and has not yet been migrated to <Select options showSearch>.",
|
|
5346
|
-
"A vendor/account picker with a large server-side list: pass loadOptions calling your API endpoint, return pages of results with hasMore, and use selectedLabel to display the pre-saved label on an edit form.",
|
|
5347
|
-
"A client-side filtered dropdown over a moderate static list (e.g. currency codes, department names) where the list fits in memory \u2014 pass options array.",
|
|
5348
|
-
"A grouped picker (e.g. account chart by category) \u2014 add option.group to each option; the component auto-renders optgroup-style headings in first-seen order.",
|
|
5349
|
-
"Custom option rendering (e.g. showing an avatar + name + role) \u2014 pass renderOption returning JSX; the default label+sublabel layout is bypassed."
|
|
5350
|
-
],
|
|
5351
|
-
related: [
|
|
5352
|
-
"Select \u2014 THE modern replacement: pass options or loadOptions to <Select> and add showSearch to get the same combobox behavior. For all new code use Select, not SearchSelect.",
|
|
5353
|
-
"Autocomplete \u2014 also deprecated; Autocomplete is a thin wrapper around SearchSelect kept for older call-sites. Do not use for new code.",
|
|
5354
|
-
"Command \u2014 low-level primitive (Popover + Command list) that SearchSelect is built on; reach for it only if you need a fully custom command-palette UI that does not fit either Select or SearchSelect."
|
|
5355
|
-
],
|
|
5356
|
-
example: `import { SearchSelect } from "@godxjp/ui/data-entry";
|
|
5357
|
-
|
|
5358
|
-
// Static list (client-side filtered) \u2014 DEPRECATED pattern, prefer <Select options showSearch>
|
|
5359
|
-
function LegacyAccountPicker({ value, onChange }) {
|
|
5360
|
-
return (
|
|
5361
|
-
<SearchSelect
|
|
5362
|
-
value={value}
|
|
5363
|
-
onValueChange={onChange}
|
|
5364
|
-
options={[
|
|
5365
|
-
{ value: "acc-001", label: "Cash", sublabel: "Current assets", group: "Assets" },
|
|
5366
|
-
{ value: "acc-002", label: "Accounts Receivable", group: "Assets" },
|
|
5367
|
-
{ value: "acc-010", label: "Revenue", group: "Income" },
|
|
5368
|
-
]}
|
|
5369
|
-
placeholder="Select account"
|
|
5370
|
-
name="account_id"
|
|
5371
|
-
data-testid="account-picker"
|
|
5372
|
-
/>
|
|
5373
|
-
);
|
|
5374
|
-
}
|
|
5375
|
-
|
|
5376
|
-
// Async / paginated \u2014 DEPRECATED pattern, prefer <Select loadOptions showSearch>
|
|
5377
|
-
async function fetchVendors({ query, page }) {
|
|
5378
|
-
const res = await fetch(\`/api/vendors?q=\${query}&page=\${page}\`);
|
|
5379
|
-
const json = await res.json();
|
|
5380
|
-
return { options: json.data, hasMore: json.meta.hasNextPage };
|
|
5381
|
-
}
|
|
5382
|
-
|
|
5383
|
-
function LegacyVendorPicker({ value, currentVendorName, onChange }) {
|
|
5384
|
-
return (
|
|
5385
|
-
<SearchSelect
|
|
5386
|
-
value={value}
|
|
5387
|
-
onValueChange={onChange}
|
|
5388
|
-
loadOptions={fetchVendors}
|
|
5389
|
-
selectedLabel={currentVendorName}
|
|
5390
|
-
placeholder="Select vendor"
|
|
5391
|
-
name="vendor_id"
|
|
5392
|
-
data-testid="vendor-picker"
|
|
5393
|
-
/>
|
|
5394
|
-
);
|
|
5395
|
-
}`,
|
|
5396
|
-
storyPath: "data-entry/SearchSelect.stories.tsx",
|
|
5397
|
-
rules: [3, 6, 33]
|
|
5398
|
-
},
|
|
5399
|
-
{
|
|
5400
|
-
name: "Autocomplete",
|
|
5401
|
-
group: "data-entry",
|
|
5402
|
-
deprecated: true,
|
|
5403
|
-
tagline: "DEPRECATED thin wrapper over SearchSelect \u2014 use <Select options showSearch> instead; kept only for backward compatibility.",
|
|
5404
|
-
props: [
|
|
5405
|
-
{
|
|
5406
|
-
name: "options",
|
|
5407
|
-
type: "{ value: string; label: string }[]",
|
|
5408
|
-
required: true,
|
|
5409
|
-
description: "Static list of option rows. Each entry must have a string value and a display label."
|
|
5410
|
-
},
|
|
5411
|
-
{
|
|
5412
|
-
name: "value",
|
|
5413
|
-
type: "string",
|
|
5414
|
-
description: "Controlled selected value. When provided the component is fully controlled; omit for uncontrolled."
|
|
5415
|
-
},
|
|
5416
|
-
{
|
|
5417
|
-
name: "onValueChange",
|
|
5418
|
-
type: "(value: string) => void",
|
|
5419
|
-
description: "Callback fired with the newly selected string value. Required for controlled usage."
|
|
5420
|
-
},
|
|
5421
|
-
{
|
|
5422
|
-
name: "placeholder",
|
|
5423
|
-
type: "string",
|
|
5424
|
-
description: "Trigger button placeholder shown when no value is selected."
|
|
5425
|
-
},
|
|
5426
|
-
{
|
|
5427
|
-
name: "searchPlaceholder",
|
|
5428
|
-
type: "string",
|
|
5429
|
-
description: "Placeholder text inside the search input in the dropdown."
|
|
5430
|
-
},
|
|
5431
|
-
{
|
|
5432
|
-
name: "emptyMessage",
|
|
5433
|
-
type: "string",
|
|
5434
|
-
description: "Message shown in the dropdown when no options match the search query."
|
|
5435
|
-
},
|
|
5436
|
-
{
|
|
5437
|
-
name: "disabled",
|
|
5438
|
-
type: "boolean",
|
|
5439
|
-
defaultValue: "false",
|
|
5440
|
-
description: "Disables the control entirely \u2014 trigger becomes non-interactive."
|
|
5441
|
-
},
|
|
5442
|
-
{
|
|
5443
|
-
name: "className",
|
|
5444
|
-
type: "string",
|
|
5445
|
-
description: "Extra Tailwind classes applied to the trigger wrapper."
|
|
5446
|
-
},
|
|
5447
|
-
{
|
|
5448
|
-
name: "id",
|
|
5449
|
-
type: "string",
|
|
5450
|
-
description: "HTML id forwarded to the underlying trigger element; wire to a <label> for a11y."
|
|
5451
|
-
}
|
|
5452
|
-
],
|
|
5453
|
-
usage: [
|
|
5454
|
-
"DEPRECATED \u2014 do NOT use for new code. Replace with `<Select options={opts} showSearch />` (single data-driven entry point) or `<SearchSelect options={opts} />` for more control. Autocomplete is a thin shim kept only for backward compatibility.",
|
|
5455
|
-
"DO pass a controlled `value` + `onValueChange` pair to keep state outside; without `value` the component is uncontrolled and you cannot read the selected item.",
|
|
5456
|
-
"DO NOT expect optgroup grouping, sublabels, async loading, or a custom `renderOption` \u2014 Autocomplete's option type only has `{ value, label }`. Use SearchSelect or Select with showSearch for those features.",
|
|
5457
|
-
"DO NOT use this inside an HTML `<form>` for native form submission \u2014 there is no `name` prop and no hidden input. Pair with React Hook Form or manage state manually via `onValueChange`.",
|
|
5458
|
-
"Always provide a matching `<label htmlFor={id}>` when using `id` for screen-reader accessibility.",
|
|
5459
|
-
"The internal clearable button is always hidden (hardcoded `clearable={false}`). If you need a clear/reset action use `<SearchSelect clearable />` or `<Select showSearch clearable />`."
|
|
5460
|
-
],
|
|
5461
|
-
useCases: [
|
|
5462
|
-
"Migrating legacy code that already imports Autocomplete \u2014 keep it running without a rewrite while you schedule the migration to Select/SearchSelect.",
|
|
5463
|
-
"Simple static list with client-side search where no grouping, sublabels, or async fetch is needed \u2014 though even here, prefer Select with showSearch for future-proofing.",
|
|
5464
|
-
"Rapid prototyping where the exact API doesn't matter yet and you know it will be replaced before shipping."
|
|
5465
|
-
],
|
|
5466
|
-
related: [
|
|
5467
|
-
"Select (with showSearch + options) \u2014 the CURRENT canonical replacement for Autocomplete; supports grouping, sublabels, async loadOptions, custom renderOption, clearable, multi, and form name prop. Use this for all new code.",
|
|
5468
|
-
"SearchSelect \u2014 the direct engine Autocomplete delegates to; exported and stable, supports optgroups, sublabels, async infinite-scroll, and clearable. Use if you need SearchSelect-specific async or render props not yet exposed by Select.",
|
|
5469
|
-
"Input with datalist \u2014 never hand-roll; use Select/SearchSelect primitives instead."
|
|
5470
|
-
],
|
|
5471
|
-
example: `{\`// \u274C DEPRECATED \u2014 do not use in new code
|
|
5472
|
-
import { Autocomplete } from "@godxjp/ui/data-entry";
|
|
5473
|
-
|
|
5474
|
-
// \u2705 Replace with:
|
|
5475
|
-
// import { Select } from "@godxjp/ui/data-entry";
|
|
5476
|
-
// <Select options={options} showSearch placeholder="Search\u2026" onValueChange={setValue} value={value} />
|
|
5477
|
-
|
|
5478
|
-
// Legacy usage (backward compat only):
|
|
5479
|
-
import { Autocomplete } from "@godxjp/ui/data-entry";
|
|
5480
|
-
|
|
5481
|
-
const options = [
|
|
5482
|
-
{ value: "acme", label: "Acme Corp" },
|
|
5483
|
-
{ value: "globex", label: "Globex Inc" },
|
|
5484
|
-
];
|
|
5485
|
-
|
|
5486
|
-
function LegacyVendorPicker() {
|
|
5487
|
-
const [value, setValue] = React.useState("");
|
|
5488
|
-
return (
|
|
5489
|
-
<Autocomplete
|
|
5490
|
-
id="vendor"
|
|
5491
|
-
options={options}
|
|
5492
|
-
value={value}
|
|
5493
|
-
onValueChange={setValue}
|
|
5494
|
-
placeholder="Select vendor\u2026"
|
|
5495
|
-
searchPlaceholder="Search vendors\u2026"
|
|
5496
|
-
emptyMessage="No vendor found"
|
|
5497
|
-
/>
|
|
5498
|
-
);
|
|
5499
|
-
}\`}`,
|
|
5500
|
-
storyPath: "data-entry/Autocomplete.stories.tsx",
|
|
5501
|
-
rules: [6, 13, 23, 31]
|
|
5502
|
-
},
|
|
5503
|
-
{
|
|
5504
|
-
name: "Popover",
|
|
5505
|
-
group: "data-display",
|
|
5506
|
-
tagline: "Radix-backed floating panel anchored to a trigger \u2014 always compose with PopoverTrigger + PopoverContent; never use a raw div overlay.",
|
|
5507
|
-
props: [
|
|
5508
|
-
{
|
|
5509
|
-
name: "open",
|
|
5510
|
-
type: "boolean",
|
|
5511
|
-
description: "Controls open state in controlled mode. Pair with onOpenChange."
|
|
5512
|
-
},
|
|
5513
|
-
{
|
|
5514
|
-
name: "defaultOpen",
|
|
5515
|
-
type: "boolean",
|
|
5516
|
-
defaultValue: "false",
|
|
5517
|
-
description: "Initial open state for uncontrolled usage."
|
|
4752
|
+
name: "defaultOpen",
|
|
4753
|
+
type: "boolean",
|
|
4754
|
+
defaultValue: "false",
|
|
4755
|
+
description: "Initial open state for uncontrolled usage."
|
|
5518
4756
|
},
|
|
5519
4757
|
{
|
|
5520
4758
|
name: "onOpenChange",
|
|
@@ -5771,525 +5009,106 @@ export function ControlledPopover() {
|
|
|
5771
5009
|
"Accordion (from @godxjp/ui/data-entry or Radix) \u2014 use Accordion when only ONE section can be open at a time across a group; use Collapsible when each section is independent and can be open simultaneously.",
|
|
5772
5010
|
"Popover \u2014 use Popover when the revealed content should float above the layout in a portal overlay; use Collapsible when the content should push surrounding content down inline.",
|
|
5773
5011
|
"Dialog/Sheet \u2014 use Dialog or Sheet for modal or slide-over panels that demand full user attention; Collapsible stays in-flow and non-modal.",
|
|
5774
|
-
"TreeList (@godxjp/ui/data-display) \u2014 use TreeList for hierarchical data that needs recursive nesting with built-in indentation and expand/collapse; use Collapsible for ad-hoc single-level toggle regions."
|
|
5775
|
-
],
|
|
5776
|
-
example: `{\`import { useState } from "react";
|
|
5777
|
-
import { ChevronDown } from "lucide-react";
|
|
5778
|
-
import {
|
|
5779
|
-
Collapsible,
|
|
5780
|
-
CollapsibleTrigger,
|
|
5781
|
-
CollapsibleContent,
|
|
5782
|
-
} from "@godxjp/ui/data-display";
|
|
5783
|
-
import { Button } from "@godxjp/ui";
|
|
5784
|
-
|
|
5785
|
-
// --- Uncontrolled (simplest) ---
|
|
5786
|
-
export function InvoiceLineDetail() {
|
|
5787
|
-
return (
|
|
5788
|
-
<Collapsible>
|
|
5789
|
-
<CollapsibleTrigger asChild>
|
|
5790
|
-
<Button variant="ghost" size="sm">
|
|
5791
|
-
<ChevronDown className="mr-1 h-4 w-4" aria-hidden="true" />
|
|
5792
|
-
Show tax breakdown
|
|
5793
|
-
</Button>
|
|
5794
|
-
</CollapsibleTrigger>
|
|
5795
|
-
<CollapsibleContent>
|
|
5796
|
-
<div className="mt-2 rounded-md border p-3 text-sm">
|
|
5797
|
-
<p>Consumption tax (10%): \xA51,234</p>
|
|
5798
|
-
<p>Withholding tax: \xA50</p>
|
|
5799
|
-
</div>
|
|
5800
|
-
</CollapsibleContent>
|
|
5801
|
-
</Collapsible>
|
|
5802
|
-
);
|
|
5803
|
-
}
|
|
5804
|
-
|
|
5805
|
-
// --- Controlled (open driven externally) ---
|
|
5806
|
-
export function FilterSection() {
|
|
5807
|
-
const [open, setOpen] = useState(false);
|
|
5808
|
-
return (
|
|
5809
|
-
<Collapsible open={open} onOpenChange={setOpen}>
|
|
5810
|
-
<CollapsibleTrigger asChild>
|
|
5811
|
-
<Button variant="outline" size="sm">
|
|
5812
|
-
Advanced filters
|
|
5813
|
-
<ChevronDown className="ml-1 h-4 w-4" aria-hidden="true" />
|
|
5814
|
-
</Button>
|
|
5815
|
-
</CollapsibleTrigger>
|
|
5816
|
-
<CollapsibleContent>
|
|
5817
|
-
{/* place filter controls here */}
|
|
5818
|
-
<p className="mt-2 text-sm text-muted-foreground">Date range, entity, status\u2026</p>
|
|
5819
|
-
</CollapsibleContent>
|
|
5820
|
-
</Collapsible>
|
|
5821
|
-
);
|
|
5822
|
-
}\`}`,
|
|
5823
|
-
storyPath: "data-display/Collapsible.stories.tsx",
|
|
5824
|
-
rules: [3, 6, 23]
|
|
5825
|
-
},
|
|
5826
|
-
{
|
|
5827
|
-
name: "TreeList",
|
|
5828
|
-
group: "data-display",
|
|
5829
|
-
tagline: "Renders a flat array of items as an indented tree-style list with chevron + package icon; depth indentation is data-driven \u2014 never nest DOM manually.",
|
|
5830
|
-
props: [
|
|
5831
|
-
{
|
|
5832
|
-
name: "items",
|
|
5833
|
-
type: "TreeListItem[]",
|
|
5834
|
-
required: true,
|
|
5835
|
-
description: "Ordered flat array of items to render. Each item carries its own depth so the tree structure is expressed in data, not DOM nesting."
|
|
5836
|
-
}
|
|
5837
|
-
],
|
|
5838
|
-
usage: [
|
|
5839
|
-
"DO pass a flat array ordered top-to-bottom with each item's `depth` set to the correct nesting level (0 = root, 1 = first child, etc.). TreeList does NOT accept nested children \u2014 the tree shape is encoded in data.",
|
|
5840
|
-
"DO set `item.active = true` on the currently selected row; the component applies `data-active` for styling \u2014 never manually add an active class.",
|
|
5841
|
-
"DO use `item.badge` (ReactNode) to surface a secondary label (count, status chip) \u2014 it is rendered as a `Badge variant='secondary'` automatically; do NOT wrap the value in a Badge yourself.",
|
|
5842
|
-
"DON'T hand-roll padding or indentation \u2014 depth-based indentation is applied via `data-depth` CSS; adding manual padding breaks the visual rhythm.",
|
|
5843
|
-
"DON'T use TreeList for interactive selection (click handlers, routing) \u2014 it has no `onItemClick` prop. Wrap items in a navigation list or add a Link inside `item.title` when interactivity is needed.",
|
|
5844
|
-
"DO provide a unique string `item.id` for every item; it is used as the React key and must be stable across renders."
|
|
5845
|
-
],
|
|
5846
|
-
useCases: [
|
|
5847
|
-
"Displaying a chart-of-accounts hierarchy (root accounts at depth 0, sub-accounts at depth 1+) in an accounting admin panel.",
|
|
5848
|
-
"Showing a package/module dependency tree where each node has a name, optional description, and an item-count badge.",
|
|
5849
|
-
"Rendering a category tree (e.g., product categories, tax codes) in a read-only reference list alongside a detail panel.",
|
|
5850
|
-
"Listing a filtered/searched subset of a hierarchy \u2014 because the flat-array model lets you pre-filter server-side and still show correct depth context.",
|
|
5851
|
-
"Sidebar or drawer content showing a tree of navigation nodes where the active branch item is highlighted via `active: true`."
|
|
5852
|
-
],
|
|
5853
|
-
related: [
|
|
5854
|
-
"Timeline \u2014 use Timeline for chronological event sequences with timestamps; use TreeList for hierarchical parent-child structures.",
|
|
5855
|
-
"Descriptions \u2014 use Descriptions for label/value pairs; use TreeList when items have a parent-child depth relationship.",
|
|
5856
|
-
"DataTable \u2014 use DataTable for tabular data with columns, sorting, and selection; use TreeList for a single-column hierarchical list without those features.",
|
|
5857
|
-
"EmptyState \u2014 pair with EmptyState when the items array may be empty; TreeList renders nothing (no empty row) when given an empty array."
|
|
5858
|
-
],
|
|
5859
|
-
example: `import { TreeList } from "@godxjp/ui/data-display";
|
|
5860
|
-
|
|
5861
|
-
const accounts = [
|
|
5862
|
-
{ id: "1000", title: "Assets", depth: 0 },
|
|
5863
|
-
{ id: "1100", title: "Current Assets", depth: 1, active: true },
|
|
5864
|
-
{ id: "1110", title: "Cash & Equivalents", description: "Bank + petty cash", depth: 2, badge: "3 accounts" },
|
|
5865
|
-
{ id: "1120", title: "Accounts Receivable", depth: 2 },
|
|
5866
|
-
{ id: "2000", title: "Liabilities", depth: 0 },
|
|
5867
|
-
];
|
|
5868
|
-
|
|
5869
|
-
export function ChartOfAccounts() {
|
|
5870
|
-
return <TreeList items={accounts} />;
|
|
5871
|
-
}`,
|
|
5872
|
-
storyPath: "data-display/TreeList.stories.tsx",
|
|
5873
|
-
rules: [3, 6, 23, 31]
|
|
5874
|
-
},
|
|
5875
|
-
{
|
|
5876
|
-
name: "PageHeader",
|
|
5877
|
-
group: "navigation",
|
|
5878
|
-
deprecated: true,
|
|
5879
|
-
tagline: "DEPRECATED header-only shell \u2014 use PageContainer instead; PageHeader renders only the title/breadcrumb/actions strip with no body or footer slots.",
|
|
5880
|
-
props: [
|
|
5881
|
-
{
|
|
5882
|
-
name: "title",
|
|
5883
|
-
type: "React.ReactNode",
|
|
5884
|
-
required: true,
|
|
5885
|
-
description: "Page heading text rendered as an <h1>. Accepts a string or any ReactNode."
|
|
5886
|
-
},
|
|
5887
|
-
{
|
|
5888
|
-
name: "description",
|
|
5889
|
-
type: "React.ReactNode",
|
|
5890
|
-
description: "Optional subtitle rendered as a <p> below the title."
|
|
5891
|
-
},
|
|
5892
|
-
{
|
|
5893
|
-
name: "breadcrumb",
|
|
5894
|
-
type: "BreadcrumbItemProp[]",
|
|
5895
|
-
description: "Ordered breadcrumb trail. Each item is { label: ReactNode; to?: string }. The last item is always rendered as a plain span (current page); earlier items with 'to' are router <Link>s."
|
|
5896
|
-
},
|
|
5897
|
-
{
|
|
5898
|
-
name: "actions",
|
|
5899
|
-
type: "React.ReactNode",
|
|
5900
|
-
description: "Action controls (buttons, menus) rendered in the trailing slot of the header row. Equivalent to PageContainer's 'extra' prop."
|
|
5901
|
-
},
|
|
5902
|
-
{
|
|
5903
|
-
name: "className",
|
|
5904
|
-
type: "string",
|
|
5905
|
-
description: "Additional CSS class names applied to the <header> element."
|
|
5906
|
-
}
|
|
5907
|
-
],
|
|
5908
|
-
usage: [
|
|
5909
|
-
"DEPRECATED \u2014 always prefer PageContainer for new pages. PageContainer provides body and footer slots, density/variant controls, and sticky footer support that PageHeader lacks.",
|
|
5910
|
-
"DO pass breadcrumb items in order from root to current page. The last item is automatically marked aria-current='page' and rendered without a link regardless of whether 'to' is set.",
|
|
5911
|
-
"DON'T place body content as children \u2014 PageHeader has no children slot. Use PageContainer with its children prop for page body content.",
|
|
5912
|
-
"The 'actions' prop (not 'extra') is the slot for action buttons or menus in the trailing position. Note that PageContainer uses 'extra' for the same slot \u2014 they are intentionally different prop names.",
|
|
5913
|
-
"Use 'description' (not 'subtitle') for the secondary text beneath the title \u2014 again, PageContainer uses 'subtitle' for the same concept.",
|
|
5914
|
-
"If you must keep PageHeader for a legacy page, avoid adding new body layout inside the same file; migrate to PageContainer to get density and variant props for responsive layout."
|
|
5915
|
-
],
|
|
5916
|
-
useCases: [
|
|
5917
|
-
"Maintaining or reading legacy pages that already use PageHeader and cannot be migrated in the current sprint.",
|
|
5918
|
-
"Quick title + breadcrumb strip for a read-only display panel that embeds inside another layout shell (not a full page).",
|
|
5919
|
-
"Regression tests and snapshot tests for the navigation module that must cover the legacy component path.",
|
|
5920
|
-
"Any scenario where you intentionally render only a header row with no body \u2014 though even then, PageContainer with no children is the preferred modern approach."
|
|
5921
|
-
],
|
|
5922
|
-
related: [
|
|
5923
|
-
"PageContainer \u2014 the current replacement for PageHeader; use this for all new pages. It adds children, footer, density, variant, stickyFooter, and uses 'subtitle'/'extra' instead of 'description'/'actions'.",
|
|
5924
|
-
"AppShell \u2014 top-level layout shell that wraps sidebar, topbar, and the page area; PageContainer lives inside AppShell's content area.",
|
|
5925
|
-
"Topbar \u2014 the fixed top bar with product/project chips and search; distinct from the per-page header rendered by PageContainer/PageHeader."
|
|
5926
|
-
],
|
|
5927
|
-
example: `import { PageHeader } from "@godxjp/ui/navigation";
|
|
5928
|
-
import { Button } from "@godxjp/ui/general";
|
|
5929
|
-
|
|
5930
|
-
// DEPRECATED \u2014 use PageContainer for new pages.
|
|
5931
|
-
// Legacy usage only:
|
|
5932
|
-
export function LegacyInvoiceHeader() {
|
|
5933
|
-
return (
|
|
5934
|
-
<PageHeader
|
|
5935
|
-
title="Invoices"
|
|
5936
|
-
description="All issued invoices for the current entity"
|
|
5937
|
-
breadcrumb={[
|
|
5938
|
-
{ label: "Home", to: "/" },
|
|
5939
|
-
{ label: "Accounting", to: "/accounting" },
|
|
5940
|
-
{ label: "Invoices" }, // last item \u2014 no 'to', rendered as current page
|
|
5941
|
-
]}
|
|
5942
|
-
actions={
|
|
5943
|
-
<Button variant="default" size="sm">
|
|
5944
|
-
New Invoice
|
|
5945
|
-
</Button>
|
|
5946
|
-
}
|
|
5947
|
-
/>
|
|
5948
|
-
);
|
|
5949
|
-
}`,
|
|
5950
|
-
storyPath: "navigation/PageHeader.stories.tsx",
|
|
5951
|
-
rules: [3, 23, 24, 33]
|
|
5952
|
-
},
|
|
5953
|
-
{
|
|
5954
|
-
name: "LocalePicker",
|
|
5955
|
-
group: "navigation",
|
|
5956
|
-
tagline: "Language selector that reads/writes AppProvider locale automatically \u2014 throws if used without AppProvider AND without controlled value+onChange.",
|
|
5957
|
-
props: [
|
|
5958
|
-
{
|
|
5959
|
-
name: "value",
|
|
5960
|
-
type: "AppLocale",
|
|
5961
|
-
description: "Controlled locale value. Must be one of 'vi' | 'en' | 'ja'. When omitted, reads the current locale from AppProvider context."
|
|
5962
|
-
},
|
|
5963
|
-
{
|
|
5964
|
-
name: "onChange",
|
|
5965
|
-
type: "(locale: AppLocale) => void",
|
|
5966
|
-
description: "Controlled change handler. When omitted, calls AppProvider's setLocale. Required when value is provided without AppProvider."
|
|
5967
|
-
},
|
|
5968
|
-
{
|
|
5969
|
-
name: "className",
|
|
5970
|
-
type: "string",
|
|
5971
|
-
description: "Extra CSS classes merged onto the SelectTrigger element. Default trigger width is w-full sm:w-40."
|
|
5972
|
-
},
|
|
5973
|
-
{ name: "disabled", type: "boolean", description: "Disables the Select control." },
|
|
5974
|
-
{
|
|
5975
|
-
name: "id",
|
|
5976
|
-
type: "string",
|
|
5977
|
-
description: "HTML id forwarded to the SelectTrigger for label association."
|
|
5978
|
-
}
|
|
5979
|
-
],
|
|
5980
|
-
usage: [
|
|
5981
|
-
"DO: Wrap the component in AppProvider for zero-config uncontrolled use \u2014 locale is read and written via context automatically, no props needed. DO NOT use without AppProvider unless you also supply both value and onChange.",
|
|
5982
|
-
"DO: Use controlled mode (value + onChange) when you need to manage locale state outside of AppProvider \u2014 for example in a standalone settings form or a Storybook story. Both props are required together in this mode.",
|
|
5983
|
-
"DO NOT: Pass only value without onChange, or only onChange without value in controlled mode. The component throws at render time if neither AppProvider context nor both controlled props are present: 'LocalePicker requires <AppProvider> or controlled value + onChange'.",
|
|
5984
|
-
"DO: The locale list is fixed to APP_LOCALES = ['vi', 'en', 'ja']. Option labels are rendered via the translation system (t('locale.vi') etc.) \u2014 ensure AppProvider is initialized with the correct defaultLocale so labels display in the right language.",
|
|
5985
|
-
"DO: Use the id prop to associate a <label> element with the trigger for accessible forms. The trigger already carries an aria-label from the translation key navigation.localePicker.ariaLabel, so a visible label is optional but still preferred for sighted users.",
|
|
5986
|
-
"DON'T hand-roll a locale Select with raw <select> or godx-ui Select \u2014 LocalePicker already composes the full Select + Languages icon + translated options + context wiring. Use it directly."
|
|
5987
|
-
],
|
|
5988
|
-
useCases: [
|
|
5989
|
-
"App shell / top-nav language switcher that persists the user's locale preference via AppProvider and localStorage without any extra state.",
|
|
5990
|
-
"Settings page 'Language' field where locale is part of a form submitted to the backend \u2014 use controlled mode: value={form.locale} onValueChange={(v) => form.setLocale(v)}.",
|
|
5991
|
-
"Onboarding wizard step that lets the user pick their language before the rest of the app is configured \u2014 mount with AppProvider persist={false} and a controlled value to keep state local to the wizard.",
|
|
5992
|
-
"Admin user-profile form where locale is one of several preferences (alongside timezone and date/time format) \u2014 pair with TimezonePicker, DateFormatPicker, TimeFormatPicker under the same AppProvider.",
|
|
5993
|
-
"Storybook / test harness where AppProvider is not present \u2014 render in fully controlled mode: <LocalePicker value='en' onValueChange={fn} />.",
|
|
5994
|
-
"Localization QA tool that cycles through locales programmatically \u2014 drive via controlled value to switch the UI language without user interaction."
|
|
5995
|
-
],
|
|
5996
|
-
related: [
|
|
5997
|
-
"TimezonePicker \u2014 same family, same pattern (uncontrolled via AppProvider or controlled). Pick LocalePicker for language, TimezonePicker for IANA timezone.",
|
|
5998
|
-
"DateFormatPicker \u2014 picks 'dmy' | 'mdy' | 'iso' display format. Use alongside LocalePicker in a preferences form; locale defaults the format automatically.",
|
|
5999
|
-
"TimeFormatPicker \u2014 picks '12h' | '24h'. Same composition pattern; locale defaults it. Use all four pickers together in a unified settings panel.",
|
|
6000
|
-
"AppProvider \u2014 required peer unless running in fully controlled mode. Provides the locale, setLocale, and i18n context that LocalePicker depends on."
|
|
6001
|
-
],
|
|
6002
|
-
example: `{\`// Uncontrolled \u2014 AppProvider manages state and persists to localStorage
|
|
6003
|
-
import { AppProvider } from "@godxjp/ui/providers";
|
|
6004
|
-
import { LocalePicker } from "@godxjp/ui/navigation";
|
|
6005
|
-
|
|
6006
|
-
export function AppShell() {
|
|
6007
|
-
return (
|
|
6008
|
-
<AppProvider defaultLocale="vi">
|
|
6009
|
-
{/* Anywhere inside the tree */}
|
|
6010
|
-
<LocalePicker />
|
|
6011
|
-
</AppProvider>
|
|
6012
|
-
);
|
|
6013
|
-
}
|
|
6014
|
-
|
|
6015
|
-
// Controlled \u2014 no AppProvider required (e.g. a standalone settings form)
|
|
6016
|
-
import { useState } from "react";
|
|
6017
|
-
import { LocalePicker } from "@godxjp/ui/navigation";
|
|
6018
|
-
import type { AppLocale } from "@godxjp/ui/navigation";
|
|
6019
|
-
|
|
6020
|
-
export function LocaleField() {
|
|
6021
|
-
const [locale, setLocale] = useState<AppLocale>("en");
|
|
6022
|
-
return (
|
|
6023
|
-
<div className="flex flex-col gap-1.5">
|
|
6024
|
-
<label htmlFor="locale-picker">Language</label>
|
|
6025
|
-
<LocalePicker id="locale-picker" value={locale} onValueChange={setLocale} />
|
|
6026
|
-
</div>
|
|
6027
|
-
);
|
|
6028
|
-
}\`}`,
|
|
6029
|
-
storyPath: "navigation/LocalePicker.stories.tsx",
|
|
6030
|
-
rules: [3, 5, 6, 23]
|
|
6031
|
-
},
|
|
6032
|
-
{
|
|
6033
|
-
name: "TimezonePicker",
|
|
6034
|
-
group: "navigation",
|
|
6035
|
-
tagline: "Globe-icon Select for picking an IANA timezone \u2014 throws at runtime if neither AppProvider context nor controlled value+onChange is supplied.",
|
|
6036
|
-
props: [
|
|
6037
|
-
{
|
|
6038
|
-
name: "value",
|
|
6039
|
-
type: "AppTimezone",
|
|
6040
|
-
description: 'Controlled IANA timezone string (e.g. "Asia/Tokyo", "UTC"). Required when used outside AppProvider; reads from AppProvider context when omitted.'
|
|
6041
|
-
},
|
|
6042
|
-
{
|
|
6043
|
-
name: "onChange",
|
|
6044
|
-
type: "(timezone: AppTimezone) => void",
|
|
6045
|
-
description: "Change handler receiving the selected IANA id. Required when used outside AppProvider; falls back to AppProvider setTimezone when omitted."
|
|
6046
|
-
},
|
|
6047
|
-
{
|
|
6048
|
-
name: "options",
|
|
6049
|
-
type: "readonly AppTimezone[]",
|
|
6050
|
-
description: "Restrict the dropdown to this list of IANA ids. Omit to inherit AppProvider timezoneOptions, or fall back to the full runtime IANA list. The current value is always injected at the top if absent from the list."
|
|
6051
|
-
},
|
|
6052
|
-
{
|
|
6053
|
-
name: "id",
|
|
6054
|
-
type: "string",
|
|
6055
|
-
description: "HTML id forwarded to the trigger element, useful for pairing with a <label>."
|
|
6056
|
-
},
|
|
6057
|
-
{ name: "disabled", type: "boolean", description: "Disables the picker trigger." },
|
|
6058
|
-
{
|
|
6059
|
-
name: "className",
|
|
6060
|
-
type: "string",
|
|
6061
|
-
description: "Additional Tailwind classes merged onto the SelectTrigger (default width: w-full sm:w-56)."
|
|
6062
|
-
}
|
|
6063
|
-
],
|
|
6064
|
-
usage: [
|
|
6065
|
-
"DO: Wrap with <AppProvider> and omit value/onChange \u2014 the picker reads and writes context automatically. This is the canonical zero-prop usage: <TimezonePicker />.",
|
|
6066
|
-
"DO: Pass value + onChange for fully controlled standalone usage (e.g. a form field that posts the timezone string): <TimezonePicker value={tz} onValueChange={setTz} />. AppProvider is not required in this mode.",
|
|
6067
|
-
"DON'T: Omit BOTH AppProvider context AND controlled props \u2014 the component throws at runtime: 'TimezonePicker requires <AppProvider> or controlled value + onChange'.",
|
|
6068
|
-
"DO: Pass options={['Asia/Tokyo', 'UTC']} to restrict the list. The current value is automatically prepended if it is missing from the list, so the picker never shows an empty/invalid selection.",
|
|
6069
|
-
"DON'T: Hand-roll a timezone <select> or a custom combobox \u2014 TimezonePicker already handles locale-aware labels (translated city + GMT offset), the full IANA list, and ARIA semantics.",
|
|
6070
|
-
"NOTE: Labels are locale-aware via i18n keys (e.g. 'Japan (Tokyo)' in en, translated equivalents in vi/ja). Labels are derived from AppProvider locale; in controlled mode outside AppProvider the locale defaults to the library fallback. No extra i18n wiring is needed."
|
|
6071
|
-
],
|
|
6072
|
-
useCases: [
|
|
6073
|
-
"User profile / settings page: let the user pick their display timezone; pair with DateFormatPicker and TimeFormatPicker in a settings panel.",
|
|
6074
|
-
"AppProvider bootstrap: pass timezoneOptions={APP_TIMEZONE_PRESET} to AppProvider to restrict the dropdown to a curated Asia-Pacific list, then render <TimezonePicker /> anywhere in the tree.",
|
|
6075
|
-
"Multi-tenant admin: render TimezonePicker in a form that POSTs a per-organization timezone; use controlled mode (value/onChange) and submit the selected IANA string.",
|
|
6076
|
-
"Shell / top-bar: drop <TimezonePicker /> into an AppShell header or Sidebar alongside LocalePicker so users can adjust timezone globally without a full settings page.",
|
|
6077
|
-
"System-timezone default: pass defaultTimezone='system' systemTimezone='Asia/Tokyo' to AppProvider so the picker initializes to the backend timezone; users can still override it."
|
|
6078
|
-
],
|
|
6079
|
-
related: [
|
|
6080
|
-
"LocalePicker \u2014 sibling picker for language/locale; use alongside TimezonePicker in settings panels. Both read/write AppProvider context.",
|
|
6081
|
-
"TimeFormatPicker \u2014 picks 12h/24h clock format; same controlled/context dual-mode API.",
|
|
6082
|
-
"DateFormatPicker \u2014 picks date display format (dmy/mdy/iso); same dual-mode API.",
|
|
6083
|
-
"Select \u2014 the underlying primitive TimezonePicker is built on; use Select directly only when you need a non-timezone dropdown, not for timezone selection."
|
|
6084
|
-
],
|
|
6085
|
-
example: `import { useState } from "react";
|
|
6086
|
-
import type { AppTimezone } from "@godxjp/ui/app";
|
|
6087
|
-
import { TimezonePicker } from "@godxjp/ui/navigation";
|
|
6088
|
-
|
|
6089
|
-
// --- Controlled (no AppProvider required) ---
|
|
6090
|
-
export function TimezoneField() {
|
|
6091
|
-
const [tz, setTz] = useState<AppTimezone>("Asia/Tokyo");
|
|
6092
|
-
return (
|
|
6093
|
-
<TimezonePicker
|
|
6094
|
-
value={tz}
|
|
6095
|
-
onValueChange={setTz}
|
|
6096
|
-
options={["Asia/Tokyo", "Asia/Ho_Chi_Minh", "UTC"]}
|
|
6097
|
-
/>
|
|
6098
|
-
);
|
|
6099
|
-
}
|
|
6100
|
-
|
|
6101
|
-
// --- Context-driven (zero props inside AppProvider) ---
|
|
6102
|
-
import { AppProvider } from "@godxjp/ui/app";
|
|
6103
|
-
import { APP_TIMEZONE_PRESET } from "@godxjp/ui/navigation";
|
|
6104
|
-
|
|
6105
|
-
export function Shell({ children }: { content: React.ReactNode }) {
|
|
6106
|
-
return (
|
|
6107
|
-
<AppProvider
|
|
6108
|
-
defaultLocale="ja"
|
|
6109
|
-
fallbackLocale="en"
|
|
6110
|
-
defaultTimezone="system"
|
|
6111
|
-
systemTimezone="Asia/Tokyo"
|
|
6112
|
-
timezoneOptions={APP_TIMEZONE_PRESET}
|
|
6113
|
-
>
|
|
6114
|
-
{children}
|
|
6115
|
-
</AppProvider>
|
|
6116
|
-
);
|
|
6117
|
-
}
|
|
6118
|
-
|
|
6119
|
-
// Anywhere inside Shell:
|
|
6120
|
-
// <TimezonePicker /> \u2190 reads/writes AppProvider context automatically`,
|
|
6121
|
-
storyPath: "navigation/TimezonePicker.stories.tsx",
|
|
6122
|
-
rules: [3, 5, 23, 31]
|
|
6123
|
-
},
|
|
6124
|
-
{
|
|
6125
|
-
name: "DateFormatPicker",
|
|
6126
|
-
group: "navigation",
|
|
6127
|
-
tagline: "Locale-aware date format selector (ISO / DMY / MDY) \u2014 throws at runtime if neither AppProvider nor controlled value+onChange is provided.",
|
|
6128
|
-
props: [
|
|
6129
|
-
{
|
|
6130
|
-
name: "value",
|
|
6131
|
-
type: "AppDateFormat | undefined",
|
|
6132
|
-
description: 'Controlled date format value. One of `"iso"` (yyyy-MM-dd), `"dmy"` (dd/MM/yyyy), or `"mdy"` (MM/dd/yyyy). When omitted the component reads from AppProvider context.'
|
|
6133
|
-
},
|
|
6134
|
-
{
|
|
6135
|
-
name: "onChange",
|
|
6136
|
-
type: "((dateFormat: AppDateFormat) => void) | undefined",
|
|
6137
|
-
description: "Callback fired when the user picks a new format. When omitted the component writes back to AppProvider context via `ctx.setDateFormat`."
|
|
6138
|
-
},
|
|
6139
|
-
{
|
|
6140
|
-
name: "className",
|
|
6141
|
-
type: "string | undefined",
|
|
6142
|
-
description: "Extra CSS classes merged onto the SelectTrigger. Trigger defaults to `w-full sm:w-44`."
|
|
6143
|
-
},
|
|
6144
|
-
{
|
|
6145
|
-
name: "disabled",
|
|
6146
|
-
type: "boolean | undefined",
|
|
6147
|
-
description: "Disables the select trigger and prevents user interaction."
|
|
6148
|
-
},
|
|
6149
|
-
{
|
|
6150
|
-
name: "id",
|
|
6151
|
-
type: "string | undefined",
|
|
6152
|
-
description: "HTML id forwarded to the SelectTrigger; use with a `<label htmlFor>` for accessible form binding."
|
|
6153
|
-
}
|
|
6154
|
-
],
|
|
6155
|
-
usage: [
|
|
6156
|
-
"DO wrap with `<AppProvider>` (uncontrolled) OR supply both `value` and `onChange` (controlled). Omitting both causes a runtime throw: `DateFormatPicker requires <AppProvider> or controlled value + onChange`.",
|
|
6157
|
-
"DO NOT pass `value` without `onChange` or vice-versa in controlled mode \u2014 the component falls back to context for whichever prop is missing, which produces split ownership bugs.",
|
|
6158
|
-
"DO use `id` + `<label htmlFor={id}>` for accessible form labeling; the trigger already carries an i18n `aria-label` but an explicit label wins for sighted users.",
|
|
6159
|
-
'DO NOT hand-roll a date format `<select>` \u2014 `DateFormatPicker` reads the locale from context and shows human-readable, locale-translated option labels (e.g. `"Ng\xE0y / Th\xE1ng / N\u0103m"` in vi, `"YYYY-MM-DD\uFF08\u5E74-\u6708-\u65E5\uFF09"` in ja). A raw select cannot do this.',
|
|
6160
|
-
"When AppProvider is present and you need to react to changes globally (e.g. re-format all displayed dates), use `AppProvider`'s `onDateFormatChange` callback instead of threading `onChange` through every picker.",
|
|
6161
|
-
'The `AppDateFormat` type is `"iso" | "dmy" | "mdy"`. Import it from `@godxjp/ui/navigation` alongside the component (it is re-exported) \u2014 do not duplicate the string-union inline.'
|
|
6162
|
-
],
|
|
6163
|
-
useCases: [
|
|
6164
|
-
"Settings / Preferences page: let users switch how dates are displayed app-wide (place inside AppProvider, omit value/onChange and it auto-reads/writes context).",
|
|
6165
|
-
"Controlled preview panel: show the effect of a date format choice before saving \u2014 pass `value` + `onChange` to a local state and commit only on Save.",
|
|
6166
|
-
"Admin user-profile form: bind with `id` and a visible `<label>` alongside other pickers (LocalePicker, TimezonePicker, TimeFormatPicker) in a preferences card.",
|
|
6167
|
-
"Multi-entity accounting dashboard where different entities prefer different regional date conventions \u2014 render a per-entity DateFormatPicker in controlled mode, persist choice per entity.",
|
|
6168
|
-
"Onboarding wizard step: collect locale + date format + time format together before creating the user account; all four app pickers compose naturally inside a single AppProvider.",
|
|
6169
|
-
"Export/report dialog: let the user choose the date format for a CSV or PDF export independently of the global app setting \u2014 use controlled mode so the choice is scoped to the dialog."
|
|
6170
|
-
],
|
|
6171
|
-
related: [
|
|
6172
|
-
"LocalePicker \u2014 picks the UI language (AppLocale). Use DateFormatPicker alongside LocalePicker, not instead of it; locale affects translation strings while date format controls the display pattern.",
|
|
6173
|
-
"TimeFormatPicker \u2014 picks 12h vs 24h clock. Sister component; same AppProvider / controlled-mode contract.",
|
|
6174
|
-
"TimezonePicker \u2014 picks the IANA timezone. Same contract; also accepts an `options` prop to restrict the list.",
|
|
6175
|
-
"DatePicker \u2014 a calendar-based date input for picking a specific calendar date. Use DatePicker for data entry; use DateFormatPicker in settings to control how those dates are displayed."
|
|
5012
|
+
"TreeList (@godxjp/ui/data-display) \u2014 use TreeList for hierarchical data that needs recursive nesting with built-in indentation and expand/collapse; use Collapsible for ad-hoc single-level toggle regions."
|
|
6176
5013
|
],
|
|
6177
|
-
example: `{
|
|
6178
|
-
import {
|
|
6179
|
-
import {
|
|
5014
|
+
example: `{\`import { useState } from "react";
|
|
5015
|
+
import { ChevronDown } from "lucide-react";
|
|
5016
|
+
import {
|
|
5017
|
+
Collapsible,
|
|
5018
|
+
CollapsibleTrigger,
|
|
5019
|
+
CollapsibleContent,
|
|
5020
|
+
} from "@godxjp/ui/data-display";
|
|
5021
|
+
import { Button } from "@godxjp/ui";
|
|
6180
5022
|
|
|
6181
|
-
|
|
5023
|
+
// --- Uncontrolled (simplest) ---
|
|
5024
|
+
export function InvoiceLineDetail() {
|
|
6182
5025
|
return (
|
|
6183
|
-
<
|
|
6184
|
-
<
|
|
6185
|
-
<
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
5026
|
+
<Collapsible>
|
|
5027
|
+
<CollapsibleTrigger asChild>
|
|
5028
|
+
<Button variant="ghost" size="sm">
|
|
5029
|
+
<ChevronDown className="mr-1 h-4 w-4" aria-hidden="true" />
|
|
5030
|
+
Show tax breakdown
|
|
5031
|
+
</Button>
|
|
5032
|
+
</CollapsibleTrigger>
|
|
5033
|
+
<CollapsibleContent>
|
|
5034
|
+
<div className="mt-2 rounded-md border p-3 text-sm">
|
|
5035
|
+
<p>Consumption tax (10%): \xA51,234</p>
|
|
5036
|
+
<p>Withholding tax: \xA50</p>
|
|
5037
|
+
</div>
|
|
5038
|
+
</CollapsibleContent>
|
|
5039
|
+
</Collapsible>
|
|
6189
5040
|
);
|
|
6190
5041
|
}
|
|
6191
5042
|
|
|
6192
|
-
// Controlled
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
import type { AppDateFormat } from "@godxjp/ui/navigation";
|
|
6196
|
-
|
|
6197
|
-
export function ExportDialog() {
|
|
6198
|
-
const [fmt, setFmt] = useState<AppDateFormat>("iso");
|
|
6199
|
-
|
|
5043
|
+
// --- Controlled (open driven externally) ---
|
|
5044
|
+
export function FilterSection() {
|
|
5045
|
+
const [open, setOpen] = useState(false);
|
|
6200
5046
|
return (
|
|
6201
|
-
<
|
|
6202
|
-
<
|
|
6203
|
-
|
|
6204
|
-
|
|
5047
|
+
<Collapsible open={open} onOpenChange={setOpen}>
|
|
5048
|
+
<CollapsibleTrigger asChild>
|
|
5049
|
+
<Button variant="outline" size="sm">
|
|
5050
|
+
Advanced filters
|
|
5051
|
+
<ChevronDown className="ml-1 h-4 w-4" aria-hidden="true" />
|
|
5052
|
+
</Button>
|
|
5053
|
+
</CollapsibleTrigger>
|
|
5054
|
+
<CollapsibleContent>
|
|
5055
|
+
{/* place filter controls here */}
|
|
5056
|
+
<p className="mt-2 text-sm text-muted-foreground">Date range, entity, status\u2026</p>
|
|
5057
|
+
</CollapsibleContent>
|
|
5058
|
+
</Collapsible>
|
|
6205
5059
|
);
|
|
6206
5060
|
}\`}`,
|
|
6207
|
-
storyPath: "
|
|
6208
|
-
rules: [3,
|
|
5061
|
+
storyPath: "data-display/Collapsible.stories.tsx",
|
|
5062
|
+
rules: [3, 6, 23]
|
|
6209
5063
|
},
|
|
6210
5064
|
{
|
|
6211
|
-
name: "
|
|
6212
|
-
group: "
|
|
6213
|
-
tagline: "
|
|
5065
|
+
name: "TreeList",
|
|
5066
|
+
group: "data-display",
|
|
5067
|
+
tagline: "Renders a flat array of items as an indented tree-style list with chevron + package icon; depth indentation is data-driven \u2014 never nest DOM manually.",
|
|
6214
5068
|
props: [
|
|
6215
5069
|
{
|
|
6216
|
-
name: "
|
|
6217
|
-
type: "
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
{
|
|
6221
|
-
name: "onChange",
|
|
6222
|
-
type: "(timeFormat: AppTimeFormat) => void | undefined",
|
|
6223
|
-
description: "Controlled change handler. If omitted the picker writes back to AppProvider via setTimeFormat."
|
|
6224
|
-
},
|
|
6225
|
-
{
|
|
6226
|
-
name: "className",
|
|
6227
|
-
type: "string | undefined",
|
|
6228
|
-
description: "Additional CSS classes merged onto the SelectTrigger. Default width is 'w-full sm:w-44'."
|
|
6229
|
-
},
|
|
6230
|
-
{
|
|
6231
|
-
name: "disabled",
|
|
6232
|
-
type: "boolean | undefined",
|
|
6233
|
-
description: "Disables the underlying Select control."
|
|
6234
|
-
},
|
|
6235
|
-
{
|
|
6236
|
-
name: "id",
|
|
6237
|
-
type: "string | undefined",
|
|
6238
|
-
description: "HTML id forwarded to the SelectTrigger, useful for associating a <label>."
|
|
5070
|
+
name: "items",
|
|
5071
|
+
type: "TreeListItem[]",
|
|
5072
|
+
required: true,
|
|
5073
|
+
description: "Ordered flat array of items to render. Each item carries its own depth so the tree structure is expressed in data, not DOM nesting."
|
|
6239
5074
|
}
|
|
6240
5075
|
],
|
|
6241
5076
|
usage: [
|
|
6242
|
-
"DO
|
|
6243
|
-
"DO
|
|
6244
|
-
"
|
|
6245
|
-
"DON'T hand-roll
|
|
6246
|
-
"
|
|
6247
|
-
"
|
|
5077
|
+
"DO pass a flat array ordered top-to-bottom with each item's `depth` set to the correct nesting level (0 = root, 1 = first child, etc.). TreeList does NOT accept nested children \u2014 the tree shape is encoded in data.",
|
|
5078
|
+
"DO set `item.active = true` on the currently selected row; the component applies `data-active` for styling \u2014 never manually add an active class.",
|
|
5079
|
+
"DO use `item.badge` (ReactNode) to surface a secondary label (count, status chip) \u2014 it is rendered as a `Badge variant='secondary'` automatically; do NOT wrap the value in a Badge yourself.",
|
|
5080
|
+
"DON'T hand-roll padding or indentation \u2014 depth-based indentation is applied via `data-depth` CSS; adding manual padding breaks the visual rhythm.",
|
|
5081
|
+
"DON'T use TreeList for interactive selection (click handlers, routing) \u2014 it has no `onItemClick` prop. Wrap items in a navigation list or add a Link inside `item.title` when interactivity is needed.",
|
|
5082
|
+
"DO provide a unique string `item.id` for every item; it is used as the React key and must be stable across renders."
|
|
6248
5083
|
],
|
|
6249
5084
|
useCases: [
|
|
6250
|
-
"
|
|
6251
|
-
"
|
|
6252
|
-
"
|
|
6253
|
-
"
|
|
6254
|
-
"
|
|
6255
|
-
"Multi-entity accounting app (e.g. CoreBooks) where different legal entities may have different locale settings \u2014 wrap each entity's UI subtree in its own AppProvider with the entity's persisted preferences."
|
|
5085
|
+
"Displaying a chart-of-accounts hierarchy (root accounts at depth 0, sub-accounts at depth 1+) in an accounting admin panel.",
|
|
5086
|
+
"Showing a package/module dependency tree where each node has a name, optional description, and an item-count badge.",
|
|
5087
|
+
"Rendering a category tree (e.g., product categories, tax codes) in a read-only reference list alongside a detail panel.",
|
|
5088
|
+
"Listing a filtered/searched subset of a hierarchy \u2014 because the flat-array model lets you pre-filter server-side and still show correct depth context.",
|
|
5089
|
+
"Sidebar or drawer content showing a tree of navigation nodes where the active branch item is highlighted via `active: true`."
|
|
6256
5090
|
],
|
|
6257
5091
|
related: [
|
|
6258
|
-
"
|
|
6259
|
-
"
|
|
6260
|
-
"
|
|
6261
|
-
"
|
|
5092
|
+
"Timeline \u2014 use Timeline for chronological event sequences with timestamps; use TreeList for hierarchical parent-child structures.",
|
|
5093
|
+
"Descriptions \u2014 use Descriptions for label/value pairs; use TreeList when items have a parent-child depth relationship.",
|
|
5094
|
+
"DataTable \u2014 use DataTable for tabular data with columns, sorting, and selection; use TreeList for a single-column hierarchical list without those features.",
|
|
5095
|
+
"EmptyState \u2014 pair with EmptyState when the items array may be empty; TreeList renders nothing (no empty row) when given an empty array."
|
|
6262
5096
|
],
|
|
6263
|
-
example: `
|
|
6264
|
-
{\`// Uncontrolled \u2014 reads/writes AppProvider automatically
|
|
6265
|
-
import { AppProvider } from "@godxjp/ui/app";
|
|
6266
|
-
import { TimeFormatPicker } from "@godxjp/ui/navigation";
|
|
6267
|
-
|
|
6268
|
-
export function PreferencesPanel() {
|
|
6269
|
-
return (
|
|
6270
|
-
<AppProvider defaultLocale="vi" defaultTimeFormat="24h" persist>
|
|
6271
|
-
<TimeFormatPicker />
|
|
6272
|
-
</AppProvider>
|
|
6273
|
-
);
|
|
6274
|
-
}
|
|
5097
|
+
example: `import { TreeList } from "@godxjp/ui/data-display";
|
|
6275
5098
|
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
5099
|
+
const accounts = [
|
|
5100
|
+
{ id: "1000", title: "Assets", depth: 0 },
|
|
5101
|
+
{ id: "1100", title: "Current Assets", depth: 1, active: true },
|
|
5102
|
+
{ id: "1110", title: "Cash & Equivalents", description: "Bank + petty cash", depth: 2, badge: "3 accounts" },
|
|
5103
|
+
{ id: "1120", title: "Accounts Receivable", depth: 2 },
|
|
5104
|
+
{ id: "2000", title: "Liabilities", depth: 0 },
|
|
5105
|
+
];
|
|
6280
5106
|
|
|
6281
|
-
export function
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
<TimeFormatPicker id="time-fmt" value={fmt} onValueChange={setFmt} />
|
|
6287
|
-
</div>
|
|
6288
|
-
);
|
|
6289
|
-
}\`}
|
|
6290
|
-
`,
|
|
6291
|
-
storyPath: "navigation/TimeFormatPicker.stories.tsx",
|
|
6292
|
-
rules: [3, 5, 6, 13]
|
|
5107
|
+
export function ChartOfAccounts() {
|
|
5108
|
+
return <TreeList items={accounts} />;
|
|
5109
|
+
}`,
|
|
5110
|
+
storyPath: "data-display/TreeList.stories.tsx",
|
|
5111
|
+
rules: [3, 6, 23, 31]
|
|
6293
5112
|
},
|
|
6294
5113
|
{
|
|
6295
5114
|
name: "Tooltip",
|
|
@@ -6482,7 +5301,7 @@ export function ControlledExample() {
|
|
|
6482
5301
|
"Link (react-router-dom) \u2014 use bare Link when no prefetching is needed or when the destination has no TanStack Query data (e.g. a static page or a form page that fetches nothing on load).",
|
|
6483
5302
|
"DataState \u2014 the companion lifecycle widget for the destination page; ensures PrefetchLink's prefetch is consumed correctly via useQuery.",
|
|
6484
5303
|
"InfiniteQueryState \u2014 use instead of PrefetchLink when the list itself is infinitely paginated and items are loaded lazily rather than navigated to.",
|
|
6485
|
-
"
|
|
5304
|
+
"ButtonRefetch \u2014 for triggering a manual cache refresh on an already-loaded page, not for navigation prefetching."
|
|
6486
5305
|
],
|
|
6487
5306
|
example: `import { PrefetchLink } from "@godxjp/ui/query";
|
|
6488
5307
|
import { fetchInvoice } from "@/api/invoices";
|
|
@@ -6510,87 +5329,6 @@ import { fetchInvoice } from "@/api/invoices";
|
|
|
6510
5329
|
storyPath: "data-display/PrefetchLink.stories.tsx",
|
|
6511
5330
|
rules: [2, 3, 31]
|
|
6512
5331
|
},
|
|
6513
|
-
{
|
|
6514
|
-
name: "QueryRefetchButton",
|
|
6515
|
-
group: "data-display",
|
|
6516
|
-
importPath: "@godxjp/ui/query",
|
|
6517
|
-
tagline: "Page-header Refresh button wired directly to a TanStack Query result \u2014 auto-disables and spins while fetching; never pass onClick or disabled yourself.",
|
|
6518
|
-
props: [
|
|
6519
|
-
{
|
|
6520
|
-
name: "query",
|
|
6521
|
-
type: "Pick<UseQueryResult<unknown>, 'isFetching' | 'refetch'>",
|
|
6522
|
-
required: true,
|
|
6523
|
-
description: "The TanStack Query result object. The button calls query.refetch() on click and is disabled while query.isFetching is true. Pass the full useQuery result; only isFetching and refetch are consumed."
|
|
6524
|
-
},
|
|
6525
|
-
{
|
|
6526
|
-
name: "label",
|
|
6527
|
-
type: "React.ReactNode",
|
|
6528
|
-
defaultValue: '"Refresh"',
|
|
6529
|
-
description: "Text label rendered inside the button. Ignored when children is provided."
|
|
6530
|
-
},
|
|
6531
|
-
{
|
|
6532
|
-
name: "children",
|
|
6533
|
-
type: "React.ReactNode",
|
|
6534
|
-
description: "If provided, overrides label. Use for custom label content (e.g. translated strings, icons)."
|
|
6535
|
-
},
|
|
6536
|
-
{
|
|
6537
|
-
name: "variant",
|
|
6538
|
-
type: "ButtonProp['variant']",
|
|
6539
|
-
defaultValue: '"outline"',
|
|
6540
|
-
description: "Visual variant forwarded to the underlying Button primitive."
|
|
6541
|
-
},
|
|
6542
|
-
{
|
|
6543
|
-
name: "size",
|
|
6544
|
-
type: "ButtonProp['size']",
|
|
6545
|
-
defaultValue: '"sm"',
|
|
6546
|
-
description: "Size forwarded to the underlying Button primitive."
|
|
6547
|
-
},
|
|
6548
|
-
{
|
|
6549
|
-
name: "className",
|
|
6550
|
-
type: "string",
|
|
6551
|
-
description: "Additional CSS classes applied to the Button root."
|
|
6552
|
-
}
|
|
6553
|
-
],
|
|
6554
|
-
usage: [
|
|
6555
|
-
"DO pass the raw useQuery result directly \u2014 the component only reads isFetching and refetch, so any UseQueryResult shape is safe: `<QueryRefetchButton query={invoicesQuery} />`",
|
|
6556
|
-
"DON'T pass onClick or disabled \u2014 both are owned by QueryRefetchButton and forwarded internally. They are omitted from the prop type (Omit<ButtonProp, 'onClick' | 'disabled'>) so TypeScript will reject them at compile time.",
|
|
6557
|
-
"DON'T use this for mutation triggers \u2014 it is wired to query.refetch(), not a mutation. For mutation actions use a plain Button + useMutation.",
|
|
6558
|
-
"DO place it in a page header or toolbar beside a title \u2014 it renders as size='sm' variant='outline' by default, matching header action patterns.",
|
|
6559
|
-
"The RefreshCw icon is always rendered automatically with a spin animation driven by data-fetching={query.isFetching} \u2014 do NOT add your own icon or loading spinner.",
|
|
6560
|
-
"For i18n, pass a translated string as label or content: `<QueryRefetchButton query={q} label={t('refresh')} />` \u2014 the default label is the English string 'Refresh'."
|
|
6561
|
-
],
|
|
6562
|
-
useCases: [
|
|
6563
|
-
"Toolbar 'Refresh' button on an invoice list page that re-fetches from the server without a full navigation.",
|
|
6564
|
-
"Dashboard header action that re-polls live financial summaries when the user wants fresh data.",
|
|
6565
|
-
"Admin data table header where stale data is a concern and users need manual control over re-fetching.",
|
|
6566
|
-
"Any page using useQuery where you want a consistent, accessible Refresh affordance without wiring up onClick/disabled logic manually.",
|
|
6567
|
-
"Pairing with DataState or a DataTable \u2014 place QueryRefetchButton in the page header while DataState manages the content area lifecycle."
|
|
6568
|
-
],
|
|
6569
|
-
related: [
|
|
6570
|
-
"DataState \u2014 use DataState (not QueryRefetchButton) to handle the full query lifecycle (pending/error/empty/data states) in the content area; QueryRefetchButton is only the header action button.",
|
|
6571
|
-
"InfiniteQueryState \u2014 for infinite-scroll / load-more lists; has its own refetch wiring; QueryRefetchButton is redundant alongside it.",
|
|
6572
|
-
"MutationFeedback \u2014 for displaying mutation errors and a retry action; do not use QueryRefetchButton for mutation retries.",
|
|
6573
|
-
"Button \u2014 the raw primitive; use Button directly when you need custom onClick logic or are not wired to a TanStack Query result."
|
|
6574
|
-
],
|
|
6575
|
-
example: `{\`import { useQuery } from "@tanstack/react-query";
|
|
6576
|
-
import { QueryRefetchButton } from "@godxjp/ui/query";
|
|
6577
|
-
|
|
6578
|
-
export function InvoiceListHeader() {
|
|
6579
|
-
const invoicesQuery = useQuery({
|
|
6580
|
-
queryKey: ["invoices"],
|
|
6581
|
-
queryFn: fetchInvoices,
|
|
6582
|
-
});
|
|
6583
|
-
|
|
6584
|
-
return (
|
|
6585
|
-
<div className="flex items-center justify-between">
|
|
6586
|
-
<h1 className="text-xl font-semibold">Invoices</h1>
|
|
6587
|
-
<QueryRefetchButton query={invoicesQuery} label="Refresh" />
|
|
6588
|
-
</div>
|
|
6589
|
-
);
|
|
6590
|
-
}\`}`,
|
|
6591
|
-
storyPath: "data-display/QueryRefetchButton.stories.tsx",
|
|
6592
|
-
rules: [3, 5, 6, 13]
|
|
6593
|
-
},
|
|
6594
5332
|
{
|
|
6595
5333
|
name: "Avatar",
|
|
6596
5334
|
group: "data-display",
|
|
@@ -6645,7 +5383,7 @@ export function InvoiceListHeader() {
|
|
|
6645
5383
|
"Dividing stacked page sections",
|
|
6646
5384
|
"Vertical split between metadata groups"
|
|
6647
5385
|
],
|
|
6648
|
-
related: ["
|
|
5386
|
+
related: ["Flex direction='col' \u2014 use for vertical spacing without a visible rule."],
|
|
6649
5387
|
example: `import { Separator } from "@godxjp/ui/layout";
|
|
6650
5388
|
|
|
6651
5389
|
<Separator />`,
|
|
@@ -6668,7 +5406,7 @@ export function InvoiceListHeader() {
|
|
|
6668
5406
|
"Custom card media placeholder",
|
|
6669
5407
|
"Inline metadata placeholder"
|
|
6670
5408
|
],
|
|
6671
|
-
related: ["SkeletonRows", "SkeletonTable", "
|
|
5409
|
+
related: ["SkeletonRows", "SkeletonTable", "SkeletonStat"],
|
|
6672
5410
|
example: `import { Skeleton } from "@godxjp/ui/feedback";
|
|
6673
5411
|
|
|
6674
5412
|
<Skeleton className="h-6 w-48" />`,
|
|
@@ -6694,8 +5432,8 @@ export function InvoiceListHeader() {
|
|
|
6694
5432
|
},
|
|
6695
5433
|
{
|
|
6696
5434
|
name: "size",
|
|
6697
|
-
type: '"sm" | "
|
|
6698
|
-
defaultValue: '"
|
|
5435
|
+
type: '"sm" | "md" | "lg"',
|
|
5436
|
+
defaultValue: '"md"',
|
|
6699
5437
|
description: "Control size."
|
|
6700
5438
|
}
|
|
6701
5439
|
],
|
|
@@ -7284,38 +6022,6 @@ export default function PasswordBlock() {
|
|
|
7284
6022
|
<CarouselPrevious />
|
|
7285
6023
|
<CarouselNext />
|
|
7286
6024
|
</Carousel>`
|
|
7287
|
-
},
|
|
7288
|
-
{
|
|
7289
|
-
name: "Combobox",
|
|
7290
|
-
group: "data-entry",
|
|
7291
|
-
tagline: "Single-select searchable combobox composed from Popover + Command + Button.",
|
|
7292
|
-
props: [
|
|
7293
|
-
{
|
|
7294
|
-
name: "options",
|
|
7295
|
-
type: "{ value: string; label: string }[]",
|
|
7296
|
-
required: true,
|
|
7297
|
-
description: "Available selection entries."
|
|
7298
|
-
},
|
|
7299
|
-
{ name: "value", type: "string", description: "Controlled selected value." },
|
|
7300
|
-
{ name: "defaultValue", type: "string", description: "Uncontrolled initial value." },
|
|
7301
|
-
{
|
|
7302
|
-
name: "onValueChange",
|
|
7303
|
-
type: "(value: string) => void",
|
|
7304
|
-
description: "Selection callback."
|
|
7305
|
-
},
|
|
7306
|
-
{ name: "placeholder", type: "string", description: "Trigger placeholder." },
|
|
7307
|
-
{ name: "searchPlaceholder", type: "string", description: "Input placeholder in popover." },
|
|
7308
|
-
{ name: "emptyText", type: "string", description: "Fallback when there are no matches." }
|
|
7309
|
-
],
|
|
7310
|
-
useCases: ["Searchable single-select", "Lookup pickers", "Static option lists"],
|
|
7311
|
-
storyPath: "data-entry/Combobox.stories.tsx",
|
|
7312
|
-
rules: [3, 6],
|
|
7313
|
-
example: `import { Combobox } from "@godxjp/ui/data-entry";
|
|
7314
|
-
|
|
7315
|
-
<Combobox
|
|
7316
|
-
options={[{ value: "a", label: "A" }, { value: "b", label: "B" }]}
|
|
7317
|
-
onValueChange={(value) => console.log(value)}
|
|
7318
|
-
/>`
|
|
7319
6025
|
},
|
|
7320
6026
|
{
|
|
7321
6027
|
name: "TimeInput",
|
|
@@ -7342,6 +6048,131 @@ export default function PasswordBlock() {
|
|
|
7342
6048
|
example: `import { TimeInput } from "@godxjp/ui/data-entry";
|
|
7343
6049
|
|
|
7344
6050
|
<TimeInput value="09:00" step={15} onValueChange={(time) => console.log(time)} />`
|
|
6051
|
+
},
|
|
6052
|
+
{
|
|
6053
|
+
name: "AppSettingPicker",
|
|
6054
|
+
group: "navigation",
|
|
6055
|
+
tagline: "One provider-bound Select for a single AppProvider setting, chosen by `kind` (locale | timezone | dateFormat | timeFormat) \u2014 replaces the former Locale/Timezone/Date-format/Time-format pickers. Throws if used without AppProvider AND without controlled value+onValueChange.",
|
|
6056
|
+
props: [
|
|
6057
|
+
{
|
|
6058
|
+
name: "kind",
|
|
6059
|
+
type: '"locale" | "timezone" | "dateFormat" | "timeFormat"',
|
|
6060
|
+
description: "Which AppProvider setting this picker reads and writes. Determines the option list, icon, trigger width, and the context value/setter used."
|
|
6061
|
+
},
|
|
6062
|
+
{
|
|
6063
|
+
name: "value",
|
|
6064
|
+
type: "string",
|
|
6065
|
+
description: "Controlled value for the chosen kind. When omitted, reads the current value from AppProvider context for that kind."
|
|
6066
|
+
},
|
|
6067
|
+
{
|
|
6068
|
+
name: "onValueChange",
|
|
6069
|
+
type: "(value: string) => void",
|
|
6070
|
+
description: "Controlled change handler. When omitted, calls the matching AppProvider setter (setLocale/setTimezone/setDateFormat/setTimeFormat). Required together with value when no AppProvider is present."
|
|
6071
|
+
},
|
|
6072
|
+
{
|
|
6073
|
+
name: "className",
|
|
6074
|
+
type: "string",
|
|
6075
|
+
description: "Extra CSS classes merged onto the SelectTrigger."
|
|
6076
|
+
},
|
|
6077
|
+
{ name: "disabled", type: "boolean", description: "Disables the Select control." },
|
|
6078
|
+
{
|
|
6079
|
+
name: "id",
|
|
6080
|
+
type: "string",
|
|
6081
|
+
description: "HTML id forwarded to the SelectTrigger for label association."
|
|
6082
|
+
}
|
|
6083
|
+
],
|
|
6084
|
+
usage: [
|
|
6085
|
+
"DO: Mount inside <AppProvider> for zero-config use \u2014 the picker reads and writes the context value named by kind, no value/onValueChange needed.",
|
|
6086
|
+
"DO: Use controlled mode (value + onValueChange) when managing state outside AppProvider, e.g. a standalone settings form or a Storybook story. Both are required together in this mode.",
|
|
6087
|
+
"DO NOT: Render without AppProvider and without both controlled props \u2014 it throws 'AppSettingPicker requires <AppProvider> or controlled value + onValueChange'.",
|
|
6088
|
+
"DO: Render four instances with different kind values to build a full preferences panel; they all share the same AppProvider context and stay in sync.",
|
|
6089
|
+
"DON'T hand-roll a locale/timezone/format Select \u2014 AppSettingPicker already composes Select + the right icon + translated, context-wired options. There is no separate LocalePicker/TimezonePicker/DateFormatPicker/TimeFormatPicker anymore; use kind."
|
|
6090
|
+
],
|
|
6091
|
+
useCases: [
|
|
6092
|
+
'App-shell top-nav language switcher: <AppSettingPicker kind="locale" /> under AppProvider, persisting to localStorage with no extra state.',
|
|
6093
|
+
"User settings page with all four preferences \u2014 render kind=locale, kind=timezone, kind=dateFormat, kind=timeFormat together under one AppProvider.",
|
|
6094
|
+
"Onboarding step that picks language/timezone before the rest of the app is configured \u2014 AppProvider persist={false} + controlled values to keep state local.",
|
|
6095
|
+
'Storybook/test harness without AppProvider \u2014 fully controlled: <AppSettingPicker kind="timeFormat" value="24h" onValueChange={fn} />.'
|
|
6096
|
+
],
|
|
6097
|
+
related: [
|
|
6098
|
+
"AppProvider \u2014 required peer unless fully controlled. Supplies locale/timezone/dateFormat/timeFormat plus their setters and the i18n context.",
|
|
6099
|
+
"Select \u2014 the data-entry primitive AppSettingPicker is built on; reach for Select directly for any non-AppProvider dropdown.",
|
|
6100
|
+
"formatDate \u2014 reads the same AppProvider date/time context that kind='dateFormat'/'timeFormat' write to."
|
|
6101
|
+
],
|
|
6102
|
+
example: `{\`// Uncontrolled \u2014 AppProvider manages and persists every setting
|
|
6103
|
+
import { AppProvider } from "@godxjp/ui/app";
|
|
6104
|
+
import { AppSettingPicker } from "@godxjp/ui/navigation";
|
|
6105
|
+
|
|
6106
|
+
export function SettingsPanel() {
|
|
6107
|
+
return (
|
|
6108
|
+
<AppProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo">
|
|
6109
|
+
<AppSettingPicker kind="locale" />
|
|
6110
|
+
<AppSettingPicker kind="timezone" />
|
|
6111
|
+
<AppSettingPicker kind="dateFormat" />
|
|
6112
|
+
<AppSettingPicker kind="timeFormat" />
|
|
6113
|
+
</AppProvider>
|
|
6114
|
+
);
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
// Controlled \u2014 no AppProvider required
|
|
6118
|
+
import { useState } from "react";
|
|
6119
|
+
import { AppSettingPicker } from "@godxjp/ui/navigation";
|
|
6120
|
+
|
|
6121
|
+
export function LocaleField() {
|
|
6122
|
+
const [locale, setLocale] = useState("en");
|
|
6123
|
+
return <AppSettingPicker kind="locale" value={locale} onValueChange={setLocale} />;
|
|
6124
|
+
}\`}`,
|
|
6125
|
+
storyPath: "navigation/AppSettingPicker.stories.tsx",
|
|
6126
|
+
rules: [3, 5, 6, 23]
|
|
6127
|
+
},
|
|
6128
|
+
{
|
|
6129
|
+
name: "Field",
|
|
6130
|
+
group: "data-entry",
|
|
6131
|
+
tagline: "Label + optional description laid out beside a single checkbox/radio/switch control \u2014 the inline alternative to FormField's full block layout.",
|
|
6132
|
+
props: [
|
|
6133
|
+
{
|
|
6134
|
+
name: "id",
|
|
6135
|
+
type: "string",
|
|
6136
|
+
description: "id wired to the control via htmlFor; pass the same id to the child control."
|
|
6137
|
+
},
|
|
6138
|
+
{ name: "label", type: "ReactNode", description: "The field label, rendered as a <Label>." },
|
|
6139
|
+
{
|
|
6140
|
+
name: "description",
|
|
6141
|
+
type: "ReactNode",
|
|
6142
|
+
description: "Optional helper text rendered under the label."
|
|
6143
|
+
},
|
|
6144
|
+
{
|
|
6145
|
+
name: "children",
|
|
6146
|
+
type: "ReactNode",
|
|
6147
|
+
description: "The control (Checkbox/Radio/Switch) placed beside the label."
|
|
6148
|
+
},
|
|
6149
|
+
{ name: "className", type: "string", description: "Extra CSS classes on the wrapper." }
|
|
6150
|
+
],
|
|
6151
|
+
usage: [
|
|
6152
|
+
"DO: Use Field to label a single boolean/choice control (Switch, Checkbox, Radio) in a compact two-column row \u2014 control beside label + description.",
|
|
6153
|
+
"DO: Match the child control's id to Field's id so the label is correctly associated.",
|
|
6154
|
+
"DON'T: Use Field for text inputs needing helper/error/required slots \u2014 use FormField (block layout) instead. There is no ChoiceField anymore; Field is the canonical name."
|
|
6155
|
+
],
|
|
6156
|
+
useCases: [
|
|
6157
|
+
"A settings list of toggle rows (notifications, auto-save) where each Switch has a label + description.",
|
|
6158
|
+
"A consent checkbox with an explanatory description beside it.",
|
|
6159
|
+
"A radio option row in a preferences form."
|
|
6160
|
+
],
|
|
6161
|
+
related: [
|
|
6162
|
+
"FormField \u2014 block label/helper/error/required layout for text inputs; use it instead when those slots are needed.",
|
|
6163
|
+
"Switch / Checkbox / Radio \u2014 the controls Field typically wraps."
|
|
6164
|
+
],
|
|
6165
|
+
example: `{\`import { Field, Switch } from "@godxjp/ui/data-entry";
|
|
6166
|
+
|
|
6167
|
+
export function NotifyRow() {
|
|
6168
|
+
return (
|
|
6169
|
+
<Field id="notify" label="\u30E1\u30FC\u30EB\u901A\u77E5" description="\u91CD\u8981\u306A\u66F4\u65B0\u3092\u30E1\u30FC\u30EB\u3067\u53D7\u3051\u53D6\u308B">
|
|
6170
|
+
<Switch id="notify" defaultChecked />
|
|
6171
|
+
</Field>
|
|
6172
|
+
);
|
|
6173
|
+
}\`}`,
|
|
6174
|
+
storyPath: "data-entry/Field.stories.tsx",
|
|
6175
|
+
rules: [23]
|
|
7345
6176
|
}
|
|
7346
6177
|
];
|
|
7347
6178
|
function findComponent(name) {
|
|
@@ -7987,7 +6818,7 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
7987
6818
|
</Card>
|
|
7988
6819
|
|
|
7989
6820
|
{filtered.length > PAGE_SIZE && (
|
|
7990
|
-
<Pagination
|
|
6821
|
+
<Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />
|
|
7991
6822
|
)}
|
|
7992
6823
|
</Stack>
|
|
7993
6824
|
</PageContainer>
|
|
@@ -8671,6 +7502,192 @@ inside-cards-inside-cards UI. Keep the hero clean, spacious, readable,
|
|
|
8671
7502
|
visible on a small laptop.`
|
|
8672
7503
|
}
|
|
8673
7504
|
]
|
|
7505
|
+
},
|
|
7506
|
+
// ── component discipline (hard contract) ───────────────────────
|
|
7507
|
+
{
|
|
7508
|
+
id: "component-discipline",
|
|
7509
|
+
name: "Component discipline \u2014 international standards (hard contract)",
|
|
7510
|
+
whenToUse: "MANDATORY before creating or changing ANY @godxjp/ui component, recipe, doc, or example. Enforces real primitives only, no duplication, i18n (Intl/CLDR/ISO/IANA/BCP-47), WAI-ARIA APG + WCAG 2.2 AA, RTL, and the controlled-vocabulary API.",
|
|
7511
|
+
source: "@godxjp/ui .claude/skills/godxjp-ui-component + international-standardization audit",
|
|
7512
|
+
sections: [
|
|
7513
|
+
{
|
|
7514
|
+
id: "real-primitives",
|
|
7515
|
+
title: "Real primitives only \u2014 never invent / fake / raw HTML",
|
|
7516
|
+
tagline: "Compose installable @godxjp/ui only; no hand-rolled wrappers, no raw controls.",
|
|
7517
|
+
body: `NEVER invent/hand-roll a component, fake the design with styled <div>s, or use raw
|
|
7518
|
+
<input>/<select>/<button>/<textarea>/<table>. Use Select, Input, Button, Textarea, DataTable,
|
|
7519
|
+
Checkbox, RadioGroup, Switch. Compose fully: CardContent for padding; a table = Card +
|
|
7520
|
+
CardContent flush + DataTable in a default padded PageContainer (NOT variant="flush").
|
|
7521
|
+
MCP-first: get_component before writing; never guess a prop. No duplication \u2014 Select
|
|
7522
|
+
(showSearch/loadOptions) is the only searchable/async select; there is no Combobox/SearchSelect/
|
|
7523
|
+
CountrySelect/Autocomplete; the 4 i18n pickers are one AppSettingPicker kind=\u2026`
|
|
7524
|
+
},
|
|
7525
|
+
{
|
|
7526
|
+
id: "i18n-intl",
|
|
7527
|
+
title: "i18n via t() + Intl/CLDR",
|
|
7528
|
+
tagline: "Every string + aria-label through t(); format via Intl with the active locale.",
|
|
7529
|
+
body: `Zero hardcoded English/Japanese. Numbers/currency (ISO 4217, minor units from
|
|
7530
|
+
resolvedOptions) + bytes via Intl.NumberFormat; dates via the date subsystem (Intl.DateTimeFormat,
|
|
7531
|
+
IANA tz, ISO-8601); names via Intl.DisplayNames (countries ISO 3166-1 alpha-2, languages BCP-47);
|
|
7532
|
+
plurals via Intl.PluralRules category maps. No emoji flags. No hand-maintained currency/country
|
|
7533
|
+
lists.`
|
|
7534
|
+
},
|
|
7535
|
+
{
|
|
7536
|
+
id: "a11y-apg",
|
|
7537
|
+
title: "WAI-ARIA APG + WCAG 2.2 AA",
|
|
7538
|
+
tagline: "Correct roles/aria/keyboard/focus + a vitest-axe test (0 violations).",
|
|
7539
|
+
body: `Implement the APG pattern: role/landmark, aria-current/expanded/selected/sort/busy +
|
|
7540
|
+
aria-live/activedescendant, aria-errormessage+aria-invalid. Keyboard: roving tabindex, arrows,
|
|
7541
|
+
Home/End, Enter/Space, Esc, visible focus, no positive tabindex. \u226524px targets (2.5.8); never
|
|
7542
|
+
colour-only state (1.4.1 \u2014 add sr-only text); icon-only buttons need a name. Add a *.a11y.test.tsx
|
|
7543
|
+
with expectNoA11yViolations. Prefer Radix/cmdk/vaul for ARIA.`
|
|
7544
|
+
},
|
|
7545
|
+
{
|
|
7546
|
+
id: "rtl-vocab",
|
|
7547
|
+
title: "RTL + controlled-vocabulary API",
|
|
7548
|
+
tagline: "Logical CSS only; value/defaultValue/onValueChange; size md not default.",
|
|
7549
|
+
body: `RTL: logical CSS only (ms/me/ps/pe, start/end, border-s/e, rounded-s/e, text-start/end)
|
|
7550
|
+
\u2014 never physical ml/mr/pl/pr/left/right. API: controlled triad value/defaultValue/onValueChange
|
|
7551
|
+
(open/onOpenChange; checked/onCheckedChange; pressed/onPressedChange); size \u2208 xs|sm|md|lg (never
|
|
7552
|
+
"default"); positive booleans; tone for status; forward ref + ...props + className + id; export
|
|
7553
|
+
XProp + XProp as XProps and register in props/registry. Then: add an MCP catalog entry + a
|
|
7554
|
+
real-screen docs page; verify typecheck/lint/audit/check:*/preview:build/test all green.`
|
|
7555
|
+
}
|
|
7556
|
+
]
|
|
7557
|
+
},
|
|
7558
|
+
// ── design-to-page (consumer: handoff → real page) ─────────────
|
|
7559
|
+
{
|
|
7560
|
+
id: "design-to-page",
|
|
7561
|
+
name: "Design handoff \u2192 real page (consumer build guide)",
|
|
7562
|
+
whenToUse: "You (a consumer agent) received a Claude Design handoff \u2014 a bundle/mock/screenshot/HTML prototype or a written brief \u2014 and must build it as a REAL page with @godxjp/ui. Read this BEFORE writing any JSX. It teaches: read intent, map every block to a real primitive via this MCP, consume existing tokens, apply the dxs-kintai DNA, treat tables as the centerpiece, resolve gaps by extend-or-ask, and verify.",
|
|
7563
|
+
source: "@godxjp/ui .design/research (chats-intent, tables, atomic-components) + dxs-kintai SKILL/colors_and_type.css",
|
|
7564
|
+
sections: [
|
|
7565
|
+
{
|
|
7566
|
+
id: "read-intent",
|
|
7567
|
+
title: "Read the intent \u2014 chats before pixels",
|
|
7568
|
+
tagline: "A handoff is a prototype, not production code. Build the intent, not the markup.",
|
|
7569
|
+
body: `A Claude Design bundle is HTML/CSS/JS to LOOK AT \u2014 never transcribe its DOM.
|
|
7570
|
+
If the bundle has chats/*.md, read them FIRST: they hold what the user actually
|
|
7571
|
+
wanted after iterating, the directions rejected, and the explicit rules. The final
|
|
7572
|
+
HTML is just the last output; the chat is the intent. Then read the README/SKILL +
|
|
7573
|
+
colors_and_type.css for the DNA. Distil each screen to ONE primary question it
|
|
7574
|
+
answers (one-intent-per-screen) before choosing components. Honesty rules that
|
|
7575
|
+
recur in this DNA: render only VALID actions (no disabled-button noise \u2014 a punch
|
|
7576
|
+
card off-state shows Check-In only, never a greyed Check-Out); label = identity
|
|
7577
|
+
(never changes), helper row = state (error/help goes BELOW, never recolours the
|
|
7578
|
+
label); entry-point affordances live in chrome, not floating in content.`
|
|
7579
|
+
},
|
|
7580
|
+
{
|
|
7581
|
+
id: "map-to-primitives",
|
|
7582
|
+
title: "Map every block to a real primitive \u2014 MCP-first, never hand-roll",
|
|
7583
|
+
tagline: "For each visual block ask 'which @godxjp/ui component is this?' \u2014 list_primitives, then get_component.",
|
|
7584
|
+
body: `NEVER hand-roll a styled <div> that looks like a Card, or use raw
|
|
7585
|
+
<input>/<select>/<button>/<table>. Decompose each screen into a shopping list and
|
|
7586
|
+
resolve each item through THIS MCP: list_primitives to discover, get_component to
|
|
7587
|
+
confirm the exact prop/union before you write (never guess a prop). Typical map:
|
|
7588
|
+
page chrome \u2192 AppShell/Sidebar/Topbar/PageContainer; stat row \u2192 ResponsiveGrid +
|
|
7589
|
+
StatCard; data grid \u2192 DataTable; status pill \u2192 Badge tone=\u2026; filter row \u2192 Form
|
|
7590
|
+
inline + Select/Input; org\u2192branch \u2192 Cascader/TreeSelect; date/time \u2192 DatePicker/
|
|
7591
|
+
TimePicker; \u2318K \u2192 Command; bulk drawer/detail \u2192 Drawer/Sheet/Dialog; split list+
|
|
7592
|
+
detail \u2192 SplitPane/Resizable; empty \u2192 EmptyState; confirm \u2192 AlertDialog; toast \u2192
|
|
7593
|
+
Sonner. No duplication: Select (showSearch/loadOptions) is the ONLY searchable/
|
|
7594
|
+
async select (no Combobox/Autocomplete); the 4 i18n pickers are one AppSettingPicker
|
|
7595
|
+
kind=\u2026. A table = Card + CardContent-flush + DataTable (not PageContainer flush).`
|
|
7596
|
+
},
|
|
7597
|
+
{
|
|
7598
|
+
id: "tokens-exist",
|
|
7599
|
+
title: "Tokens already exist \u2014 consume var(--\u2026), never redeclare",
|
|
7600
|
+
tagline: "The design's colors_and_type.css is already implemented as foundation.css.",
|
|
7601
|
+
body: `The handoff's colors_and_type.css (SmartHR blue, wa-iro, M PLUS 2, the
|
|
7602
|
+
density scale) is ALREADY shipped as @godxjp/ui's foundation tokens. Never paste a
|
|
7603
|
+
hex, never redeclare a token, never invent a neutral. Consume var(--\u2026) and the
|
|
7604
|
+
semantic utilities. Use get_tokens (MCP) to find the right name \u2014 if a token seems
|
|
7605
|
+
missing it almost certainly exists under a different name. Soft tints come from
|
|
7606
|
+
color-mix(in oklch, var(--primary) 15%, transparent), NOT a new pale hex. Control
|
|
7607
|
+
heights come from the density scale (xs 24 / sm 28 / default 32 / lg 36 / xl 44),
|
|
7608
|
+
never a literal px. Radii: card 6px, control 4px, inner pill 2px.`
|
|
7609
|
+
},
|
|
7610
|
+
{
|
|
7611
|
+
id: "dna",
|
|
7612
|
+
title: "Apply the dxs-kintai DNA",
|
|
7613
|
+
tagline: "\u6E0B\u307F / \u9593 / \u7C21\u7D20 \u2014 fixed color signaling, dense, small headings, 14/1.7, no emoji.",
|
|
7614
|
+
body: `These rules survive when you drop the prototype's divs:
|
|
7615
|
+
\u2022 \u6E0B\u307F (restraint): primary chroma \u2264 0.18 \u2014 --primary is the single most-important
|
|
7616
|
+
action + brand surfaces ONLY, never status. No gradients, no pill cards, no
|
|
7617
|
+
saturated brand.
|
|
7618
|
+
\u2022 \u9593 (breathing): body 14px / 1.7 (NEVER 16/1.5); tabular-nums on every numeric
|
|
7619
|
+
column/stat so digits align under 1.7 leading.
|
|
7620
|
+
\u2022 \u7C21\u7D20 (simplicity): three weights only \u2014 400/500/700 (no 300, no 600). Headings
|
|
7621
|
+
stay SMALL: h1 = 20px, h2 = 18 (not 32) \u2014 JP enterprise is dense, big headings
|
|
7622
|
+
waste \u9593.
|
|
7623
|
+
\u2022 Color signaling is FIXED-mapping: success \u82E5\u7AF9 \xB7 warning \u5C71\u5439(yellow) \xB7 info \u7FA4\u9752
|
|
7624
|
+
\xB7 attention \u6731(orange \u2014 PREFER over red for non-destructive: \u9045\u523B/lateness) \xB7
|
|
7625
|
+
danger \u831C(destructive only). Wa-iro is decorative (charts/tags/tenant) \u2014 NEVER
|
|
7626
|
+
remap a wa-iro hue to a semantic role.
|
|
7627
|
+
\u2022 Density up front: compact 28 (heavy tables) \xB7 default 32 \xB7 comfortable 44 (login/
|
|
7628
|
+
mobile, 44px touch floor). Set on the container; don't mix mid-page.
|
|
7629
|
+
\u2022 Cards: 1px border, NO shadow at rest (shadows only on popover md / dialog xl).
|
|
7630
|
+
\u2022 Copy quiet & factual \u2014 \u300C\u627F\u8A8D\u3057\u307E\u3057\u305F\u300D not \u300C\u627F\u8A8D\u306B\u6210\u529F\u3057\u307E\u3057\u305F\u{1F389}\u300D. Empty state =
|
|
7631
|
+
one calm sentence, no illustration. NO emoji in product UI; Lucide 1.5px icons,
|
|
7632
|
+
currentColor, sized by context (14 table / 16 nav / 18 button / 20 header).
|
|
7633
|
+
\u2022 Multi-tenant: tenants override only --primary/--ring/--foreground; semantic
|
|
7634
|
+
colors stay shared (a "rejected" badge means the same everywhere).`
|
|
7635
|
+
},
|
|
7636
|
+
{
|
|
7637
|
+
id: "tables-central",
|
|
7638
|
+
title: "Tables are the centerpiece \u2014 DataTable + the variant catalogue",
|
|
7639
|
+
tagline: "Enterprise \u52E4\u6020/admin lives in tables; showcase the family broadly.",
|
|
7640
|
+
body: `Most of this DNA's real value is in tables \u2014 make DataTable the
|
|
7641
|
+
centerpiece. One shell: Card + CardContent-flush wrapping DataTable (1px border,
|
|
7642
|
+
6px radius, no shadow). Region order: view tabs \xB7 toolbar (search + \u2318K + density +
|
|
7643
|
+
columns + import/export + primary CTA) \xB7 active-filter chip bar \xB7 table \xB7 footer
|
|
7644
|
+
totals \xB7 pagination \u2014 every region optional. Build each pattern as its own block:
|
|
7645
|
+
assembled CRUD list \xB7 bulk-action toolbar (selection REPLACES toolbar; cross-page
|
|
7646
|
+
"select all 1,284"; destructive isolated last) \xB7 column manager \xB7 advanced filter
|
|
7647
|
+
AND/OR \xB7 sort/resize \xB7 expandable detail row \xB7 inline editable row (row-level
|
|
7648
|
+
commit, dirty dot, "\u672A\u4FDD\u5B58" footer) \xB7 grouped rows w/ subtotals \xB7 tree rows \xB7 sticky
|
|
7649
|
+
columns + horizontal scroll \xB7 pagination \xD73 (numbered/load-more/cursor) \xB7 import/
|
|
7650
|
+
export stepper \xB7 empty/loading(Skeleton)/error/no-perm states \xB7 footer totals \xB7
|
|
7651
|
+
compact kintone grid \xB7 conditional row/cell formatting. Cells: status \u2192 Badge tone;
|
|
7652
|
+
identity \u2192 Avatar + two-line; numerics right-aligned tabular-nums with \u2014 for null;
|
|
7653
|
+
IDs mono. Confirmed (\u78BA\u5B9A\u6E08) rows are frozen \u2014 no edit, no destructive bulk. Row
|
|
7654
|
+
states change only background via color-mix, never height/padding. get_component
|
|
7655
|
+
DataTable + get_vocab ColumnDef/TableDensity/SortState before you build.`
|
|
7656
|
+
},
|
|
7657
|
+
{
|
|
7658
|
+
id: "gaps-extend-or-ask",
|
|
7659
|
+
title: "Gaps \u2192 extend or ask \u2014 never invent",
|
|
7660
|
+
tagline: "A block no primitive expresses is a decision, not a hand-roll.",
|
|
7661
|
+
body: `When a block has no clean primitive/prop/variant, do NOT bake a bespoke
|
|
7662
|
+
one-off into the page. First try to EXTEND: can an existing component take one more
|
|
7663
|
+
tone/size/variant/slot, or be composed from existing primitives (e.g. a punch-card
|
|
7664
|
+
FSM, a mobile selection-bar, an i18n locale-field are compositions over Button/Card/
|
|
7665
|
+
Badge/Tabs/Input, not new primitives)? If it's a genuine gap or you're unsure, STOP
|
|
7666
|
+
and ASK the user (or surface it as an ADR/decision) \u2014 name the gap, propose
|
|
7667
|
+
"new variant on <X> vs. app-level composition vs. new component", and converge
|
|
7668
|
+
before building. Known gaps to expect in this DNA (ask rather than invent): three-
|
|
7669
|
+
level table density (current is binary), multi-sort priority badges, column resize/
|
|
7670
|
+
manager, numbered/load-more pagination, expandable/editable/grouped/tree/sticky-col
|
|
7671
|
+
table modes, filter chip bar + AND/OR panel, saved-view tabs, week-timeline/staff\xD7
|
|
7672
|
+
time calendar, multilingual-field, no-code builders. Never silently fill a gap.`
|
|
7673
|
+
},
|
|
7674
|
+
{
|
|
7675
|
+
id: "verify",
|
|
7676
|
+
title: "Verify \u2014 states complete, a11y, build green",
|
|
7677
|
+
tagline: "Every state shown, WCAG 2.2 AA, typecheck/lint/audit clean, eyeballed at 3 widths.",
|
|
7678
|
+
body: `Before calling it done: every prop \xD7 union value \xD7 state is exercised
|
|
7679
|
+
(default/hover/focus/active/disabled/loading/empty/error) \u2014 Skeleton for INIT fetch,
|
|
7680
|
+
spinner/loading for active save, EmptyState for no-data, inline error near the field
|
|
7681
|
+
(not a disappearing toast). A11y: correct roles/landmarks, keyboard (arrows/Home/End/
|
|
7682
|
+
Enter/Esc, visible focus, no positive tabindex), \u226524px targets, never colour-only
|
|
7683
|
+
state (add sr-only text), icon-only buttons have a name; aim for 0 vitest-axe
|
|
7684
|
+
violations. i18n: zero hardcoded strings \u2014 every label + aria-label through t(),
|
|
7685
|
+
format numbers/dates via Intl. Then run the build: pnpm typecheck + pnpm lint +
|
|
7686
|
+
pnpm audit must be green, console clean, and eyeball the page at 390 / 768 / 1280
|
|
7687
|
+
(atoms never wrap, containers wrap with row-gap, tabs horizontal-scroll, grids
|
|
7688
|
+
minmax(0,1fr), heights never break).`
|
|
7689
|
+
}
|
|
7690
|
+
]
|
|
8674
7691
|
}
|
|
8675
7692
|
];
|
|
8676
7693
|
function findSkill(id) {
|
|
@@ -8769,6 +7786,13 @@ function routeTask(task) {
|
|
|
8769
7786
|
"workflow",
|
|
8770
7787
|
"Generate design image first \u2192 analyze \u2192 implement."
|
|
8771
7788
|
);
|
|
7789
|
+
route(
|
|
7790
|
+
["handoff", "design bundle", "claude design", "prototype", "build the page", "implement the design", "build this screen", "mockup"],
|
|
7791
|
+
"design-to-page",
|
|
7792
|
+
"map-to-primitives",
|
|
7793
|
+
"Map every block to a real @godxjp/ui primitive (MCP-first), consume existing tokens, apply the dxs-kintai DNA, tables central, gaps \u2192 extend-or-ask, verify.",
|
|
7794
|
+
["design-to-page/read-intent", "design-to-page/dna", "design-to-page/tables-central"]
|
|
7795
|
+
);
|
|
8772
7796
|
if (matches.length === 0) {
|
|
8773
7797
|
return [
|
|
8774
7798
|
{
|
|
@@ -10272,12 +9296,71 @@ ${c.example}
|
|
|
10272
9296
|
return out;
|
|
10273
9297
|
}
|
|
10274
9298
|
|
|
9299
|
+
// package.json
|
|
9300
|
+
var package_default = {
|
|
9301
|
+
name: "@godxjp/ui-mcp",
|
|
9302
|
+
version: "0.17.1",
|
|
9303
|
+
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, 34 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).",
|
|
9304
|
+
type: "module",
|
|
9305
|
+
main: "./dist/index.js",
|
|
9306
|
+
module: "./dist/index.js",
|
|
9307
|
+
types: "./dist/index.d.ts",
|
|
9308
|
+
bin: {
|
|
9309
|
+
"godx-ui-mcp": "./dist/index.js"
|
|
9310
|
+
},
|
|
9311
|
+
files: [
|
|
9312
|
+
"dist",
|
|
9313
|
+
"README.md"
|
|
9314
|
+
],
|
|
9315
|
+
publishConfig: {
|
|
9316
|
+
registry: "https://registry.npmjs.org/",
|
|
9317
|
+
access: "public"
|
|
9318
|
+
},
|
|
9319
|
+
repository: {
|
|
9320
|
+
type: "git",
|
|
9321
|
+
url: "git+https://github.com/godx-jp/godxjp-ui.git",
|
|
9322
|
+
directory: "mcp"
|
|
9323
|
+
},
|
|
9324
|
+
homepage: "https://github.com/godx-jp/godxjp-ui/tree/main/mcp#readme",
|
|
9325
|
+
license: "Apache-2.0",
|
|
9326
|
+
scripts: {
|
|
9327
|
+
build: "tsup",
|
|
9328
|
+
dev: "tsup --watch",
|
|
9329
|
+
start: "node dist/index.js",
|
|
9330
|
+
inspect: "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
9331
|
+
"type-check": "tsc --noEmit",
|
|
9332
|
+
test: "vitest run",
|
|
9333
|
+
prepublishOnly: "npm run build"
|
|
9334
|
+
},
|
|
9335
|
+
dependencies: {
|
|
9336
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
9337
|
+
zod: "^4.4.3"
|
|
9338
|
+
},
|
|
9339
|
+
devDependencies: {
|
|
9340
|
+
"@types/node": "^22.10.0",
|
|
9341
|
+
tsup: "^8.5.1",
|
|
9342
|
+
typescript: "^6.0.3",
|
|
9343
|
+
vitest: "^4.1.6"
|
|
9344
|
+
},
|
|
9345
|
+
keywords: [
|
|
9346
|
+
"mcp",
|
|
9347
|
+
"model-context-protocol",
|
|
9348
|
+
"godxjp",
|
|
9349
|
+
"ui",
|
|
9350
|
+
"design-system",
|
|
9351
|
+
"react",
|
|
9352
|
+
"claude",
|
|
9353
|
+
"cursor"
|
|
9354
|
+
]
|
|
9355
|
+
};
|
|
9356
|
+
|
|
10275
9357
|
// src/index.ts
|
|
10276
9358
|
async function main() {
|
|
10277
9359
|
const server = new Server(
|
|
10278
9360
|
{
|
|
10279
9361
|
name: "godx-ui-mcp",
|
|
10280
|
-
version
|
|
9362
|
+
// Track the package version — never hardcode (see server.test.ts guard).
|
|
9363
|
+
version: package_default.version
|
|
10281
9364
|
},
|
|
10282
9365
|
{
|
|
10283
9366
|
capabilities: {
|