@godxjp/ui-mcp 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +536 -1098
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ var COMPONENTS = [
|
|
|
71
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'` + `<PageInset>` for the FilterBar above the table.",
|
|
72
72
|
"A detail / edit form page where the footer holds Save and Cancel buttons \u2014 use `footer={<Inline><Button>\u4FDD\u5B58</Button><Button variant='outline'>\u30AD\u30E3\u30F3\u30BB\u30EB</Button></Inline>}` 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={<Stack gap='lg'>\u2026</Stack>}` to vertically stack multiple Card/
|
|
74
|
+
"A dashboard page with KPI cards and chart sections \u2014 use `variant='default'` with `children={<Stack gap='lg'>\u2026</Stack>}` 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
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, FilterBar, and controls in a single prop."
|
|
77
77
|
],
|
|
@@ -123,15 +123,15 @@ export default function OrdersPage() {
|
|
|
123
123
|
],
|
|
124
124
|
useCases: [
|
|
125
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 `
|
|
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
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 `
|
|
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
129
|
'Modal/Sheet interiors: `<Stack gap="md">` inside `<Dialog>` or `<Sheet>` content area to separate sections (description, form, preview) with consistent token spacing.',
|
|
130
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
131
|
],
|
|
132
132
|
related: [
|
|
133
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.,
|
|
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
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
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
137
|
],
|
|
@@ -170,7 +170,7 @@ export default function OrdersPage() {
|
|
|
170
170
|
useCases: [
|
|
171
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
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 `
|
|
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
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
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
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).'
|
|
@@ -191,6 +191,75 @@ import { Button } from "@godxjp/ui/general";
|
|
|
191
191
|
storyPath: "layout/Inline.stories.tsx",
|
|
192
192
|
rules: [2]
|
|
193
193
|
},
|
|
194
|
+
{
|
|
195
|
+
name: "Flex",
|
|
196
|
+
group: "layout",
|
|
197
|
+
tagline: "Token-spaced flex primitive with explicit direction, alignment, justification, and wrapping controls.",
|
|
198
|
+
props: [
|
|
199
|
+
{
|
|
200
|
+
name: "direction",
|
|
201
|
+
type: '"row" | "col"',
|
|
202
|
+
defaultValue: '"col"',
|
|
203
|
+
description: "Main axis direction. Use row for horizontal runs, col for vertical stacks."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "gap",
|
|
207
|
+
type: '"xs" | "sm" | "md" | "lg" | "xl"',
|
|
208
|
+
defaultValue: '"md"',
|
|
209
|
+
description: "Token gap between children, shared with Stack and Inline."
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "align",
|
|
213
|
+
type: '"start" | "center" | "end" | "stretch" | "baseline"',
|
|
214
|
+
description: "Cross-axis alignment, emitted as a data attribute for the layout CSS."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "justify",
|
|
218
|
+
type: '"start" | "center" | "end" | "between" | "around" | "evenly"',
|
|
219
|
+
description: "Main-axis distribution, emitted as a data attribute for the layout CSS."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "wrap",
|
|
223
|
+
type: "boolean",
|
|
224
|
+
defaultValue: "false",
|
|
225
|
+
description: "Allows children to wrap onto additional flex lines."
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
usage: [
|
|
229
|
+
'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 Stack and Inline, so spacing remains tied to the design system.",
|
|
231
|
+
'DO use `direction="row"` with `wrap` for responsive control rows, chip clusters, and action groups that need more control than Inline exposes.',
|
|
232
|
+
'DO use `direction="col"` for vertical groupings that need explicit `align` or `justify` behavior. If all you need is a vertical gap, Stack is the thinner alias.',
|
|
233
|
+
"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
|
+
"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
|
+
],
|
|
236
|
+
useCases: [
|
|
237
|
+
"Toolbar internals where controls should sit in a row, wrap on narrow widths, and stay vertically centered.",
|
|
238
|
+
"Card headers that need title content on the left and actions on the right via `justify='between'` without hand-rolling flex utility classes.",
|
|
239
|
+
"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 Stack exposes.",
|
|
241
|
+
"Badge, chip, or tag clusters where wrapping is required but the caller also needs a larger token gap than Inline's default.",
|
|
242
|
+
"Low-level layout composition inside custom components where Stack and Inline are too opinionated but raw flex classes would duplicate the primitive."
|
|
243
|
+
],
|
|
244
|
+
related: [
|
|
245
|
+
"Stack \u2014 thin `direction='col'` alias of Flex. Use Stack for ordinary vertical block spacing; use Flex when you need align, justify, or wrap control.",
|
|
246
|
+
"Inline \u2014 thin `direction='row'` alias of Flex with wrapping and centered alignment. Use Inline for simple horizontal groups; use Flex for explicit axis/distribution control.",
|
|
247
|
+
"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
|
+
"PageContainer \u2014 page scaffold and padding context. Flex is an inner layout primitive used inside page sections, cards, dialogs, and toolbars."
|
|
249
|
+
],
|
|
250
|
+
example: `import { Flex } from "@godxjp/ui/layout";
|
|
251
|
+
import { Button } from "@godxjp/ui/general";
|
|
252
|
+
|
|
253
|
+
<Flex direction="row" gap="sm" align="center" justify="between" wrap>
|
|
254
|
+
<SearchSummary />
|
|
255
|
+
<Flex direction="row" gap="xs" align="center" wrap>
|
|
256
|
+
<Button variant="outline">\u30EA\u30BB\u30C3\u30C8</Button>
|
|
257
|
+
<Button>\u9069\u7528</Button>
|
|
258
|
+
</Flex>
|
|
259
|
+
</Flex>`,
|
|
260
|
+
storyPath: "layout/Flex.stories.tsx",
|
|
261
|
+
rules: [2, 40]
|
|
262
|
+
},
|
|
194
263
|
{
|
|
195
264
|
name: "ResponsiveGrid",
|
|
196
265
|
group: "layout",
|
|
@@ -206,39 +275,39 @@ import { Button } from "@godxjp/ui/general";
|
|
|
206
275
|
name: "children",
|
|
207
276
|
type: "ReactNode",
|
|
208
277
|
required: true,
|
|
209
|
-
description: "Grid items \u2014 typically Card or
|
|
278
|
+
description: "Grid items \u2014 typically Card or StatCard."
|
|
210
279
|
}
|
|
211
280
|
],
|
|
212
281
|
usage: [
|
|
213
|
-
"DO place
|
|
282
|
+
"DO place StatCard tiles directly as immediate children \u2014 StatCard IS already a bordered card; never wrap it in an extra <Card><CardContent>. The canonical pattern is <ResponsiveGrid columns={4}><StatCard .../><StatCard .../></ResponsiveGrid>.",
|
|
214
283
|
"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.",
|
|
215
284
|
"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).",
|
|
216
285
|
"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).",
|
|
217
|
-
"DO render SkeletonCard children in place of
|
|
286
|
+
"DO render SkeletonCard 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.",
|
|
218
287
|
"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."
|
|
219
288
|
],
|
|
220
289
|
useCases: [
|
|
221
|
-
"Dashboard KPI row: rendering 3\u20134
|
|
222
|
-
"Summary header above a list page: a 2-column grid of two
|
|
223
|
-
"Accounting period overview: 4
|
|
224
|
-
"Loading state for a KPI row: identical <ResponsiveGrid columns={4}> wrapping four <SkeletonCard /> placeholders rendered while async data is in flight, swapped for real
|
|
225
|
-
"Settings or profile summary cards: 2- or 3-column grid of Card+CardContent blocks (not
|
|
290
|
+
"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 FilterBar and DataTable.",
|
|
292
|
+
"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 <SkeletonCard /> placeholders rendered while async data is in flight, swapped for real StatCard tiles once resolved.",
|
|
294
|
+
"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.",
|
|
226
295
|
"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."
|
|
227
296
|
],
|
|
228
297
|
related: [
|
|
229
298
|
"Stack \u2014 use Stack (vertical) or Inline (horizontal) for sequential blocks of mixed-width content (forms, description lists, button rows). Use ResponsiveGrid only when you want equal-width, auto-reflowing tile columns.",
|
|
230
299
|
"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.",
|
|
231
|
-
"
|
|
232
|
-
"SkeletonCard \u2014 the loading-state sibling of
|
|
300
|
+
"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
|
+
"SkeletonCard \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."
|
|
233
302
|
],
|
|
234
303
|
example: `import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
235
|
-
import {
|
|
304
|
+
import { StatCard } from "@godxjp/ui/data-display";
|
|
236
305
|
|
|
237
306
|
<ResponsiveGrid columns={4}>
|
|
238
|
-
<
|
|
239
|
-
<
|
|
240
|
-
<
|
|
241
|
-
<
|
|
307
|
+
<StatCard label="\u7DCF\u4F1A\u54E1\u6570" value="12,400" />
|
|
308
|
+
<StatCard label="\u516C\u958B\u4E2D\u30AF\u30FC\u30DD\u30F3" value="8" />
|
|
309
|
+
<StatCard label="\u6708\u9593\u5229\u7528\u6570" value="3,210" />
|
|
310
|
+
<StatCard label="\u5272\u5F15\u7DCF\u984D" value="\xA5480,000" />
|
|
242
311
|
</ResponsiveGrid>`,
|
|
243
312
|
storyPath: "layout/ResponsiveGrid.stories.tsx",
|
|
244
313
|
rules: [24, 40]
|
|
@@ -297,7 +366,7 @@ import { CardStat } from "@godxjp/ui/data-display";
|
|
|
297
366
|
"DO use the auto-built topbar rail (logo / topbarLeft / topbarRight) for simple shells. Pass a fully configured <Topbar> to the `topbar` prop only when you need live handlers (entity switcher via productMenu, search, notifications, user avatar) \u2014 when `topbar` is provided, logo/topbarLeft/topbarRight are ignored entirely.",
|
|
298
367
|
"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.",
|
|
299
368
|
"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>.",
|
|
300
|
-
"DO NOT nest a second AppShell or
|
|
369
|
+
"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.",
|
|
301
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 <PageInset> inside a flush PageContainer) inside children to get standard page padding."
|
|
302
371
|
],
|
|
303
372
|
useCases: [
|
|
@@ -309,7 +378,7 @@ import { CardStat } from "@godxjp/ui/data-display";
|
|
|
309
378
|
"Breadcrumb-aware shell: pass a <Breadcrumb items={\u2026}> node to AppShell's `breadcrumb` prop so the breadcrumb strip appears above all page content without each page having to render it separately."
|
|
310
379
|
],
|
|
311
380
|
related: [
|
|
312
|
-
"
|
|
381
|
+
"AppShell \u2014 opinionated wrapper that composes AppShell + a frozen default Topbar in three props (menu, children, breadcrumb). Use AppShell for quick scaffolding when the default GodX product chip and no-op search/notification handlers are acceptable; switch to AppShell directly the moment you need a custom entity switcher, real onSearchOpen, user slot, or any topbar configuration.",
|
|
313
382
|
"Sidebar \u2014 the canonical node to pass as AppShell's `sidebar` prop; owns activeId, collapsible submenu groups, collapsed icon-only mode, and section labels. Never hand-roll a nav list inside the sidebar slot.",
|
|
314
383
|
"Topbar \u2014 the structured topbar component to pass to AppShell's `topbar` prop when you need live product/project chip switchers, search, notifications, sidebar toggle, user avatar, or rightSlot extras. When `topbar` is provided, AppShell's logo/topbarLeft/topbarRight props are ignored.",
|
|
315
384
|
"PageContainer \u2014 the mandatory direct child inside AppShell's `children` for every page; provides title, subtitle, extra actions, breadcrumb, footer, variant (flush/narrow/ghost), and density. Never render raw content directly as AppShell's child without a PageContainer wrapper."
|
|
@@ -330,7 +399,7 @@ const sidebar = (
|
|
|
330
399
|
/>
|
|
331
400
|
);
|
|
332
401
|
|
|
333
|
-
export function CrmLayout({ children }: {
|
|
402
|
+
export function CrmLayout({ children }: { content: React.ReactNode }) {
|
|
334
403
|
return <AppShell sidebar={sidebar}>{children}</AppShell>;
|
|
335
404
|
}`,
|
|
336
405
|
storyPath: "layout/AppShell.stories.tsx",
|
|
@@ -387,7 +456,7 @@ export function CrmLayout({ children }: { children: React.ReactNode }) {
|
|
|
387
456
|
],
|
|
388
457
|
usage: [
|
|
389
458
|
"DO: Define all nav items as a SidebarSectionProp[] data structure and pass it to sections \u2014 never hand-roll nav buttons alongside or instead of the Sidebar.",
|
|
390
|
-
"DO: Add
|
|
459
|
+
"DO: Add content: SidebarItemProp[] to any SidebarItemProp to create a collapsible submenu group. The parent item's icon is required even for groups. The group auto-opens and highlights when activeId matches any descendant.",
|
|
391
460
|
"DO: Mirror the collapsed boolean between AppShell's sidebarCollapsed prop and Sidebar's collapsed prop \u2014 they must stay in sync so the shell layout grid adjusts correctly.",
|
|
392
461
|
"DO: Use the footer prop for user info or status \u2014 it is pinned below the scroll area and does not scroll away.",
|
|
393
462
|
"DON'T: Manage collapse state inside the Sidebar \u2014 it is stateless. Hoist the boolean to your shell/page state and pass it down via both AppShell.sidebarCollapsed and Sidebar.collapsed.",
|
|
@@ -421,7 +490,7 @@ const sections: SidebarSection[] = [
|
|
|
421
490
|
id: "ledger",
|
|
422
491
|
label: "Ledger",
|
|
423
492
|
icon: BookOpen,
|
|
424
|
-
|
|
493
|
+
content: [
|
|
425
494
|
{ id: "journal", label: "Journal", icon: FileText },
|
|
426
495
|
{ id: "chart-of-accounts", label: "Chart of Accounts", icon: CreditCard },
|
|
427
496
|
],
|
|
@@ -480,7 +549,7 @@ export default function Shell() {
|
|
|
480
549
|
{
|
|
481
550
|
name: "Topbar",
|
|
482
551
|
group: "layout",
|
|
483
|
-
tagline: "App-shell top bar with product/project chip switchers, search, notifications, and sidebar toggle \u2014 pass DropdownMenuContent to productMenu/projectMenu to turn any chip into a real dropdown switcher; the project chip is hidden entirely when neither project nor
|
|
552
|
+
tagline: "App-shell top bar with product/project chip switchers, search, notifications, and sidebar toggle \u2014 pass DropdownMenuContent to productMenu/projectMenu to turn any chip into a real dropdown switcher; the project chip is hidden entirely when neither project nor projectSidebar is set.",
|
|
484
553
|
props: [
|
|
485
554
|
{
|
|
486
555
|
name: "product",
|
|
@@ -587,7 +656,7 @@ export default function Shell() {
|
|
|
587
656
|
"AppShell \u2014 the parent shell component that places Topbar inside its `app-topbar` header region via the `topbar` prop. Always use Topbar inside AppShell, not standalone.",
|
|
588
657
|
"Sidebar \u2014 the companion left-rail nav; pair with Topbar's `collapsed`/`onToggleCollapsed` to keep sidebar and topbar toggle state in sync.",
|
|
589
658
|
"DropdownMenu / DropdownMenuContent \u2014 pass a `DropdownMenuContent` as `productMenu` or `projectMenu` to turn a chip into an inline switcher. Topbar handles the `DropdownMenuTrigger` wrapping internally; only the Content node is needed.",
|
|
590
|
-
"
|
|
659
|
+
"AppShell \u2014 a higher-level opinionated shell that already composes AppShell + Topbar with hardcoded product/project chips; use it for prototypes but use AppShell + Topbar directly for production apps that need real switcher props."
|
|
591
660
|
],
|
|
592
661
|
example: `import { Topbar } from "@godxjp/ui/layout";
|
|
593
662
|
import { AppShell } from "@godxjp/ui/layout";
|
|
@@ -598,7 +667,7 @@ import {
|
|
|
598
667
|
|
|
599
668
|
// Entity-switcher example: product chip opens an entity dropdown,
|
|
600
669
|
// project chip is hidden (no project concept in this app).
|
|
601
|
-
function MyShell({ children }: {
|
|
670
|
+
function MyShell({ children }: { content: React.ReactNode }) {
|
|
602
671
|
const [collapsed, setCollapsed] = React.useState(false);
|
|
603
672
|
const [unread, setUnread] = React.useState(true);
|
|
604
673
|
|
|
@@ -661,7 +730,7 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
661
730
|
"Dashboard section inside a flush PageContainer that shows a brief intro paragraph or status summary strip before a full-width chart or DataTable.",
|
|
662
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.",
|
|
663
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.',
|
|
664
|
-
"Accounting detail pages (e.g., journal entry list) where a summary
|
|
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."
|
|
665
734
|
],
|
|
666
735
|
related: [
|
|
667
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).',
|
|
@@ -706,7 +775,7 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
706
775
|
"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."
|
|
707
776
|
],
|
|
708
777
|
useCases: [
|
|
709
|
-
"Invoice / transaction detail page: list of records in `children` (DataTable), selected-record detail panel in `aside` (
|
|
778
|
+
"Invoice / transaction detail page: list of records in `children` (DataTable), selected-record detail panel in `aside` (Descriptions + Timeline).",
|
|
710
779
|
'Accounting ledger drill-down: account list on the left, chart-of-accounts metadata or running balance breakdown on the right using `asideWidth="sm"`.',
|
|
711
780
|
"Document review workflow: PDF or rich-text viewer in `children`, approval form or annotation panel in `aside`.",
|
|
712
781
|
"Settings page with a category list or Steps navigator in `children` and a live preview or summary card in `aside`.",
|
|
@@ -741,14 +810,14 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
741
810
|
usage: [
|
|
742
811
|
"DO import from `@godxjp/ui/layout` (not from a navigation or general sub-path) and pass a single `items` prop \u2014 an ordered array of `{ label, to? }` objects. No children, no sub-components, no render-prop API.",
|
|
743
812
|
'DO omit `to` on the last (current-page) segment \u2014 the component automatically renders it as a `<span aria-current="page">` instead of a router `<Link>`. Passing `to` on the last item does NOT make it a link; drop it intentionally.',
|
|
744
|
-
"DO pass the Breadcrumb node as a ReactNode to the `breadcrumb` prop of `
|
|
813
|
+
"DO pass the Breadcrumb node as a ReactNode to the `breadcrumb` prop of `AppShell` (or `AppShell`) for shell-level breadcrumbs, or to `PageContainer`'s `breadcrumb` prop (which accepts `BreadcrumbItemProp[]` directly \u2014 not a ReactNode). When passing to `PageContainer`, pass the raw array; when passing to `AppShell`, wrap it: `breadcrumb={<Breadcrumb items={\u2026} />}`.",
|
|
745
814
|
'DON\'T hand-roll a breadcrumb strip (divs with chevrons, anchors, separators) \u2014 Breadcrumb ships the `<nav aria-label="Breadcrumb">` + `<ol>` + `aria-hidden` chevrons. Any custom trail is a violation of the no-hand-roll rule and will fail `npm run ui:audit`.',
|
|
746
815
|
"DON'T use Breadcrumb for tab-style or step-style navigation (multi-step forms, wizard progress). Those flows belong to `Steps`. Breadcrumb is strictly a spatial location trail, not a process indicator.",
|
|
747
816
|
"The component is fully uncontrolled and stateless \u2014 it renders whatever `items` you pass. Dynamic breadcrumbs (route-derived, breadcrumb context, etc.) must be assembled in the parent and passed down as a plain array; there is no internal routing awareness."
|
|
748
817
|
],
|
|
749
818
|
useCases: [
|
|
750
819
|
"Per-page location trail on any admin page deeper than two levels \u2014 e.g. Home \u2192 Accounting \u2192 Invoices \u2192 Invoice #1042 \u2014 passed to `PageContainer`'s `breadcrumb` prop so it appears above the page `<h1>`.",
|
|
751
|
-
"Persistent shell-level breadcrumb in a `
|
|
820
|
+
"Persistent shell-level breadcrumb in a `AppShell` or `AppShell` layout that updates as the user navigates between Inertia/React Router pages; constructed from route params and passed as a ReactNode to `AppShell`'s `breadcrumb` prop.",
|
|
752
821
|
"Master-detail drill-down in an accounting app: the detail page (journal entry, partner, bank account) shows a breadcrumb back to the list and to the domain root, giving the user a one-click escape without using the browser back button.",
|
|
753
822
|
"Embedded sub-panel breadcrumb inside a `SplitPane` or `Sheet` where a secondary content area has its own navigable hierarchy and needs a compact location indicator.",
|
|
754
823
|
"Audit log or document history page where the entity being reviewed (invoice, payment) is the current segment and the parent module (Accounting, Receivables) is a clickable ancestor.",
|
|
@@ -756,7 +825,7 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
756
825
|
],
|
|
757
826
|
related: [
|
|
758
827
|
"PageContainer \u2014 accepts `breadcrumb` as `BreadcrumbItemProp[]` (raw array, not a ReactNode); use this when each page owns its own breadcrumb and you want it co-located with the page title, actions, and body.",
|
|
759
|
-
"
|
|
828
|
+
"AppShell \u2014 accepts `breadcrumb` as `ReactNode`; pass `<Breadcrumb items={\u2026} />` here when the breadcrumb is a persistent shell-level strip that sits above all page content rather than being owned by individual pages.",
|
|
760
829
|
"Steps \u2014 use instead of Breadcrumb when showing progress through an ordered multi-step flow (wizard, checkout, onboarding); Steps conveys sequence and completion state, not spatial location.",
|
|
761
830
|
"PrefetchLink \u2014 if ancestor breadcrumb segments should prefetch their destination query on hover/focus, consider pairing the `to` values with `PrefetchLink` in a custom breadcrumb or pre-warming the cache on mount; Breadcrumb's internal links are plain react-router-dom `<Link>` with no prefetch behaviour."
|
|
762
831
|
],
|
|
@@ -811,7 +880,7 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
811
880
|
],
|
|
812
881
|
useCases: [
|
|
813
882
|
'Primary form submission in a Dialog or Sheet (e.g. `<Button type="submit" disabled={form.processing}>\u4FDD\u5B58</Button>`) \u2014 the `disabled` prop greys it out and blocks pointer events, preventing double-submit during async operations.',
|
|
814
|
-
'Destructive confirmation inside a Dialog \u2014 pair `
|
|
883
|
+
'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.',
|
|
815
884
|
'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.',
|
|
816
885
|
"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.",
|
|
817
886
|
"Async mutation trigger in an accounting workflow (e.g. 'Sync from MF', 'Export CSV') \u2014 disable on processing state; pair with `MutationFeedback` for error/retry UI rather than inline `try/catch` alerts.",
|
|
@@ -850,7 +919,7 @@ import { Trash2 } from "lucide-react";
|
|
|
850
919
|
name: "columns",
|
|
851
920
|
type: "ColumnDef<T>[]",
|
|
852
921
|
required: true,
|
|
853
|
-
description: "Column definitions. Each column: {
|
|
922
|
+
description: "Column definitions. Each column: { value: string; header: ReactNode; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hiddenOnMobile?: boolean }. If render is omitted, the raw value at row[key] is rendered as a string."
|
|
854
923
|
},
|
|
855
924
|
{
|
|
856
925
|
name: "getRowId",
|
|
@@ -892,12 +961,12 @@ import { Trash2 } from "lucide-react";
|
|
|
892
961
|
},
|
|
893
962
|
{
|
|
894
963
|
name: "sort",
|
|
895
|
-
type: "{
|
|
964
|
+
type: "{ value: string; direction: 'asc' | 'desc' }",
|
|
896
965
|
description: "Active sort state. When provided alongside onSortChange, sortable columns show directional arrow icons and are clickable. Clicking the active column twice clears sort (calls onSortChange(undefined))."
|
|
897
966
|
},
|
|
898
967
|
{
|
|
899
968
|
name: "onSortChange",
|
|
900
|
-
type: "(sort: {
|
|
969
|
+
type: "(sort: { value: string; direction: 'asc' | 'desc' } | undefined) => void",
|
|
901
970
|
description: "Called when a sortable column header is clicked. Receives undefined when sort is cleared (third click on same column)."
|
|
902
971
|
},
|
|
903
972
|
{
|
|
@@ -956,10 +1025,10 @@ type Invoice = {
|
|
|
956
1025
|
};
|
|
957
1026
|
|
|
958
1027
|
const columns: ColumnDef<Invoice>[] = [
|
|
959
|
-
{
|
|
960
|
-
{
|
|
1028
|
+
{ value: "id", header: "Invoice #", width: "w-32" },
|
|
1029
|
+
{ value: "customer", header: "Customer" },
|
|
961
1030
|
{
|
|
962
|
-
|
|
1031
|
+
value: "status",
|
|
963
1032
|
header: "Status",
|
|
964
1033
|
render: (row) => (
|
|
965
1034
|
<Badge
|
|
@@ -971,7 +1040,7 @@ const columns: ColumnDef<Invoice>[] = [
|
|
|
971
1040
|
</Badge>
|
|
972
1041
|
),
|
|
973
1042
|
},
|
|
974
|
-
{
|
|
1043
|
+
{ value: "amount", header: "Amount", align: "right", sortable: true },
|
|
975
1044
|
];
|
|
976
1045
|
|
|
977
1046
|
export default function InvoiceList({
|
|
@@ -982,7 +1051,7 @@ export default function InvoiceList({
|
|
|
982
1051
|
loading: boolean;
|
|
983
1052
|
}) {
|
|
984
1053
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
985
|
-
const [sort, setSort] = useState<{
|
|
1054
|
+
const [sort, setSort] = useState<{ value: string; direction: "asc" | "desc" } | undefined>();
|
|
986
1055
|
|
|
987
1056
|
return (
|
|
988
1057
|
<DataTable
|
|
@@ -1050,20 +1119,20 @@ export default function InvoiceList({
|
|
|
1050
1119
|
"DO use <CardContent flush> for edge-to-edge children such as DataTable, Table, or a Tabs list \u2014 this removes horizontal padding. Combine with <CardContent tight> when there is no visual gap needed after the header, and <CardContent solo> when there is no CardHeader above (top padding matches the card shell).",
|
|
1051
1120
|
"DO use <CardFooter separated> to render a top-bordered action band (Save/Cancel buttons, table summary row). Use <CardFooter flush> for a full-bleed footer bar.",
|
|
1052
1121
|
"DO use <CardCover> as the first child for full-bleed cover media \u2014 the header below it uses card-section top spacing, not the card shell.",
|
|
1053
|
-
"DON'T hand-roll a stat/KPI tile with <Card> + raw divs \u2014 use <
|
|
1122
|
+
"DON'T hand-roll a stat/KPI tile with <Card> + raw divs \u2014 use <StatCard> (label, value, hint, delta, layout, inverse props) which is already a Card internally with correct token-driven layout."
|
|
1054
1123
|
],
|
|
1055
1124
|
useCases: [
|
|
1056
|
-
'Dashboard KPI summary row: wrap each metric in <
|
|
1057
|
-
'Invoice or order detail panel: <Card accent="primary"> with <CardHeader banded><CardTitle>, <CardContent> body rows (use <
|
|
1125
|
+
'Dashboard KPI summary row: wrap each metric in <StatCard> (or a plain <Card size="compact"> with <CardContent>) to render a uniform grid of labeled value tiles with optional trend deltas.',
|
|
1126
|
+
'Invoice or order detail panel: <Card accent="primary"> with <CardHeader banded><CardTitle>, <CardContent> body rows (use <Descriptions> inside), and <CardFooter separated> holding approve/reject buttons.',
|
|
1058
1127
|
"Section container on a settings or form page: a single <Card> wrapping a <CardHeader><CardTitle> plus <CardContent> containing <FormField> groups, with <CardFooter separated> for Save/Cancel.",
|
|
1059
1128
|
"Data table with toolbar: <Card> + <CardHeader> (title + filter controls in <CardAction>) + <CardContent flush> containing <DataTable> \u2014 <CardContent flush> removes horizontal padding so the table header spans full width.",
|
|
1060
1129
|
'Featured announcement or alert card: <Card variant="featured"> with an accent stripe (<accent="warning">) to visually elevate a card above sibling cards on the page.',
|
|
1061
1130
|
"Media/cover card (e.g. entity profile): <CardCover> first (full-bleed image), then <CardHeader> + <CardContent> below it for structured metadata."
|
|
1062
1131
|
],
|
|
1063
1132
|
related: [
|
|
1064
|
-
"
|
|
1133
|
+
"StatCard \u2014 use instead of a plain Card when rendering a KPI/metric tile (label + value + optional delta/hint). StatCard is a Card internally; do not re-wrap it in another Card.",
|
|
1065
1134
|
"CardContent \u2014 mandatory inner wrapper for all body content inside Card. Provides the correct padding and supports flush/tight/solo variants. The only correct way to put padded content inside Card.",
|
|
1066
|
-
"
|
|
1135
|
+
"Descriptions \u2014 use inside <CardContent> when body content is a label-value metadata list (e.g. entity details, invoice fields); do not hand-roll a dl/dt/dd grid.",
|
|
1067
1136
|
"DataState / InfiniteQueryState \u2014 use instead of Card when the content is a TanStack Query-driven list that needs automatic skeleton, empty, and error states; Card does not manage loading lifecycle."
|
|
1068
1137
|
],
|
|
1069
1138
|
example: `import { Card, CardHeader, CardTitle, CardContent } from "@godxjp/ui/data-display";
|
|
@@ -1102,19 +1171,19 @@ export default function InvoiceList({
|
|
|
1102
1171
|
"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.",
|
|
1103
1172
|
"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.",
|
|
1104
1173
|
"DON'T: Nest a FilterBar inside <CardContent flush> \u2014 flush strips horizontal padding and FilterBar will lose its own padding. Put FilterBar outside the flush CardContent or in a separate non-flush CardContent above it.",
|
|
1105
|
-
"DON'T: Wrap a
|
|
1174
|
+
"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."
|
|
1106
1175
|
],
|
|
1107
1176
|
useCases: [
|
|
1108
1177
|
"Wrapping a form body (Input, Select, Textarea fields) inside a Card that has a CardHeader title \u2014 ensures the form fields have correct internal padding.",
|
|
1109
1178
|
"Hosting a DataTable inside a Card edge-to-edge: <CardContent flush><DataTable .../></CardContent> \u2014 the table occupies the full card width with the card's border acting as the table container.",
|
|
1110
1179
|
"Dashboard detail panels where the card has no title \u2014 <CardContent solo> gives top padding equivalent to the card shell so the content doesn't sit too close to the top border.",
|
|
1111
|
-
"Placing a
|
|
1180
|
+
"Placing a Descriptions or Timeline inside a card to display invoice/accounting details \u2014 <CardContent> provides the standard 16px (or density-adjusted) padding without needing manual className.",
|
|
1112
1181
|
"Pairing with <CardHeader banded> and <CardFooter separated> in a multi-section layout such as a payment summary card \u2014 each section slot (header, content, footer) carries its own semantic spacing tokens.",
|
|
1113
1182
|
"Putting a ScrollArea inside <CardContent> (not flush) to create a scrollable card body with consistent padding, e.g. a chat or log viewer panel."
|
|
1114
1183
|
],
|
|
1115
1184
|
related: [
|
|
1116
1185
|
"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.",
|
|
1117
|
-
"
|
|
1186
|
+
"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.",
|
|
1118
1187
|
"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.",
|
|
1119
1188
|
"SkeletonCard \u2014 the loading placeholder for a Card+CardContent shape; swap in SkeletonCard while card data is loading instead of rendering an empty CardContent."
|
|
1120
1189
|
],
|
|
@@ -1129,9 +1198,9 @@ export default function InvoiceList({
|
|
|
1129
1198
|
rules: [37, 38]
|
|
1130
1199
|
},
|
|
1131
1200
|
{
|
|
1132
|
-
name: "
|
|
1201
|
+
name: "StatCard",
|
|
1133
1202
|
group: "data-display",
|
|
1134
|
-
tagline: "KPI tile. \u26A0\uFE0F
|
|
1203
|
+
tagline: "KPI tile. \u26A0\uFE0F StatCard IS ALREADY a bordered Card \u2014 render it DIRECTLY in ResponsiveGrid. NEVER wrap it in <Card>/<CardContent> (that double-borders it \u2192 looks too thick). NO accent prop (accent is a Card prop).",
|
|
1135
1204
|
props: [
|
|
1136
1205
|
{ name: "label", type: "ReactNode", required: true, description: "Metric name." },
|
|
1137
1206
|
{
|
|
@@ -1155,12 +1224,12 @@ export default function InvoiceList({
|
|
|
1155
1224
|
{ name: "align", type: '"start" | "end"', description: "Align the metric group." }
|
|
1156
1225
|
],
|
|
1157
1226
|
usage: [
|
|
1158
|
-
"DO place
|
|
1227
|
+
"DO place StatCard directly as a child of ResponsiveGrid \u2014 it renders its own bordered Card shell internally, so no wrapping <Card> or <CardContent> is needed or allowed. Wrapping creates a double border.",
|
|
1159
1228
|
"DO pass `delta` as a sign-prefixed string (e.g. '+12%' or '-3%') to get automatic color tone: '+' renders text-success, '-' renders text-destructive. For metrics where a negative delta is good (e.g. cost reduction, error rate), pass `inverse` so the tone is flipped correctly.",
|
|
1160
1229
|
"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.",
|
|
1161
|
-
"DO NOT add an `accent` prop \u2014 accent is a Card prop and
|
|
1162
|
-
"DO NOT hand-roll a KPI tile using a plain <Card><CardContent>.
|
|
1163
|
-
"WHILE data is loading, replace each
|
|
1230
|
+
"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
|
+
"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 <SkeletonCard /> at the same grid position \u2014 never render an empty value string or a spinner inside StatCard itself."
|
|
1164
1233
|
],
|
|
1165
1234
|
useCases: [
|
|
1166
1235
|
"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.",
|
|
@@ -1168,132 +1237,82 @@ export default function InvoiceList({
|
|
|
1168
1237
|
"Coupon/membership admin overview: active members, live coupons, monthly redemptions, and total discount amount \u2014 the canonical example in the catalog.",
|
|
1169
1238
|
"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.",
|
|
1170
1239
|
"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.",
|
|
1171
|
-
"Loading state for any KPI grid: render the same ResponsiveGrid columns filled with <SkeletonCard /> components while the query is in-flight, then replace with
|
|
1240
|
+
"Loading state for any KPI grid: render the same ResponsiveGrid columns filled with <SkeletonCard /> components while the query is in-flight, then replace with StatCard tiles once data resolves."
|
|
1172
1241
|
],
|
|
1173
1242
|
related: [
|
|
1174
|
-
"ResponsiveGrid \u2014 required layout wrapper for
|
|
1175
|
-
"SkeletonCard \u2014 exact loading placeholder shaped like a
|
|
1176
|
-
"
|
|
1177
|
-
"Card + CardContent \u2014 use when you need a general-purpose content container with a header, footer, or arbitrary body; do NOT wrap
|
|
1243
|
+
"ResponsiveGrid \u2014 required layout wrapper for StatCard grids; controls column count and responsive breakpoints. Always pair them together.",
|
|
1244
|
+
"SkeletonCard \u2014 exact loading placeholder shaped like a StatCard tile; swap in while KPI data is fetching, then replace with the real StatCard.",
|
|
1245
|
+
"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
|
+
"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."
|
|
1178
1247
|
],
|
|
1179
|
-
example: `import {
|
|
1248
|
+
example: `import { StatCard } from "@godxjp/ui/data-display";
|
|
1180
1249
|
import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
1181
1250
|
|
|
1182
|
-
// \u2705
|
|
1251
|
+
// \u2705 StatCard sits directly in the grid \u2014 it draws its own card + border.
|
|
1183
1252
|
<ResponsiveGrid columns={3}>
|
|
1184
|
-
<
|
|
1185
|
-
<
|
|
1186
|
-
<
|
|
1253
|
+
<StatCard label="\u7DCF\u4F1A\u54E1\u6570" value="12,450" hint="\u5148\u6708\u6BD4 +3%" />
|
|
1254
|
+
<StatCard label="\u6708\u6B21\u58F2\u4E0A" value="\xA58,200,000" delta="+12%" />
|
|
1255
|
+
<StatCard label="\u5229\u7528\u7387" value="68.4%" />
|
|
1187
1256
|
</ResponsiveGrid>
|
|
1188
1257
|
|
|
1189
|
-
// \u274C Double border \u2014 do NOT wrap
|
|
1190
|
-
// <Card><CardContent><
|
|
1191
|
-
storyPath: "data-display/
|
|
1258
|
+
// \u274C Double border \u2014 do NOT wrap StatCard in a Card:
|
|
1259
|
+
// <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>`,
|
|
1260
|
+
storyPath: "data-display/StatCard.stories.tsx",
|
|
1192
1261
|
rules: []
|
|
1193
1262
|
},
|
|
1194
1263
|
{
|
|
1195
|
-
name: "
|
|
1264
|
+
name: "Badge",
|
|
1196
1265
|
group: "data-display",
|
|
1197
|
-
tagline: "
|
|
1266
|
+
tagline: "Plain or lifecycle badge. Use `variant` for static chips, or `status` to auto-map lifecycle keys to semantic tone + icon. Labels never wrap.",
|
|
1198
1267
|
props: [
|
|
1199
1268
|
{
|
|
1200
|
-
name: "
|
|
1201
|
-
type: "
|
|
1202
|
-
|
|
1203
|
-
description: "
|
|
1269
|
+
name: "variant",
|
|
1270
|
+
type: '"default" | "secondary" | "outline" | "success" | "warning" | "destructive" | "info" | "neutral"',
|
|
1271
|
+
defaultValue: '"default"',
|
|
1272
|
+
description: "Visual variant. Overrides the auto-mapped status tone when status is provided."
|
|
1204
1273
|
},
|
|
1205
1274
|
{
|
|
1206
|
-
name: "
|
|
1207
|
-
type:
|
|
1208
|
-
description: "
|
|
1275
|
+
name: "status",
|
|
1276
|
+
type: "string",
|
|
1277
|
+
description: "Lifecycle key. Known keys auto-map to variant + icon + i18n label; unknown keys fall back to neutral."
|
|
1209
1278
|
},
|
|
1210
1279
|
{
|
|
1211
1280
|
name: "icon",
|
|
1212
|
-
type: "
|
|
1213
|
-
description: "
|
|
1281
|
+
type: "React.ComponentType<{ className?: string }> | null",
|
|
1282
|
+
description: "Leading icon override. Pass null to suppress the auto status icon."
|
|
1214
1283
|
},
|
|
1215
|
-
{
|
|
1216
|
-
name: "label",
|
|
1217
|
-
type: "ReactNode",
|
|
1218
|
-
description: "Override display text (default: i18n of key, or raw status)."
|
|
1219
|
-
}
|
|
1220
|
-
],
|
|
1221
|
-
usage: [
|
|
1222
|
-
"DO pass one of the known English lifecycle keys (active, draft, pending, completed, failed, cancelled, deleted, bounced, scheduled, sending, temporary, permanent, done, delivered, succeeded, internal, public, private, ASSIGNMENT_STATUS_ACTIVE, ASSIGNMENT_STATUS_SUSPENDED, ASSIGNMENT_STATUS_TERMINATED) as `status` \u2014 these resolve to the correct tone + icon + i18n label automatically without any extra props.",
|
|
1223
|
-
"DO pass `tone` explicitly when `status` is a localized string or categorical tier label (e.g. a Japanese tier name like '\u30D7\u30EC\u30DF\u30A2\u30E0') \u2014 unknown keys fall back silently to neutral grey, which is visually wrong for a success/warning tier.",
|
|
1224
|
-
"DO pass `icon={null}` for categorical / tier badges (membership tiers, plan names, visibility levels) where a lifecycle glyph (checkmark, clock, X) would be semantically misleading. Never omit this override for non-lifecycle uses.",
|
|
1225
|
-
"DON'T extend STATUS_MAP inline at call sites \u2014 to add a new global status, add the key to STATUS_MAP in status-badge.tsx AND add a `status.<key>` i18n key. Never inline a one-off `<Badge>` just because a status key doesn't exist yet.",
|
|
1226
|
-
"DON'T use `label` to carry translated text for known keys \u2014 the component already calls `t('status.<key>')` automatically. Only use `label` when you must show a domain-specific override that diverges from the canonical i18n string.",
|
|
1227
|
-
"A11y: the icon is rendered with `aria-hidden='true'`; the visible `<span>` child carries the accessible label. Do not add separate `aria-label` on the wrapper \u2014 it is redundant and will cause double-announce."
|
|
1228
|
-
],
|
|
1229
|
-
useCases: [
|
|
1230
|
-
"Invoice / payment status column in a DataTable \u2014 show 'draft', 'pending', 'completed', 'failed' states with color-coded tone and icon, no extra configuration needed for standard keys.",
|
|
1231
|
-
"Journal entry or approval workflow state \u2014 lifecycle values like 'pending', 'active', 'cancelled' auto-resolve; pair with KeyValueGrid for detail pages showing a single entity's current state.",
|
|
1232
|
-
"Subscription or membership tier label in a user detail view \u2014 pass the tier name as `status`, set `tone='success'|'warning'` for the tier rank, and `icon={null}` to suppress the lifecycle glyph.",
|
|
1233
|
-
"Delivery or send status in an email / notification log table \u2014 'bounced', 'sending', 'delivered', 'scheduled' all resolve out of the box with distinct icons and tones.",
|
|
1234
|
-
"Assignment or role activation status \u2014 the ASSIGNMENT_STATUS_* keys map to active/suspended/terminated tones and icons without any manual configuration.",
|
|
1235
|
-
"Visibility / publication status chip (public / private / internal) \u2014 all three keys are mapped in STATUS_MAP, so a CMS or document list gets consistent color coding for free."
|
|
1236
|
-
],
|
|
1237
|
-
related: [
|
|
1238
|
-
"Badge \u2014 use Badge (not StatusBadge) for static category tags, labels, or counts that have no lifecycle meaning (e.g. 'A/B test', 'New', category chips). Badge takes a `variant` prop; StatusBadge takes a `status` key. The rule of thumb: if the value can change over time as an entity moves through a workflow, use StatusBadge; if it's a fixed label, use Badge.",
|
|
1239
|
-
"CodeBadge \u2014 use CodeBadge for monospaced identifier chips (invoice numbers, error codes, HTTP status codes). Never use StatusBadge to display a code or ID string.",
|
|
1240
|
-
"DataTable \u2014 StatusBadge is the canonical cell renderer for status columns inside DataTable. Compose them together: render StatusBadge inside the column `cell` callback rather than hand-rolling colored text or raw Badge variants for lifecycle data.",
|
|
1241
|
-
"MutationFeedback \u2014 use MutationFeedback (not StatusBadge) to communicate the outcome of an in-flight or just-completed mutation (save, delete, submit). StatusBadge is a display-only chip for persisted entity state, not transient operation feedback."
|
|
1242
|
-
],
|
|
1243
|
-
example: `import { StatusBadge } from "@godxjp/ui/data-display";
|
|
1244
|
-
|
|
1245
|
-
<>
|
|
1246
|
-
<StatusBadge status="active" label="\u516C\u958B\u4E2D" /> {/* green check */}
|
|
1247
|
-
<StatusBadge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} /> {/* tier pill, no icon */}
|
|
1248
|
-
<StatusBadge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
|
|
1249
|
-
</>`,
|
|
1250
|
-
storyPath: "data-display/StatusBadge.stories.tsx",
|
|
1251
|
-
rules: [35, 36]
|
|
1252
|
-
},
|
|
1253
|
-
{
|
|
1254
|
-
name: "Badge",
|
|
1255
|
-
group: "data-display",
|
|
1256
|
-
tagline: "Plain label chip with semantic variants. Use for static category tags; use StatusBadge for lifecycle status.",
|
|
1257
|
-
props: [
|
|
1258
|
-
{
|
|
1259
|
-
name: "variant",
|
|
1260
|
-
type: '"default" | "secondary" | "destructive" | "outline" | "success" | "warning"',
|
|
1261
|
-
defaultValue: '"default"',
|
|
1262
|
-
description: "Visual variant."
|
|
1263
|
-
},
|
|
1264
|
-
{ name: "children", type: "ReactNode", required: true, description: "Badge text/content." }
|
|
1284
|
+
{ name: "children", type: "ReactNode", description: "Badge label. When omitted with status, Badge renders the translated lifecycle label or raw status." }
|
|
1265
1285
|
],
|
|
1266
1286
|
usage: [
|
|
1267
1287
|
"DO pick the correct variant semantically: `success` (approved/paid), `warning` (pending/overdue), `destructive` (rejected/error), `secondary` (neutral category), `outline` (subtle label), `default` (primary accent). Never force a colour just for aesthetics \u2014 agents and screen readers read the variant as intent.",
|
|
1268
|
-
"DO
|
|
1269
|
-
"DO
|
|
1288
|
+
"DO use `status` for entity lifecycle statuses (active, draft, pending, cancelled, failed, scheduled, etc.) so the component resolves the correct tone, icon, and i18n label.",
|
|
1289
|
+
"DO pass `variant` explicitly for localized labels or categorical tiers, and pass `icon={null}` when a lifecycle glyph would be misleading.",
|
|
1270
1290
|
"Badge renders as a `<div>` (HTMLAttributes<HTMLDivElement>). It carries no interactive semantics. If you need a clickable chip, wrap it in a `<button>` or use a Button with a matching variant \u2014 never add an `onClick` directly to Badge without an accessible role.",
|
|
1271
1291
|
"Badge is a leaf \u2014 pass plain text or a short ReactNode as children. Do NOT nest another Badge, a Button, or interactive controls inside it; that breaks focus order and creates invalid HTML (div-in-inline-context).",
|
|
1272
|
-
"Use semantic tokens for any className overrides (`text-muted-foreground`, `bg-destructive`) \u2014 never raw Tailwind palette classes like `bg-green-500`.
|
|
1292
|
+
"Use semantic tokens for any className overrides (`text-muted-foreground`, `bg-destructive`) \u2014 never raw Tailwind palette classes like `bg-green-500`."
|
|
1273
1293
|
],
|
|
1274
1294
|
useCases: [
|
|
1275
1295
|
'Category or tier labels on table rows \u2014 e.g. plan tier (`<Badge variant="secondary">Pro</Badge>`), document type (`<Badge variant="outline">Invoice</Badge>`), or locale tag (`<Badge variant="secondary">EN</Badge>`).',
|
|
1276
|
-
'Approval or review state in an accounting list where the value is not a lifecycle key in
|
|
1296
|
+
'Approval or review state in an accounting list where the value is not a lifecycle key in Badge\'s STATUS_MAP \u2014 e.g. a custom approval tier like `<Badge tone="success">\u627F\u8A8D\u6E08</Badge>` or `<Badge tone="warning">\u8981\u78BA\u8A8D</Badge>`.',
|
|
1277
1297
|
"Inline count or highlight next to a heading or nav item \u2014 e.g. `<Badge variant=\"destructive\">3</Badge>` beside 'Overdue invoices' to draw attention to a non-zero count.",
|
|
1278
1298
|
'Feature flags or experiment variant labels on admin records \u2014 e.g. `<Badge variant="outline">A/B</Badge>` alongside a campaign row to indicate it is in a split test.',
|
|
1279
|
-
"Read-only metadata chips inside a
|
|
1299
|
+
"Read-only metadata chips inside a Descriptions.Item or Card header where a lifecycle icon would be visually heavy \u2014 e.g. currency code, payment method, or region tag."
|
|
1280
1300
|
],
|
|
1281
1301
|
related: [
|
|
1282
|
-
"StatusBadge \u2014 use instead of Badge whenever the value is an entity lifecycle status (active, draft, pending, cancelled, failed, etc.). StatusBadge auto-resolves icon, colour, and i18n label from STATUS_MAP; Badge does none of that. Mixing them for the same semantic concept breaks visual consistency.",
|
|
1283
|
-
"CodeBadge \u2014 use instead of Badge for structured reference codes (internal order IDs, seller codes, Yamato tracking). CodeBadge has a typed `kind` prop and renders a prefix label + icon; Badge has no such structure.",
|
|
1284
1302
|
"Button \u2014 use instead of Badge when the chip must be interactive (clickable, toggleable). Badge carries no button role or keyboard handler; a naked `onClick` on Badge is inaccessible."
|
|
1285
1303
|
],
|
|
1286
1304
|
example: `import { Badge } from "@godxjp/ui/data-display";
|
|
1287
1305
|
|
|
1288
1306
|
<Badge variant="secondary">A/B</Badge>
|
|
1289
|
-
<Badge
|
|
1307
|
+
<Badge status="active">\u516C\u958B\u4E2D</Badge>
|
|
1308
|
+
<Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null}>\u30D7\u30EC\u30DF\u30A2\u30E0</Badge>`,
|
|
1290
1309
|
storyPath: "data-display/Badge.stories.tsx",
|
|
1291
1310
|
rules: [35]
|
|
1292
1311
|
},
|
|
1293
1312
|
{
|
|
1294
|
-
name: "
|
|
1313
|
+
name: "Descriptions",
|
|
1295
1314
|
group: "data-display",
|
|
1296
|
-
tagline: "Responsive definition grid for detail-page metadata. COMPOUND \u2014 value goes in
|
|
1315
|
+
tagline: "Responsive definition grid for detail-page metadata. COMPOUND \u2014 value goes in Descriptions.Item children.",
|
|
1297
1316
|
props: [
|
|
1298
1317
|
{
|
|
1299
1318
|
name: "columns",
|
|
@@ -1305,39 +1324,39 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1305
1324
|
name: "children",
|
|
1306
1325
|
type: "ReactNode",
|
|
1307
1326
|
required: true,
|
|
1308
|
-
description: "
|
|
1327
|
+
description: "Descriptions.Item elements."
|
|
1309
1328
|
}
|
|
1310
1329
|
],
|
|
1311
1330
|
usage: [
|
|
1312
|
-
'DO use
|
|
1331
|
+
'DO use Descriptions.Item as the ONLY direct child \u2014 never raw <div>, <dt>/<dd>, or plain text nodes. Every label/value pair must be wrapped in <Descriptions.Item label="\u2026">value</Descriptions.Item>.',
|
|
1313
1332
|
"DO pass span={2} or span={3} on an Item when its value is long (e.g. a full address, a memo field, a JSON blob) \u2014 span={2} applies sm:col-span-2 and span={3} applies lg:col-span-3, keeping the grid aligned across breakpoints.",
|
|
1314
1333
|
"DO pass mono on Item for machine-readable values: IDs, UUIDs, file paths, currency codes, JSON snippets. This sets font-mono + break-all so long strings wrap rather than overflow.",
|
|
1315
|
-
"DO embed any ReactNode as the Item child \u2014
|
|
1316
|
-
"DON'T use
|
|
1317
|
-
"DON'T add className padding or margin to the root
|
|
1334
|
+
"DO embed any ReactNode as the Item child \u2014 Badge, Badge, formatDate output, a Tooltip-wrapped value, or a plain string all work. The value slot is not text-only.",
|
|
1335
|
+
"DON'T use Descriptions as a hand-rolled <dl>/<dt>/<dd> replacement for prose or running text \u2014 it is for structured metadata on detail/show pages only. For flowing key\u2192value prose, use a plain <dl>.",
|
|
1336
|
+
"DON'T add className padding or margin to the root Descriptions to simulate a Card \u2014 wrap it in CardContent instead. Descriptions provides only grid layout (gap-x-6 gap-y-3); outer spacing is the Card/CardContent concern."
|
|
1318
1337
|
],
|
|
1319
1338
|
useCases: [
|
|
1320
1339
|
"Detail/show page header block \u2014 displaying entity metadata such as invoice number, status, due date, vendor name, and payment method in a 2- or 3-column grid before the line-item DataTable.",
|
|
1321
1340
|
"Account or member profile panel \u2014 showing user ID (mono), plan, registered date, email, and a status Badge in one scannable block instead of a vertical stack of FormField-looking rows.",
|
|
1322
1341
|
"Accounting journal entry detail \u2014 date, reference code (mono), debit account, credit account, amount, and memo (span={2}) grouped in a compact grid alongside a Timeline of audit events.",
|
|
1323
|
-
"Read-only summary step in a multi-step form or wizard \u2014 displaying the values the user entered before final submission (Steps +
|
|
1324
|
-
"Sidebar or Sheet detail pane \u2014 a narrow 1-column
|
|
1325
|
-
"API / webhook event inspector \u2014 showing event ID (mono, span={2}), event type, timestamp, HTTP status, and payload size in a grid, with a
|
|
1342
|
+
"Read-only summary step in a multi-step form or wizard \u2014 displaying the values the user entered before final submission (Steps + Descriptions), without any input controls.",
|
|
1343
|
+
"Sidebar or Sheet detail pane \u2014 a narrow 1-column Descriptions inside a Sheet presenting the selected row's metadata while the main DataTable stays visible.",
|
|
1344
|
+
"API / webhook event inspector \u2014 showing event ID (mono, span={2}), event type, timestamp, HTTP status, and payload size in a grid, with a Badge for the status code."
|
|
1326
1345
|
],
|
|
1327
1346
|
related: [
|
|
1328
|
-
"Card / CardContent \u2014
|
|
1329
|
-
"DataTable \u2014 use DataTable when you have multiple rows of the same entity type that need sorting, filtering, or pagination. Use
|
|
1330
|
-
"Table \u2014 use Table (the lower-level primitive) for tabular data with explicit column headers and multiple data rows. Use
|
|
1331
|
-
"Stack / Inline \u2014 use Stack or Inline for arbitrary vertical/horizontal layout of heterogeneous UI elements. Use
|
|
1332
|
-
],
|
|
1333
|
-
example: `import {
|
|
1334
|
-
|
|
1335
|
-
<
|
|
1336
|
-
<
|
|
1337
|
-
<
|
|
1338
|
-
<
|
|
1339
|
-
</
|
|
1340
|
-
storyPath: "data-display/
|
|
1347
|
+
"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.",
|
|
1348
|
+
"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).",
|
|
1349
|
+
"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).",
|
|
1350
|
+
"Stack / Inline \u2014 use Stack or Inline 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."
|
|
1351
|
+
],
|
|
1352
|
+
example: `import { Descriptions } from "@godxjp/ui/data-display";
|
|
1353
|
+
|
|
1354
|
+
<Descriptions columns={2}>
|
|
1355
|
+
<Descriptions.Item label="\u4F1A\u54E1ID" mono>{member.id}</Descriptions.Item>
|
|
1356
|
+
<Descriptions.Item label="\u30D7\u30E9\u30F3">{member.plan}</Descriptions.Item>
|
|
1357
|
+
<Descriptions.Item label="\u30E1\u30E2" span={2}>{member.note}</Descriptions.Item>
|
|
1358
|
+
</Descriptions>`,
|
|
1359
|
+
storyPath: "data-display/Descriptions.stories.tsx",
|
|
1341
1360
|
rules: []
|
|
1342
1361
|
},
|
|
1343
1362
|
{
|
|
@@ -1379,7 +1398,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1379
1398
|
rules: []
|
|
1380
1399
|
},
|
|
1381
1400
|
{
|
|
1382
|
-
name: "
|
|
1401
|
+
name: "Progress",
|
|
1383
1402
|
group: "data-display",
|
|
1384
1403
|
tagline: "Horizontal progress bar 0\u2013100 with optional label and semantic tone.",
|
|
1385
1404
|
props: [
|
|
@@ -1398,31 +1417,31 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1398
1417
|
}
|
|
1399
1418
|
],
|
|
1400
1419
|
usage: [
|
|
1401
|
-
'DO import from `@godxjp/ui/data-display`, not from a generic UI path: `import {
|
|
1420
|
+
'DO import from `@godxjp/ui/data-display`, not from a generic UI path: `import { Progress } from "@godxjp/ui/data-display";`',
|
|
1402
1421
|
"DO pass `value` as a 0\u2013100 number \u2014 the component clamps it internally via `Math.max(0, Math.min(100, value))`, so out-of-range values are safe but misleading; compute the real percentage before passing it.",
|
|
1403
|
-
'DO drive `tone` dynamically from business logic \u2014 e.g. `
|
|
1404
|
-
"DON'T use a `disabled` Slider as a read-only progress bar \u2014 Slider is semantically an interactive control even when disabled, which pollutes the a11y tree and exposes the wrong ARIA role (`slider` vs `progressbar`).
|
|
1405
|
-
"DON'T pass children or sub-components \u2014
|
|
1406
|
-
"DON'T use
|
|
1422
|
+
'DO drive `tone` dynamically from business logic \u2014 e.g. `variant={pct >= 80 ? "warning" : "success"}` \u2014 to communicate threshold status semantically rather than with raw colour classes.',
|
|
1423
|
+
"DON'T use a `disabled` Slider as a read-only progress bar \u2014 Slider is semantically an interactive control even when disabled, which pollutes the a11y tree and exposes the wrong ARIA role (`slider` vs `progressbar`). Progress renders the correct read-only indicator.",
|
|
1424
|
+
"DON'T pass children or sub-components \u2014 Progress is a single self-contained element (track + bar + label). The `label` prop is the only text injection point; don't wrap it in a custom parent div to add a label alongside it.",
|
|
1425
|
+
"DON'T use Progress for editable numeric input or range selection \u2014 it has no callbacks, no interactivity, and no form `name` prop. Use Slider (bounded range input) or Input (free-form number) for data-entry scenarios."
|
|
1407
1426
|
],
|
|
1408
1427
|
useCases: [
|
|
1409
1428
|
'Budget utilisation in an accounting dashboard \u2014 show how much of a monthly budget has been consumed, switching to `tone="warning"` when the figure crosses 80%.',
|
|
1410
1429
|
'Invoice payment progress \u2014 display the proportion of an invoice total that has been settled (e.g. partial payments), with a label like `"\xA545,000 / \xA560,000 \u652F\u6255\u6E08"` computed before passing `value`.',
|
|
1411
1430
|
"Storage or quota indicator in an admin panel \u2014 visualise disk usage, API quota, or seat licence consumption against a fixed limit.",
|
|
1412
1431
|
"Sync / import job completion feedback \u2014 surface the completion percentage of a long-running background job (polling the server) without giving the user an interactive control.",
|
|
1413
|
-
"
|
|
1414
|
-
"Multi-step onboarding or setup checklist \u2014 render one
|
|
1432
|
+
"StatCard companion \u2014 pair with a `StatCard` metric to add a visual fill below the KPI number, reinforcing how close a target is to being met.",
|
|
1433
|
+
"Multi-step onboarding or setup checklist \u2014 render one Progress per section (e.g. 3/5 steps complete = 60%) to give users a quick scan of overall progress across areas."
|
|
1415
1434
|
],
|
|
1416
1435
|
related: [
|
|
1417
|
-
"Slider \u2014 use Slider when the user must drag or set a bounded numeric value (volume, priority, price range); use
|
|
1418
|
-
"Steps \u2014 use Steps for a discrete, named sequence of phases (onboarding wizard, checkout flow) where each step has a label and a clear current/done/pending state; use
|
|
1419
|
-
'
|
|
1420
|
-
"
|
|
1436
|
+
"Slider \u2014 use Slider when the user must drag or set a bounded numeric value (volume, priority, price range); use Progress when the value is read-only and must not be interacted with.",
|
|
1437
|
+
"Steps \u2014 use Steps for a discrete, named sequence of phases (onboarding wizard, checkout flow) where each step has a label and a clear current/done/pending state; use Progress for a continuous 0\u2013100 fill.",
|
|
1438
|
+
'Badge / Badge \u2014 use Badge or Badge to communicate a categorical status label (e.g. "Paid", "Overdue") without a fill metaphor; use Progress when the numeric proportion itself is the information.',
|
|
1439
|
+
"StatCard \u2014 use StatCard to headline a single KPI metric with a title; compose Progress inside or alongside StatCard when a visual fill adds meaning to the number."
|
|
1421
1440
|
],
|
|
1422
|
-
example: `import {
|
|
1441
|
+
example: `import { Progress } from "@godxjp/ui/data-display";
|
|
1423
1442
|
|
|
1424
|
-
<
|
|
1425
|
-
storyPath: "data-display/
|
|
1443
|
+
<Progress value={pct} label={pct + "% \u4F7F\u7528\u4E2D"} variant={pct >= 80 ? "warning" : "success"} />`,
|
|
1444
|
+
storyPath: "data-display/Progress.stories.tsx",
|
|
1426
1445
|
rules: []
|
|
1427
1446
|
},
|
|
1428
1447
|
{
|
|
@@ -1440,7 +1459,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1440
1459
|
usage: [
|
|
1441
1460
|
"DO pass an array of `TimelineItem` objects to `items` \u2014 this is the ONLY prop; there are no sub-components to compose. Each item is `{ title, location?, time?, note?, current? }`. All fields except `title` are optional.",
|
|
1442
1461
|
"DO mark exactly one item with `current: true` to highlight the in-progress event. The component renders a `Plane` icon for the current item and a `CheckCircle2` icon for all past items \u2014 do NOT try to pass a custom icon; the icon is determined entirely by the `current` flag.",
|
|
1443
|
-
"DO pass `ReactNode` to `title`, `location`, `time`, and `note` \u2014 you can embed formatted text, `<Badge>`, `<
|
|
1462
|
+
"DO pass `ReactNode` to `title`, `location`, `time`, and `note` \u2014 you can embed formatted text, `<Badge>`, `<Badge>`, or `<span>` inside those fields. Use `formatDate` to pre-format timestamps before passing them as `time`.",
|
|
1444
1463
|
"DO NOT hand-roll a vertical event list with divs, icons, and connector lines \u2014 that is exactly what Timeline ships. Do not apply extra padding or wrapping outside the component; it manages its own rail and spacing internally.",
|
|
1445
1464
|
"DO NOT use Timeline for user-facing wizard progress (steps the user must complete in order) \u2014 use `Steps` for that. Timeline is read-only historical/status display; it has no interactive state, no `onClick`, and no concept of 'go to step'.",
|
|
1446
1465
|
"DO wrap Timeline in `<CardContent>` when placing it inside a `Card` \u2014 bare `Card` has no inner padding, so the rail will render flush against the card edge without `CardContent`."
|
|
@@ -1451,13 +1470,13 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1451
1470
|
"Support ticket / task history \u2014 displaying a chronological log of status transitions (Open \u2192 Assigned \u2192 In Review \u2192 Closed) with agent names in the `note` field and timestamps in `time`.",
|
|
1452
1471
|
"MF sync log viewer \u2014 listing each sync run event (OAuth refresh, fetch, upsert) with timestamps and record counts so an operator can see what the last sync did.",
|
|
1453
1472
|
"Approval workflow status panel \u2014 showing a multi-stage approval chain where completed stages have CheckCircle2 icons and the pending stage has the Plane (in-flight) icon.",
|
|
1454
|
-
"Order / purchase-order lifecycle in an admin detail page \u2014 placed alongside a `
|
|
1473
|
+
"Order / purchase-order lifecycle in an admin detail page \u2014 placed alongside a `Descriptions` summary at the top of a `Card` to give a compact at-a-glance history."
|
|
1455
1474
|
],
|
|
1456
1475
|
related: [
|
|
1457
1476
|
"Steps \u2014 use Steps (navigation group) when the user must actively progress through a wizard (interactive, shows step numbers/status, horizontal layout by default); use Timeline for read-only historical event sequences that have already happened.",
|
|
1458
|
-
"
|
|
1477
|
+
"Descriptions \u2014 use Descriptions to display a flat set of label/value metadata fields (e.g., invoice header); use Timeline when events are ordered chronologically and a connector rail communicates sequence and progress.",
|
|
1459
1478
|
"DataTable \u2014 use DataTable for multi-row, multi-column tabular event logs where sorting, filtering, and pagination are needed; use Timeline when the sequence/rail visual is the primary communication and there are fewer than ~10 events.",
|
|
1460
|
-
"
|
|
1479
|
+
"Badge \u2014 Badge is a single-item inline indicator; Timeline sequences multiple statuses with connectors. Compose Badge inside a Timeline `title` or `note` field for richer per-event context, but do not replace Timeline with a stack of Badges."
|
|
1461
1480
|
],
|
|
1462
1481
|
example: `import { Timeline } from "@godxjp/ui/data-display";
|
|
1463
1482
|
|
|
@@ -1500,7 +1519,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1500
1519
|
related: [
|
|
1501
1520
|
"DataTable \u2014 choose DataTable for any data array that needs sorting, filtering, pagination, row selection, bulk actions, or density toggle. DataTable internally renders Table primitives, so switching up is non-breaking. Default to DataTable for all admin list pages.",
|
|
1502
1521
|
"SkeletonTable \u2014 use as a loading placeholder before a Table or DataTable mounts. Drop it in the `skeleton` slot of DataState, or render it directly while data is fetching. Do not show a Table with empty rows as a loading state.",
|
|
1503
|
-
"
|
|
1522
|
+
"Descriptions \u2014 choose Descriptions when content is label\u2192value pairs (two columns, no repeated rows of the same type). Table is better when every row shares the same typed columns.",
|
|
1504
1523
|
"DataState \u2014 when your Table's data comes from `useQuery`, wrap it in DataState to handle loading/error/empty states declaratively instead of writing conditional logic around the Table yourself."
|
|
1505
1524
|
],
|
|
1506
1525
|
example: `import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@godxjp/ui/data-display";
|
|
@@ -1545,7 +1564,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1545
1564
|
"A detail page that loads a single invoice/journal entry via `useQuery` \u2014 DataState renders the skeleton row while fetching, an error alert with retry if the API fails, and the `<InvoiceCard>` only when data is confirmed non-null.",
|
|
1546
1565
|
"A list page that shows a `DataTable` of members/partners \u2014 wrap the table in DataState so the skeleton matches the column count while loading and `EmptyState` appears when the filtered result set is empty.",
|
|
1547
1566
|
"A sidebar panel that lazily loads related transactions for the selected entity \u2014 DataState keeps the panel in skeleton state during the background fetch without any manual `isPending` branching in the parent.",
|
|
1548
|
-
"A dashboard stat card that calls a summary API \u2014 DataState handles the loading/error/empty lifecycle so `<
|
|
1567
|
+
"A dashboard stat card that calls a summary API \u2014 DataState handles the loading/error/empty lifecycle so `<StatCard>` is only rendered with fully resolved numbers, preventing NaN or undefined rendering.",
|
|
1549
1568
|
"Any page using `useQuery` where the empty state and loading state are visually different \u2014 DataState enforces the correct visual for each phase without scattered `if` statements across the component tree."
|
|
1550
1569
|
],
|
|
1551
1570
|
related: [
|
|
@@ -1705,7 +1724,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1705
1724
|
"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.",
|
|
1706
1725
|
"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.",
|
|
1707
1726
|
"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.",
|
|
1708
|
-
"DON'T wrap `Switch` in FormField \u2014 use `
|
|
1727
|
+
"DON'T wrap `Switch` in FormField \u2014 use `ChoiceField` instead, which already handles the label, hidden `<input name>` for HTML form submission, error, and helper internally.",
|
|
1709
1728
|
"DON'T use FormField for checkbox-beside-label or radio-beside-label patterns \u2014 use `ChoiceField` (single checkbox/radio with description) or `CheckboxGroup` / `RadioGroup` (multiple options), which have their own integrated labelling."
|
|
1710
1729
|
],
|
|
1711
1730
|
useCases: [
|
|
@@ -1718,14 +1737,14 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1718
1737
|
],
|
|
1719
1738
|
related: [
|
|
1720
1739
|
"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.",
|
|
1721
|
-
"
|
|
1740
|
+
"ChoiceField \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.",
|
|
1722
1741
|
"ChoiceField \u2014 pairs a single checkbox or radio with a label and optional description in a horizontal layout (control beside text). Use ChoiceField instead of FormField when the control and its label sit side-by-side rather than stacked.",
|
|
1723
1742
|
"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."
|
|
1724
1743
|
],
|
|
1725
1744
|
example: `import { FormField, Input } from "@godxjp/ui/data-entry";
|
|
1726
1745
|
|
|
1727
1746
|
<FormField id="coupon-name" label="\u30AF\u30FC\u30DD\u30F3\u540D" required error={errors.name} helper="\u6700\u592750\u6587\u5B57">
|
|
1728
|
-
<Input id="coupon-name" placeholder="\u6625\u306E\u82B1\u7C89\u75C7\u5BFE\u7B5615%OFF" value={name}
|
|
1747
|
+
<Input id="coupon-name" placeholder="\u6625\u306E\u82B1\u7C89\u75C7\u5BFE\u7B5615%OFF" value={name} onValueChange={(e) => setName(e.target.value)} />
|
|
1729
1748
|
</FormField>`,
|
|
1730
1749
|
storyPath: "data-entry/FormField.stories.tsx",
|
|
1731
1750
|
rules: [23]
|
|
@@ -1768,7 +1787,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1768
1787
|
],
|
|
1769
1788
|
example: `import { Input } from "@godxjp/ui/data-entry";
|
|
1770
1789
|
|
|
1771
|
-
<Input id="qty" type="number" placeholder="\u4F8B: 500" value={value}
|
|
1790
|
+
<Input id="qty" type="number" placeholder="\u4F8B: 500" value={value} onValueChange={(e) => setValue(e.target.value)} />`,
|
|
1772
1791
|
storyPath: "data-entry/Input.stories.tsx",
|
|
1773
1792
|
rules: []
|
|
1774
1793
|
},
|
|
@@ -1966,7 +1985,7 @@ export function StatusSelect({ value, onChange }) {
|
|
|
1966
1985
|
return (
|
|
1967
1986
|
<Select
|
|
1968
1987
|
value={value}
|
|
1969
|
-
|
|
1988
|
+
onValueChange={onChange}
|
|
1970
1989
|
options={[
|
|
1971
1990
|
{ value: "draft", label: "Draft" },
|
|
1972
1991
|
{ value: "sent", label: "Sent" },
|
|
@@ -1985,7 +2004,7 @@ export function CurrencySelect({ value, onChange }) {
|
|
|
1985
2004
|
return (
|
|
1986
2005
|
<Select
|
|
1987
2006
|
value={value}
|
|
1988
|
-
|
|
2007
|
+
onValueChange={onChange}
|
|
1989
2008
|
showSearch
|
|
1990
2009
|
options={[
|
|
1991
2010
|
{ value: "JPY", label: "Japanese Yen", group: "Asia" },
|
|
@@ -2011,7 +2030,7 @@ export function AccountSelect({ value, onChange, selectedLabel }) {
|
|
|
2011
2030
|
return (
|
|
2012
2031
|
<Select
|
|
2013
2032
|
value={value}
|
|
2014
|
-
|
|
2033
|
+
onValueChange={onChange}
|
|
2015
2034
|
loadOptions={loadOptions}
|
|
2016
2035
|
selectedLabel={selectedLabel}
|
|
2017
2036
|
placeholder="Search accounts\u2026"
|
|
@@ -2051,7 +2070,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2051
2070
|
{
|
|
2052
2071
|
name: "Switch",
|
|
2053
2072
|
group: "data-entry",
|
|
2054
|
-
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use
|
|
2073
|
+
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use ChoiceField.",
|
|
2055
2074
|
props: [
|
|
2056
2075
|
{ name: "checked", type: "boolean", description: "Controlled checked state." },
|
|
2057
2076
|
{
|
|
@@ -2069,21 +2088,21 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2069
2088
|
],
|
|
2070
2089
|
usage: [
|
|
2071
2090
|
"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.",
|
|
2072
|
-
"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
|
|
2091
|
+
"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 ChoiceField (which mirrors a hidden `0`/`1` input) for any field that must submit inside an HTML <form>.",
|
|
2073
2092
|
"DO use the `size` prop ('sm' | 'default') to control thumb size. 'sm' is appropriate in dense DataTable rows or filter bars; omit it (defaults to 'default') everywhere else.",
|
|
2074
|
-
"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 (
|
|
2075
|
-
"DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field \u2014 that is exactly what
|
|
2093
|
+
"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 (ChoiceField handles that internally).",
|
|
2094
|
+
"DON'T hand-roll a <div> + <label> wrapper with bare Switch to get a labelled field \u2014 that is exactly what ChoiceField provides, including aria-describedby, aria-invalid, error/helper text, and the hidden input. Reach for ChoiceField instead.",
|
|
2076
2095
|
"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."
|
|
2077
2096
|
],
|
|
2078
2097
|
useCases: [
|
|
2079
2098
|
"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.",
|
|
2080
|
-
"Settings panel where a React state boolean is toggled immediately via an optimistic API call \u2014 no <form> submit, so
|
|
2081
|
-
"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
|
|
2099
|
+
"Settings panel where a React state boolean is toggled immediately via an optimistic API call \u2014 no <form> submit, so ChoiceField's hidden input is unnecessary.",
|
|
2100
|
+
"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 ChoiceField).",
|
|
2082
2101
|
"Filter toolbar toggle (e.g., 'Show archived') rendered inline next to other filter controls, using size='sm' for density parity with adjacent inputs.",
|
|
2083
2102
|
"Preview/demo UI where the switch controls a local display state (dark-mode preview, feature flag preview) with no server persistence."
|
|
2084
2103
|
],
|
|
2085
2104
|
related: [
|
|
2086
|
-
"
|
|
2105
|
+
"ChoiceField \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>. ChoiceField composes Label + Switch + hidden input automatically.",
|
|
2087
2106
|
"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.",
|
|
2088
2107
|
"ChoiceField \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.",
|
|
2089
2108
|
"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."
|
|
@@ -2134,7 +2153,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2134
2153
|
],
|
|
2135
2154
|
example: `import { Textarea } from "@godxjp/ui/data-entry";
|
|
2136
2155
|
|
|
2137
|
-
<Textarea id="notes" rows={4} placeholder="\u81EA\u7531\u8A18\u8FF0" value={notes}
|
|
2156
|
+
<Textarea id="notes" rows={4} placeholder="\u81EA\u7531\u8A18\u8FF0" value={notes} onValueChange={(e) => setNotes(e.target.value)} />`,
|
|
2138
2157
|
storyPath: "data-entry/Textarea.stories.tsx",
|
|
2139
2158
|
rules: []
|
|
2140
2159
|
},
|
|
@@ -2150,13 +2169,13 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2150
2169
|
"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.",
|
|
2151
2170
|
'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.',
|
|
2152
2171
|
"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.",
|
|
2153
|
-
"DON'T: wrap Label around a control that is already labelled internally. FormField, ChoiceField,
|
|
2154
|
-
"DO: pair Label with Checkbox or Switch when NOT using the compound wrappers (ChoiceField /
|
|
2172
|
+
"DON'T: wrap Label around a control that is already labelled internally. FormField, ChoiceField, ChoiceField, and CheckboxGroup all render Label internally \u2014 adding a second Label creates a duplicate association and redundant screen-reader announcement.",
|
|
2173
|
+
"DO: pair Label with Checkbox or Switch when NOT using the compound wrappers (ChoiceField / ChoiceField). In that case generate the shared id with `React.useId()` and pass it to both `id` on the control and `htmlFor` on Label.",
|
|
2155
2174
|
"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."
|
|
2156
2175
|
],
|
|
2157
2176
|
useCases: [
|
|
2158
2177
|
"Pairing with a standalone Checkbox when ChoiceField's two-line layout is unnecessary \u2014 e.g. a single 'Remember me' option in a login form.",
|
|
2159
|
-
"Labelling a bare Switch (not
|
|
2178
|
+
"Labelling a bare Switch (not ChoiceField) in a settings row where the switch is controlled by parent state and no HTML form name attribute is needed.",
|
|
2160
2179
|
"Adding a visible label to a custom or third-party control that accepts an `id` prop but isn't wrapped by FormField or ChoiceField.",
|
|
2161
2180
|
"Labelling a Textarea in a free-text form field when FormField's helper/error slots aren't needed, keeping the markup minimal.",
|
|
2162
2181
|
"Rendering an accessible label inside a table row where a FormField's block layout would break the inline/grid structure.",
|
|
@@ -2165,7 +2184,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2165
2184
|
related: [
|
|
2166
2185
|
"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.",
|
|
2167
2186
|
"ChoiceField \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.",
|
|
2168
|
-
"
|
|
2187
|
+
"ChoiceField \u2014 use instead of a bare Switch + Label pair when the control must submit a value via an HTML form name; ChoiceField owns the Label + hidden input composition.",
|
|
2169
2188
|
"Checkbox \u2014 the most common bare-Label partner; pair with Label via shared useId() id/htmlFor when ChoiceField's layout is too heavy."
|
|
2170
2189
|
],
|
|
2171
2190
|
example: `import { Label } from "@godxjp/ui/data-entry";
|
|
@@ -2209,7 +2228,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2209
2228
|
],
|
|
2210
2229
|
related: [
|
|
2211
2230
|
"CheckboxGroup \u2014 use instead of bare Checkbox when you have a list of 2+ options from an array; it handles id generation, ChoiceField wrapping, value array management, and the `name` prop for form submission. Checkbox is for a single boolean; CheckboxGroup is for multi-select.",
|
|
2212
|
-
"Switch /
|
|
2231
|
+
"Switch / ChoiceField \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'. ChoiceField adds a hidden input for HTML form compatibility.",
|
|
2213
2232
|
"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.",
|
|
2214
2233
|
"ChoiceField \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."
|
|
2215
2234
|
],
|
|
@@ -2264,7 +2283,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
2264
2283
|
related: [
|
|
2265
2284
|
"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.",
|
|
2266
2285
|
"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.",
|
|
2267
|
-
"
|
|
2286
|
+
"ChoiceField \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.",
|
|
2268
2287
|
"ChoiceField \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."
|
|
2269
2288
|
],
|
|
2270
2289
|
example: `import { RadioGroup } from "@godxjp/ui/data-entry";
|
|
@@ -2370,7 +2389,7 @@ export function InvoiceDueDateField() {
|
|
|
2370
2389
|
id="due-date"
|
|
2371
2390
|
name="due_date"
|
|
2372
2391
|
value={dueDate}
|
|
2373
|
-
|
|
2392
|
+
onValueChange={setDueDate}
|
|
2374
2393
|
fromDate={new Date()}
|
|
2375
2394
|
placeholder="yyyy-mm-dd"
|
|
2376
2395
|
/>
|
|
@@ -2469,7 +2488,7 @@ function CreateDialog() {
|
|
|
2469
2488
|
useCases: [
|
|
2470
2489
|
"Filter/search panel: slide in from the right with filter FormFields (Select, DateRangePicker, CheckboxGroup) that affect a DataTable \u2014 preferred over a Dialog because filters do not require confirmation and benefit from seeing the table behind the overlay.",
|
|
2471
2490
|
"Quick-edit drawer: open an entity's editable fields (e.g. invoice line items, account settings) without navigating away, with Save/Cancel in SheetFooter \u2014 use side='right' and keep the main page visible as context.",
|
|
2472
|
-
"Detail peek panel: show read-only
|
|
2491
|
+
"Detail peek panel: show read-only Descriptions / Timeline of a selected record (e.g. a journal entry or invoice) from a DataTable row click, using side='right' with showCloseButton={true}.",
|
|
2473
2492
|
"Mobile-first navigation drawer: side='left' sheet acting as a slide-in nav menu on small viewports when the AppShell Sidebar is hidden \u2014 triggered by a hamburger Button.",
|
|
2474
2493
|
"Step-by-step wizard side panel: multi-step form (Steps component inside SheetContent) for onboarding or import flows where full-page navigation would lose list context."
|
|
2475
2494
|
],
|
|
@@ -2515,7 +2534,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2515
2534
|
}
|
|
2516
2535
|
],
|
|
2517
2536
|
usage: [
|
|
2518
|
-
'DO compose with sub-parts in order: wrap text content in `<Alert.Content>` (or bare `<AlertContent>`), then `<Alert.Title>` + `<Alert.Description>` inside it, then `<Alert.Actions>` for any retry/CTA buttons. Example: `<Alert
|
|
2537
|
+
'DO compose with sub-parts in order: wrap text content in `<Alert.Content>` (or bare `<AlertContent>`), then `<Alert.Title>` + `<Alert.Description>` inside it, then `<Alert.Actions>` for any retry/CTA buttons. Example: `<Alert tone="destructive"><Alert.Content><Alert.Title>Error</Alert.Title><Alert.Description>{msg}</Alert.Description></Alert.Content><Alert.Actions><Button \u2026/></Alert.Actions></Alert>`.',
|
|
2519
2538
|
"DO use `Alert.QueryError` (alias `AlertQueryError`) for TanStack Query / API failure surfaces \u2014 it already renders humanError(error), an i18n title, and an optional Retry button. Never hand-roll that pattern.",
|
|
2520
2539
|
'DON\'T pass raw action elements directly as top-level children of `<Alert>` without wrapping them in `<Alert.Actions>` \u2014 the layout slot only activates correctly via the `data-slot="alert-actions"` wrapper.',
|
|
2521
2540
|
'DON\'T hand-roll a dismiss \u2715 button \u2014 pass `onDismiss` to `<Alert>` and the component renders its own accessible dismiss button with `aria-label="Dismiss"`. The `onDismiss` handler may return a Promise.',
|
|
@@ -2523,12 +2542,12 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2523
2542
|
"DO NOT use `Alert` for transient ephemeral feedback (e.g. 'saved successfully'). Use `toast()` from sonner + `<Toaster>` for that. `Alert` is for persistent, page-scoped banners that stay visible until the user acts or dismisses."
|
|
2524
2543
|
],
|
|
2525
2544
|
useCases: [
|
|
2526
|
-
'Page-level error banner after a form submission fails server-side validation \u2014 `
|
|
2545
|
+
'Page-level error banner after a form submission fails server-side validation \u2014 `tone="destructive"` with `Alert.Title` summarising the error and `Alert.Description` listing field issues, paired with `onDismiss` so the user can clear it.',
|
|
2527
2546
|
"Inline warning at the top of an accounting invoice list when the OAuth token for the MF sync is about to expire \u2014 `variant=\"warning\"` with an `Alert.Actions` containing a 'Reconnect' Button.",
|
|
2528
|
-
'Success confirmation banner rendered after a bulk-import job completes and the user returns to the list page \u2014 `
|
|
2547
|
+
'Success confirmation banner rendered after a bulk-import job completes and the user returns to the list page \u2014 `tone="success"` with `Alert.Description` showing the record count imported.',
|
|
2529
2548
|
"TanStack Query data-fetch failure inside a Card body \u2014 use `<Alert.QueryError error={error} onRetry={refetch} />` instead of writing a custom error state.",
|
|
2530
2549
|
"Informational notice at the top of a settings page when a feature is in beta or requires a plan upgrade \u2014 `variant=\"default\"` (Info icon) with a short description and an `Alert.Actions` 'Learn more' link.",
|
|
2531
|
-
'Dismissible billing-overdue notice at the top of the dashboard \u2014 `
|
|
2550
|
+
'Dismissible billing-overdue notice at the top of the dashboard \u2014 `tone="destructive"` with `onDismiss` that sets a session flag so it does not reappear until the next login.'
|
|
2532
2551
|
],
|
|
2533
2552
|
related: [
|
|
2534
2553
|
"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.",
|
|
@@ -2538,7 +2557,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2538
2557
|
],
|
|
2539
2558
|
example: `import { Alert, AlertTitle, AlertDescription } from "@godxjp/ui/feedback";
|
|
2540
2559
|
|
|
2541
|
-
<Alert
|
|
2560
|
+
<Alert tone="warning">
|
|
2542
2561
|
<AlertTitle>3 \u4EF6\u306E\u6253\u523B\u6F0F\u308C\u304C\u3042\u308A\u307E\u3059</AlertTitle>
|
|
2543
2562
|
<AlertDescription>\u672C\u65E5\u4E2D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002</AlertDescription>
|
|
2544
2563
|
</Alert>`,
|
|
@@ -2577,7 +2596,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2577
2596
|
related: [
|
|
2578
2597
|
"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.",
|
|
2579
2598
|
"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.",
|
|
2580
|
-
"SkeletonCard \u2014 sibling skeleton shaped like a
|
|
2599
|
+
"SkeletonCard \u2014 sibling skeleton shaped like a StatCard tile; use inside a ResponsiveGrid to placeholder KPI dashboard cards, not tabular data.",
|
|
2581
2600
|
"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."
|
|
2582
2601
|
],
|
|
2583
2602
|
example: `import { SkeletonTable } from "@godxjp/ui/feedback";
|
|
@@ -2589,12 +2608,12 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2589
2608
|
{
|
|
2590
2609
|
name: "SkeletonCard",
|
|
2591
2610
|
group: "feedback",
|
|
2592
|
-
tagline: "Loading placeholder shaped like a
|
|
2611
|
+
tagline: "Loading placeholder shaped like a StatCard tile. Use inside a ResponsiveGrid while KPIs load.",
|
|
2593
2612
|
props: [],
|
|
2594
2613
|
usage: [
|
|
2595
|
-
"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
|
|
2596
|
-
"DO render one SkeletonCard per expected
|
|
2597
|
-
"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
|
|
2614
|
+
"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).",
|
|
2615
|
+
"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.",
|
|
2616
|
+
"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.",
|
|
2598
2617
|
"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.",
|
|
2599
2618
|
`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.`,
|
|
2600
2619
|
"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."
|
|
@@ -2602,12 +2621,12 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2602
2621
|
useCases: [
|
|
2603
2622
|
"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.",
|
|
2604
2623
|
"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.",
|
|
2605
|
-
"Per-entity KPI switcher: when the user switches the active legal entity and a new round of stats is re-fetched, swap the
|
|
2606
|
-
"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
|
|
2624
|
+
"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.",
|
|
2625
|
+
"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.",
|
|
2607
2626
|
"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."
|
|
2608
2627
|
],
|
|
2609
2628
|
related: [
|
|
2610
|
-
"
|
|
2629
|
+
"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.",
|
|
2611
2630
|
"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.",
|
|
2612
2631
|
"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.",
|
|
2613
2632
|
"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."
|
|
@@ -2667,8 +2686,13 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
2667
2686
|
{
|
|
2668
2687
|
name: "Tabs",
|
|
2669
2688
|
group: "navigation",
|
|
2670
|
-
tagline: "Radix tab container.
|
|
2689
|
+
tagline: "Radix tab container with optional Ant-style `items` API. Pass items for the common full TabsList/TabsContent set, or compose TabsList/TabsTrigger/TabsContent manually when you need per-panel control.",
|
|
2671
2690
|
props: [
|
|
2691
|
+
{
|
|
2692
|
+
name: "items",
|
|
2693
|
+
type: "{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean }[]",
|
|
2694
|
+
description: "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels."
|
|
2695
|
+
},
|
|
2672
2696
|
{ name: "value", type: "string", description: "Controlled active tab key." },
|
|
2673
2697
|
{ name: "defaultValue", type: "string", description: "Uncontrolled initial tab key." },
|
|
2674
2698
|
{
|
|
@@ -2678,36 +2702,34 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
2678
2702
|
}
|
|
2679
2703
|
],
|
|
2680
2704
|
usage: [
|
|
2681
|
-
|
|
2705
|
+
"DO pass `items` when all tab content is known up front \u2014 each item needs a unique `value`, trigger `label`, and panel `content`.",
|
|
2706
|
+
'When not using `items`, compose the full four-part tree \u2014 `<Tabs>` root, `<TabsList>` trigger bar, one `<TabsTrigger value="\u2026">` per tab, one `<TabsContent value="\u2026">` per matching trigger.',
|
|
2682
2707
|
"DO: use `defaultValue` (uncontrolled) for simple local state; use `value` + `onValueChange` together (controlled) when the active tab is driven by URL query params, router state, or parent state. NEVER set both simultaneously.",
|
|
2683
|
-
"DO
|
|
2708
|
+
"DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",
|
|
2684
2709
|
'DO: pass `orientation="vertical"` to `<Tabs>` (not to `TabsList`) for a side-rail layout \u2014 the CSS group classes on root and triggers respond automatically, so no extra className gymnastics are needed.',
|
|
2685
|
-
"DON'T: hand-roll the active-indicator underline or selected-state ring \u2014 `TabsTrigger` already applies `data-[state=active]` styles including the `after:` line element for the `line` variant. Adding your own underline breaks the design."
|
|
2686
|
-
"DON'T: reach for `Tabs` primitives when all content is known up-front and you don't need per-panel `forceMount` or custom `TabsContent` attributes \u2014 use `TabsItems` instead (items-array API, handles all composition internally and is less verbose)."
|
|
2710
|
+
"DON'T: hand-roll the active-indicator underline or selected-state ring \u2014 `TabsTrigger` already applies `data-[state=active]` styles including the `after:` line element for the `line` variant. Adding your own underline breaks the design."
|
|
2687
2711
|
],
|
|
2688
2712
|
useCases: [
|
|
2689
|
-
"Detail drawers or pages that need full per-panel control \u2014 e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `
|
|
2713
|
+
"Detail drawers or pages that need full per-panel control \u2014 e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `Tabs` cannot pass.",
|
|
2690
2714
|
"Controlled tabs driven by URL search params (e.g. `?tab=history`) where the parent reads/writes the active key and passes it to `value` / `onValueChange`.",
|
|
2691
2715
|
'Vertical side-rail navigation inside a `SplitPane` or settings layout where `orientation="vertical"` on the root and `variant="line"` on `TabsList` combine to produce a sidebar-style tab strip.',
|
|
2692
2716
|
"Lightweight widget tabs on a dashboard card \u2014 e.g. switching a `DataTable` between 'Pending' and 'Paid' invoice views \u2014 where an uncontrolled `defaultValue` is sufficient and no URL state is needed.",
|
|
2693
2717
|
"Admin entity profile pages (company, partner, employee) where each `TabsContent` wraps an Inertia deferred prop panel, lazy-loading expensive data only when the tab is first activated."
|
|
2694
2718
|
],
|
|
2695
2719
|
related: [
|
|
2696
|
-
"TabsItems (@godxjp/ui/navigation) \u2014 higher-level wrapper that accepts a flat `items` array and composes TabsList/TabsTrigger/TabsContent internally. Prefer TabsItems for the common case where all content is known up-front and you do not need per-panel `forceMount` or custom TabsContent attributes; drop down to raw Tabs primitives only when you need that extra control.",
|
|
2697
2720
|
"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.",
|
|
2698
2721
|
"FilterBar / FilterGroup (@godxjp/ui/navigation) \u2014 horizontal filter chip row. Visually resembles `line`-variant tabs but is semantically different: FilterBar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.",
|
|
2699
|
-
"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
|
|
2700
|
-
],
|
|
2701
|
-
example: `import { Tabs
|
|
2702
|
-
|
|
2703
|
-
<Tabs
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
</Tabs>`,
|
|
2722
|
+
"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."
|
|
2723
|
+
],
|
|
2724
|
+
example: `import { Tabs } from "@godxjp/ui/navigation";
|
|
2725
|
+
|
|
2726
|
+
<Tabs
|
|
2727
|
+
defaultValue="overview"
|
|
2728
|
+
items={[
|
|
2729
|
+
{ value: "overview", label: "\u6982\u8981", content: "\u6982\u8981\u30B3\u30F3\u30C6\u30F3\u30C4" },
|
|
2730
|
+
{ value: "history", label: "\u5C65\u6B74", content: "\u5C65\u6B74\u30B3\u30F3\u30C6\u30F3\u30C4" },
|
|
2731
|
+
]}
|
|
2732
|
+
/>`,
|
|
2711
2733
|
storyPath: "navigation/Tabs.stories.tsx",
|
|
2712
2734
|
rules: []
|
|
2713
2735
|
},
|
|
@@ -2735,7 +2757,7 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
2735
2757
|
"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.",
|
|
2736
2758
|
"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'}.",
|
|
2737
2759
|
"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.",
|
|
2738
|
-
"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 /
|
|
2760
|
+
"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."
|
|
2739
2761
|
],
|
|
2740
2762
|
useCases: [
|
|
2741
2763
|
"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.",
|
|
@@ -2748,7 +2770,7 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
2748
2770
|
related: [
|
|
2749
2771
|
"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.",
|
|
2750
2772
|
"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.",
|
|
2751
|
-
"Tabs /
|
|
2773
|
+
"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.",
|
|
2752
2774
|
"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."
|
|
2753
2775
|
],
|
|
2754
2776
|
example: `import { FilterBar, FilterGroup } from "@godxjp/ui/navigation";
|
|
@@ -2856,7 +2878,7 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
2856
2878
|
],
|
|
2857
2879
|
example: `import { Pagination } from "@godxjp/ui/navigation";
|
|
2858
2880
|
|
|
2859
|
-
<Pagination current={page} total={filtered.length} pageSize={10} showTotal
|
|
2881
|
+
<Pagination current={page} total={filtered.length} pageSize={10} showTotal onValueChange={(p) => setPage(p)} />`,
|
|
2860
2882
|
storyPath: "navigation/Pagination.stories.tsx",
|
|
2861
2883
|
rules: [40]
|
|
2862
2884
|
},
|
|
@@ -2885,14 +2907,14 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
2885
2907
|
"Topbar / avatar chip: a user-avatar Button triggers a DropdownMenu with Profile, Settings, DropdownMenuSeparator, Sign out \u2014 standard app-shell pattern for account actions.",
|
|
2886
2908
|
"Bulk-action toolbar: after selecting rows, an 'Actions' Button opens a DropdownMenu with Approve, Reject, Export \u2014 prevents the toolbar from overflowing with individual buttons.",
|
|
2887
2909
|
"Column visibility toggle in a report table: a 'Columns' Button opens a DropdownMenu whose items are DropdownMenuCheckboxItem entries, letting users show/hide columns without a Dialog.",
|
|
2888
|
-
"Quick status change on an accounting entry: a
|
|
2910
|
+
"Quick status change on an accounting entry: a Badge-like trigger opens a DropdownMenu with DropdownMenuRadioGroup items (Draft, Posted, Voided) so the user can transition status without navigating away.",
|
|
2889
2911
|
"Context menu for a sidebar nav item: right-click or kebab on a project entry opens a DropdownMenu with Rename, Duplicate, Archive actions scoped to that item."
|
|
2890
2912
|
],
|
|
2891
2913
|
related: [
|
|
2892
2914
|
"Popover \u2014 use Popover when the floating panel needs arbitrary layout (filter forms, date pickers, rich content grids). Use DropdownMenu only for a list of discrete clickable actions or toggle items; DropdownMenu has no layout flexibility beyond label/separator/group.",
|
|
2893
2915
|
"Command \u2014 use Command (cmdk) when the list is large, needs fuzzy-search filtering, or acts as a keyboard-driven command palette. DropdownMenu has no built-in search input; once the list exceeds ~8 items or needs filtering, switch to Command (often inside a Popover).",
|
|
2894
2916
|
"Select \u2014 use Select when the purpose is choosing a value to submit in a form field (has a name prop for native form submission, renders a hidden select for a11y). Use DropdownMenu when the purpose is triggering actions, not picking a form value.",
|
|
2895
|
-
"
|
|
2917
|
+
"Sidebar \u2014 use Sidebar for persistent left-rail navigation. DropdownMenu is transient (opens on click, dismisses on select); Sidebar is always-visible structural navigation."
|
|
2896
2918
|
],
|
|
2897
2919
|
example: `import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@godxjp/ui/navigation";
|
|
2898
2920
|
import { Button } from "@godxjp/ui/general";
|
|
@@ -2902,7 +2924,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2902
2924
|
<DropdownMenuContent>
|
|
2903
2925
|
<DropdownMenuItem>\u7DE8\u96C6</DropdownMenuItem>
|
|
2904
2926
|
<DropdownMenuSeparator />
|
|
2905
|
-
<DropdownMenuItem
|
|
2927
|
+
<DropdownMenuItem tone="destructive">\u524A\u9664</DropdownMenuItem>
|
|
2906
2928
|
</DropdownMenuContent>
|
|
2907
2929
|
</DropdownMenu>`,
|
|
2908
2930
|
storyPath: "navigation/DropdownMenu.stories.tsx",
|
|
@@ -2935,7 +2957,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2935
2957
|
"DO: Pass all steps via the `items` array (each `{ title, subTitle?, description?/content?, icon?, status?, disabled? }`) \u2014 Steps is a single-component API with no child sub-components to compose manually.",
|
|
2936
2958
|
"DO: Control the active step with `current` (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`.",
|
|
2937
2959
|
"DO: Use per-item `status` to pin individual steps independently of `current` (e.g. a skipped or already-errored step). Per-item `status` takes precedence over the derived status from `current`.",
|
|
2938
|
-
"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`/`
|
|
2960
|
+
"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.",
|
|
2939
2961
|
"DON'T: Wire `onChange` unless you actually support non-linear navigation. `onChange` 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 `onChange`, or the prop is meaningless.",
|
|
2940
2962
|
"A11y: The `<ol>` is given `aria-label='Progress'` automatically. Individual steps render as `<button type='button'>` when `onChange` is present \u2014 ensure each `item.title` is descriptive enough to serve as the button label; avoid icon-only steps without a visible title."
|
|
2941
2963
|
],
|
|
@@ -2948,8 +2970,8 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2948
2970
|
],
|
|
2949
2971
|
related: [
|
|
2950
2972
|
"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.",
|
|
2951
|
-
"Tabs /
|
|
2952
|
-
"
|
|
2973
|
+
"Tabs / Tabs \u2014 use Tabs when each section has its own rendered panel and users switch freely between them; use Steps when stages are ordered and the indicator communicates completion state rather than just selection.",
|
|
2974
|
+
"Progress \u2014 use Progress for a single continuous percentage (file upload, quota fill); use Steps for discrete named stages with individual pass/fail status.",
|
|
2953
2975
|
"Breadcrumb \u2014 use Breadcrumb for hierarchical location within a page tree; use Steps for sequential workflow progress where order and completion matter."
|
|
2954
2976
|
],
|
|
2955
2977
|
example: `import { Steps } from "@godxjp/ui/navigation";
|
|
@@ -3009,7 +3031,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
3009
3031
|
"LocalePicker \u2014 the language-selector control that reads/writes AppProvider locale context automatically when used as a zero-prop child. Prefer LocalePicker over calling setLocale from useAppContext() directly in UI.",
|
|
3010
3032
|
"TimezonePicker \u2014 the timezone-selector control; inherits `timezoneOptions` from AppProvider context when its own `options` prop is omitted. Both pickers require AppProvider to be in the tree unless controlled props are passed.",
|
|
3011
3033
|
"formatDate \u2014 the MANDATORY date/time formatter that reads locale, timezone, timeFormat, and dateFormat from AppProvider context. Do NOT call date-fns or Intl.DateTimeFormat directly; formatDate is the single source of truth for display.",
|
|
3012
|
-
"
|
|
3034
|
+
"AppShell \u2014 the top-level application shell that composes AppProvider, AppShell, Sidebar, and Topbar into a single ready-to-use layout. If your project uses AppShell, AppProvider is already mounted inside it \u2014 do not add a second one."
|
|
3013
3035
|
],
|
|
3014
3036
|
example: `import { AppProvider } from "@godxjp/ui/app";
|
|
3015
3037
|
|
|
@@ -3047,7 +3069,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
3047
3069
|
],
|
|
3048
3070
|
useCases: [
|
|
3049
3071
|
"Rendering all date/time columns in a DataTable \u2014 invoice due dates (`kind: 'date'`), transaction timestamps (`kind: 'datetime'`), and elapsed time since last sync (`kind: 'relative'`) all go through `formatDate` so the locale/timezone/12h-24h setting from AppProvider is respected everywhere.",
|
|
3050
|
-
"Displaying a single date/time field in a
|
|
3072
|
+
"Displaying a single date/time field in a Descriptions or Card detail panel, e.g. `formatDate(invoice.issuedAt)` for the issued-at row \u2014 no extra formatting logic needed, null is handled as `'\u2014'` automatically.",
|
|
3051
3073
|
"Formatting a stored `HH:mm` string (24h canonical storage) for display according to the user's timeFormat preference \u2014 pass the raw `'14:30'` string and auto-detection routes it through `formatTimeOfDay`, outputting `'2:30 PM'` or `'14:30'` based on context.",
|
|
3052
3074
|
"Rendering a 'last modified' timestamp with relative wording in an activity feed or audit log row \u2014 `formatDate(entry.updatedAt, { kind: 'relative' })` produces locale-correct relative strings like `'3\u65E5\u524D'` / `'3 days ago'`.",
|
|
3053
3075
|
"Converting a `Date` selected from `DatePicker` (react-day-picker) back to a display string \u2014 pass `{ calendar: true }` to avoid the UTC midnight shift that the default instant path would apply."
|
|
@@ -3157,7 +3179,7 @@ export function ShiftStartField() {
|
|
|
3157
3179
|
id="shift-start"
|
|
3158
3180
|
name="shift_start"
|
|
3159
3181
|
value={startTime}
|
|
3160
|
-
|
|
3182
|
+
onValueChange={setStartTime}
|
|
3161
3183
|
minuteStep={15}
|
|
3162
3184
|
className="w-36"
|
|
3163
3185
|
/>
|
|
@@ -3277,7 +3299,7 @@ export function InvoicePeriodFilter() {
|
|
|
3277
3299
|
id="invoice-period"
|
|
3278
3300
|
name="period"
|
|
3279
3301
|
value={range}
|
|
3280
|
-
|
|
3302
|
+
onValueChange={setRange}
|
|
3281
3303
|
fromDate={new Date(2020, 0, 1)}
|
|
3282
3304
|
toDate={new Date(2030, 11, 31)}
|
|
3283
3305
|
/>
|
|
@@ -3288,124 +3310,6 @@ export function InvoicePeriodFilter() {
|
|
|
3288
3310
|
storyPath: "data-entry/DateRangePicker.stories.tsx",
|
|
3289
3311
|
rules: [3, 6, 23, 31]
|
|
3290
3312
|
},
|
|
3291
|
-
{
|
|
3292
|
-
name: "SwitchField",
|
|
3293
|
-
group: "data-entry",
|
|
3294
|
-
tagline: "Labelled boolean switch with a hidden 0/1 input for HTML form submission \u2014 use this instead of bare Switch whenever you need a label or a form name.",
|
|
3295
|
-
props: [
|
|
3296
|
-
{
|
|
3297
|
-
name: "label",
|
|
3298
|
-
type: "string",
|
|
3299
|
-
required: true,
|
|
3300
|
-
description: "Visible text label rendered to the right of the switch toggle."
|
|
3301
|
-
},
|
|
3302
|
-
{
|
|
3303
|
-
name: "name",
|
|
3304
|
-
type: "string",
|
|
3305
|
-
required: true,
|
|
3306
|
-
description: "HTML name attribute used by the hidden input \u2014 the value submitted is '1' (on) or '0' (off)."
|
|
3307
|
-
},
|
|
3308
|
-
{
|
|
3309
|
-
name: "id",
|
|
3310
|
-
type: "string",
|
|
3311
|
-
description: "ID wired to the Switch and Label htmlFor. Auto-generated via useId() when omitted."
|
|
3312
|
-
},
|
|
3313
|
-
{
|
|
3314
|
-
name: "checked",
|
|
3315
|
-
type: "boolean",
|
|
3316
|
-
description: "Controlled checked state. When provided the component is fully controlled; onCheckedChange must also be supplied."
|
|
3317
|
-
},
|
|
3318
|
-
{
|
|
3319
|
-
name: "defaultChecked",
|
|
3320
|
-
type: "boolean",
|
|
3321
|
-
defaultValue: "false",
|
|
3322
|
-
description: "Uncontrolled initial checked state. Ignored when checked is provided."
|
|
3323
|
-
},
|
|
3324
|
-
{
|
|
3325
|
-
name: "onCheckedChange",
|
|
3326
|
-
type: "(checked: boolean) => void",
|
|
3327
|
-
description: "Fires with the new boolean value whenever the switch is toggled."
|
|
3328
|
-
},
|
|
3329
|
-
{
|
|
3330
|
-
name: "required",
|
|
3331
|
-
type: "boolean",
|
|
3332
|
-
description: "Marks the field as required \u2014 renders a red asterisk next to the label and sets aria-required on the switch."
|
|
3333
|
-
},
|
|
3334
|
-
{
|
|
3335
|
-
name: "helper",
|
|
3336
|
-
type: "string",
|
|
3337
|
-
description: "Secondary hint text shown below the label. Hidden when error is set."
|
|
3338
|
-
},
|
|
3339
|
-
{
|
|
3340
|
-
name: "error",
|
|
3341
|
-
type: "string",
|
|
3342
|
-
description: "Validation error message. Renders below the row as a role=alert paragraph and sets aria-invalid on the switch."
|
|
3343
|
-
},
|
|
3344
|
-
{
|
|
3345
|
-
name: "labelAddon",
|
|
3346
|
-
type: "React.ReactNode",
|
|
3347
|
-
description: "Optional node rendered inline after the label text (e.g. a Badge or tooltip trigger)."
|
|
3348
|
-
},
|
|
3349
|
-
{ name: "disabled", type: "boolean", description: "Disables the switch toggle." },
|
|
3350
|
-
{
|
|
3351
|
-
name: "size",
|
|
3352
|
-
type: '"sm" | "default"',
|
|
3353
|
-
description: "Switch toggle size. Forwarded to the inner Switch primitive."
|
|
3354
|
-
},
|
|
3355
|
-
{
|
|
3356
|
-
name: "className",
|
|
3357
|
-
type: "string",
|
|
3358
|
-
description: "Extra class names applied to the outer wrapper div."
|
|
3359
|
-
}
|
|
3360
|
-
],
|
|
3361
|
-
usage: [
|
|
3362
|
-
"DO use SwitchField (not bare Switch) whenever a form name is required \u2014 it automatically mirrors a hidden input with value '1'/'0' so native HTML form.submit() and FormData work without extra wiring.",
|
|
3363
|
-
"DO NOT use SwitchField without the name prop if you are not submitting via HTML form \u2014 pass name anyway; it is required by the type signature and is a safe no-op when FormData is not read.",
|
|
3364
|
-
"Controlled mode: supply both checked and onCheckedChange. Uncontrolled mode: use defaultChecked only. Never mix \u2014 passing checked without onCheckedChange leaves the switch frozen.",
|
|
3365
|
-
"The error prop replaces the helper text \u2014 both cannot appear simultaneously. Show validation errors via error, not as helper text that is always visible.",
|
|
3366
|
-
"labelAddon is rendered inline after the label text at the same font size \u2014 use it for a small Badge ('Beta'), InfoTooltip, or similar inline decoration, NOT for a full action button.",
|
|
3367
|
-
"NEVER hand-roll a Switch + Label + hidden-input pattern yourself; SwitchField already composes Switch, Label, hidden input, aria-describedby, aria-required, aria-invalid, and role=alert in a single component."
|
|
3368
|
-
],
|
|
3369
|
-
useCases: [
|
|
3370
|
-
"Settings toggles in an admin form (e.g. 'Enable auto-invoice', 'Allow concurrent sessions') where the page POSTs via Inertia useForm \u2014 the hidden 0/1 input is picked up by FormData automatically.",
|
|
3371
|
-
"Permissions or feature-flag checkboxes on a user/role edit page where the UI needs a helper hint ('Allows access to billing module') alongside the toggle.",
|
|
3372
|
-
"Inline required toggles in a multi-step wizard step where validation errors need to surface below the row via the error prop.",
|
|
3373
|
-
"Accounting app: 'Mark as reconciled', 'Exclude from report', 'Apply tax-exempt status' \u2014 boolean flags that must be submitted with the record form.",
|
|
3374
|
-
"Row-level status toggles in a card layout (e.g. 'Active' on a payment method card) using the sm size to keep the toggle compact.",
|
|
3375
|
-
"Any boolean setting that needs a visible label + accessible association (htmlFor / aria) without writing the Switch + Label wiring manually."
|
|
3376
|
-
],
|
|
3377
|
-
related: [
|
|
3378
|
-
"Switch \u2014 bare Radix toggle with no label, no hidden input, and no error/helper. Use Switch when you are managing the label yourself (e.g. inside a custom flex row) and do not need HTML form submission. Use SwitchField for any form field that needs a label, helper, error, or native form submission.",
|
|
3379
|
-
"Checkbox / CheckboxField \u2014 semantically for multi-select or tri-state (indeterminate). Use Checkbox for 'agree to terms' or multi-option selection. Use SwitchField for a single on/off boolean setting where the switch affordance fits better than a checkbox.",
|
|
3380
|
-
"FormField \u2014 generic field wrapper used by Input, Select, etc. SwitchField already bundles its own label/error layout; do NOT also wrap it in FormField."
|
|
3381
|
-
],
|
|
3382
|
-
example: `import { SwitchField } from "@godxjp/ui/data-entry";
|
|
3383
|
-
import { useState } from "react";
|
|
3384
|
-
|
|
3385
|
-
// Uncontrolled \u2014 defaultChecked, value submitted as hidden 0/1
|
|
3386
|
-
<SwitchField
|
|
3387
|
-
name="auto_invoice"
|
|
3388
|
-
label="\u81EA\u52D5\u8ACB\u6C42\u3092\u6709\u52B9\u306B\u3059\u308B"
|
|
3389
|
-
helper="\u6709\u52B9\u306B\u3059\u308B\u3068\u6708\u672B\u306B\u81EA\u52D5\u3067\u8ACB\u6C42\u66F8\u304C\u767A\u884C\u3055\u308C\u307E\u3059"
|
|
3390
|
-
defaultChecked={false}
|
|
3391
|
-
/>
|
|
3392
|
-
|
|
3393
|
-
// Controlled with validation error
|
|
3394
|
-
function ReconcileToggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) {
|
|
3395
|
-
return (
|
|
3396
|
-
<SwitchField
|
|
3397
|
-
name="reconciled"
|
|
3398
|
-
label="\u7167\u5408\u6E08\u307F"
|
|
3399
|
-
checked={value}
|
|
3400
|
-
onCheckedChange={onChange}
|
|
3401
|
-
required
|
|
3402
|
-
error={!value ? "\u7167\u5408\u3092\u5B8C\u4E86\u3057\u3066\u304F\u3060\u3055\u3044" : undefined}
|
|
3403
|
-
/>
|
|
3404
|
-
);
|
|
3405
|
-
}`,
|
|
3406
|
-
storyPath: "data-entry/SwitchField.stories.tsx",
|
|
3407
|
-
rules: [3, 6, 13, 23]
|
|
3408
|
-
},
|
|
3409
3313
|
{
|
|
3410
3314
|
name: "Cascader",
|
|
3411
3315
|
group: "data-entry",
|
|
@@ -3516,11 +3420,11 @@ const REGIONS = [
|
|
|
3516
3420
|
{
|
|
3517
3421
|
value: "jp",
|
|
3518
3422
|
label: "\u65E5\u672C",
|
|
3519
|
-
|
|
3423
|
+
content: [
|
|
3520
3424
|
{
|
|
3521
3425
|
value: "tokyo",
|
|
3522
3426
|
label: "\u6771\u4EAC\u90FD",
|
|
3523
|
-
|
|
3427
|
+
content: [
|
|
3524
3428
|
{ value: "shinjuku", label: "\u65B0\u5BBF\u533A" },
|
|
3525
3429
|
{ value: "shibuya", label: "\u6E0B\u8C37\u533A" },
|
|
3526
3430
|
],
|
|
@@ -3530,11 +3434,11 @@ const REGIONS = [
|
|
|
3530
3434
|
{
|
|
3531
3435
|
value: "vn",
|
|
3532
3436
|
label: "Vi\u1EC7t Nam",
|
|
3533
|
-
|
|
3437
|
+
content: [
|
|
3534
3438
|
{
|
|
3535
3439
|
value: "hcm",
|
|
3536
3440
|
label: "TP. H\u1ED3 Ch\xED Minh",
|
|
3537
|
-
|
|
3441
|
+
content: [
|
|
3538
3442
|
{ value: "q1", label: "Qu\u1EADn 1" },
|
|
3539
3443
|
{ value: "q3", label: "Qu\u1EADn 3" },
|
|
3540
3444
|
],
|
|
@@ -3551,7 +3455,7 @@ function RegionPicker() {
|
|
|
3551
3455
|
<Cascader
|
|
3552
3456
|
options={REGIONS}
|
|
3553
3457
|
value={path}
|
|
3554
|
-
|
|
3458
|
+
onValueChange={(v) => setPath(v as string[])}
|
|
3555
3459
|
showSearch
|
|
3556
3460
|
placeholder="Select region\u2026"
|
|
3557
3461
|
/>
|
|
@@ -3567,7 +3471,7 @@ function MultiRegionPicker() {
|
|
|
3567
3471
|
options={REGIONS}
|
|
3568
3472
|
multiple
|
|
3569
3473
|
value={paths}
|
|
3570
|
-
|
|
3474
|
+
onValueChange={(v) => setPaths(v as string[][])}
|
|
3571
3475
|
showSearch
|
|
3572
3476
|
/>
|
|
3573
3477
|
);
|
|
@@ -3576,7 +3480,7 @@ function MultiRegionPicker() {
|
|
|
3576
3480
|
// With custom field names (data uses 'name'/'id'/'nodes')
|
|
3577
3481
|
<Cascader
|
|
3578
3482
|
options={rawApiData}
|
|
3579
|
-
fieldNames={{ label: "name", value: "id",
|
|
3483
|
+
fieldNames={{ label: "name", value: "id", content: "nodes" }}
|
|
3580
3484
|
defaultValue={["dept-1", "team-3"]}
|
|
3581
3485
|
/>
|
|
3582
3486
|
|
|
@@ -3584,7 +3488,7 @@ function MultiRegionPicker() {
|
|
|
3584
3488
|
<Cascader
|
|
3585
3489
|
options={REGIONS}
|
|
3586
3490
|
changeOnSelect
|
|
3587
|
-
|
|
3491
|
+
onValueChange={(v) => console.log("path", v)}
|
|
3588
3492
|
/>
|
|
3589
3493
|
\`}`,
|
|
3590
3494
|
storyPath: "data-entry/Cascader.stories.tsx",
|
|
@@ -3682,7 +3586,7 @@ function MultiRegionPicker() {
|
|
|
3682
3586
|
{
|
|
3683
3587
|
name: "fieldNames",
|
|
3684
3588
|
type: "{ label?: string; value?: string; children?: string }",
|
|
3685
|
-
description: "Remap data object keys. Example: `{ label: 'name', value: 'id',
|
|
3589
|
+
description: "Remap data object keys. Example: `{ label: 'name', value: 'id', content: 'items' }` so you don't have to transform your API response before passing it to `treeData`."
|
|
3686
3590
|
}
|
|
3687
3591
|
],
|
|
3688
3592
|
usage: [
|
|
@@ -3714,13 +3618,13 @@ const accountTree = [
|
|
|
3714
3618
|
{
|
|
3715
3619
|
value: "assets",
|
|
3716
3620
|
label: "Assets",
|
|
3717
|
-
|
|
3718
|
-
{ value: "current-assets", label: "Current Assets",
|
|
3621
|
+
content: [
|
|
3622
|
+
{ value: "current-assets", label: "Current Assets", content: [
|
|
3719
3623
|
{ value: "cash", label: "Cash" },
|
|
3720
3624
|
{ value: "ar", label: "Accounts Receivable" },
|
|
3721
3625
|
],
|
|
3722
3626
|
},
|
|
3723
|
-
{ value: "fixed-assets", label: "Fixed Assets",
|
|
3627
|
+
{ value: "fixed-assets", label: "Fixed Assets", content: [
|
|
3724
3628
|
{ value: "equipment", label: "Equipment" },
|
|
3725
3629
|
],
|
|
3726
3630
|
},
|
|
@@ -3729,7 +3633,7 @@ const accountTree = [
|
|
|
3729
3633
|
{
|
|
3730
3634
|
value: "liabilities",
|
|
3731
3635
|
label: "Liabilities",
|
|
3732
|
-
|
|
3636
|
+
content: [
|
|
3733
3637
|
{ value: "ap", label: "Accounts Payable" },
|
|
3734
3638
|
],
|
|
3735
3639
|
},
|
|
@@ -3747,7 +3651,7 @@ export function AccountPicker() {
|
|
|
3747
3651
|
id="account-picker"
|
|
3748
3652
|
treeData={accountTree}
|
|
3749
3653
|
value={account}
|
|
3750
|
-
|
|
3654
|
+
onValueChange={(v) => setAccount(v as string | undefined)}
|
|
3751
3655
|
showSearch
|
|
3752
3656
|
treeDefaultExpandAll
|
|
3753
3657
|
placeholder="Select account\u2026"
|
|
@@ -3765,7 +3669,7 @@ export function DepartmentFilter() {
|
|
|
3765
3669
|
id="dept-filter"
|
|
3766
3670
|
treeData={accountTree}
|
|
3767
3671
|
value={selected}
|
|
3768
|
-
|
|
3672
|
+
onValueChange={(v) => setSelected(v as string[])}
|
|
3769
3673
|
treeCheckable
|
|
3770
3674
|
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
|
3771
3675
|
showSearch
|
|
@@ -3838,7 +3742,7 @@ export function DepartmentFilter() {
|
|
|
3838
3742
|
}
|
|
3839
3743
|
],
|
|
3840
3744
|
usage: [
|
|
3841
|
-
"DO own `targetKeys` in state and update it inside `onChange`: `const [targetKeys, setTargetKeys] = useState<string[]>([]);
|
|
3745
|
+
"DO own `targetKeys` in state and update it inside `onChange`: `const [targetKeys, setTargetKeys] = useState<string[]>([]); onValueChange={(next) => setTargetKeys(next)}`.",
|
|
3842
3746
|
"DO NOT hand-roll a two-panel checkbox picker \u2014 Transfer ships the full shuttle UX (select-all header, indeterminate state, search, move buttons, empty state) out of the box.",
|
|
3843
3747
|
"DO enable `showSearch` for lists longer than ~10 items; the built-in SearchInput filters by both `title` and `description` text content, including ReactNode content via `reactNodeText`.",
|
|
3844
3748
|
"DO use `oneWay={true}` for append-only flows (e.g. adding permissions to a role) where items must never be moved back.",
|
|
@@ -3863,11 +3767,11 @@ export function DepartmentFilter() {
|
|
|
3863
3767
|
import { Transfer } from "@godxjp/ui/data-entry";
|
|
3864
3768
|
|
|
3865
3769
|
const ALL_ACCOUNTS = [
|
|
3866
|
-
{
|
|
3867
|
-
{
|
|
3868
|
-
{
|
|
3869
|
-
{
|
|
3870
|
-
{
|
|
3770
|
+
{ value: "1010", title: "Cash", description: "Asset" },
|
|
3771
|
+
{ value: "1020", title: "Accounts Receivable", description: "Asset" },
|
|
3772
|
+
{ value: "2010", title: "Accounts Payable", description: "Liability" },
|
|
3773
|
+
{ value: "3010", title: "Revenue", description: "Income" },
|
|
3774
|
+
{ value: "4010", title: "Cost of Goods Sold", description: "Expense", disabled: true },
|
|
3871
3775
|
];
|
|
3872
3776
|
|
|
3873
3777
|
export function AccountMapping() {
|
|
@@ -3877,7 +3781,7 @@ export function AccountMapping() {
|
|
|
3877
3781
|
<Transfer
|
|
3878
3782
|
dataSource={ALL_ACCOUNTS}
|
|
3879
3783
|
targetKeys={targetKeys}
|
|
3880
|
-
|
|
3784
|
+
onValueChange={(nextKeys) => setTargetKeys(nextKeys)}
|
|
3881
3785
|
titles={["Available Accounts", "Mapped Accounts"]}
|
|
3882
3786
|
showSearch
|
|
3883
3787
|
/>
|
|
@@ -4007,7 +3911,7 @@ export function AvatarUploadForm() {
|
|
|
4007
3911
|
<Upload
|
|
4008
3912
|
variant="avatar-crop"
|
|
4009
3913
|
value={items}
|
|
4010
|
-
|
|
3914
|
+
onValueChange={setItems}
|
|
4011
3915
|
onUpload={handleUpload}
|
|
4012
3916
|
maxSizeBytes={5 * 1024 * 1024}
|
|
4013
3917
|
/>
|
|
@@ -4024,7 +3928,7 @@ export function DocumentUploadDropzone() {
|
|
|
4024
3928
|
<Upload
|
|
4025
3929
|
variant="dropzone"
|
|
4026
3930
|
value={items}
|
|
4027
|
-
|
|
3931
|
+
onValueChange={setItems}
|
|
4028
3932
|
accept=".pdf,.xlsx"
|
|
4029
3933
|
maxCount={10}
|
|
4030
3934
|
maxSizeBytes={20 * 1024 * 1024}
|
|
@@ -4113,7 +4017,7 @@ export function AvatarField() {
|
|
|
4113
4017
|
|
|
4114
4018
|
return (
|
|
4115
4019
|
<>
|
|
4116
|
-
<input type="file" accept="image/*"
|
|
4020
|
+
<input type="file" accept="image/*" onValueChange={handleFileChange} />
|
|
4117
4021
|
<UploadCropDialog
|
|
4118
4022
|
open={cropFile !== null}
|
|
4119
4023
|
onOpenChange={(open) => { if (!open) setCropFile(null); }}
|
|
@@ -4169,7 +4073,7 @@ export function AvatarField() {
|
|
|
4169
4073
|
}
|
|
4170
4074
|
],
|
|
4171
4075
|
usage: [
|
|
4172
|
-
"DO wrap in FormField when a label or validation message is needed \u2014 pass the same id to both FormField and ColorPicker so htmlFor wires up correctly: `<FormField id='brand' label='Brand color'><ColorPicker id='brand' value={v}
|
|
4076
|
+
"DO wrap in FormField when a label or validation message is needed \u2014 pass the same id to both FormField and ColorPicker so htmlFor wires up correctly: `<FormField id='brand' label='Brand color'><ColorPicker id='brand' value={v} onValueChange={setV} /></FormField>`.",
|
|
4173
4077
|
"DO use controlled mode (value + onChange) \u2014 there is no defaultValue/uncontrolled path; always supply value.",
|
|
4174
4078
|
"DON'T pass an invalid or empty string to value \u2014 the component will flash the invalid color on the preview swatch. Always initialize state to a valid 3- or 6-digit hex (e.g. '#2563eb').",
|
|
4175
4079
|
"The hex Input is a live draft field \u2014 onChange is NOT called until the user presses Enter or blurs; only then is the value validated and the parent notified. Do not rely on onChange firing on every keystroke.",
|
|
@@ -4199,7 +4103,7 @@ export function BrandColorField() {
|
|
|
4199
4103
|
<ColorPicker
|
|
4200
4104
|
id="brand-color"
|
|
4201
4105
|
value={color}
|
|
4202
|
-
|
|
4106
|
+
onValueChange={setColor}
|
|
4203
4107
|
/>
|
|
4204
4108
|
</FormField>
|
|
4205
4109
|
);
|
|
@@ -4208,7 +4112,7 @@ export function BrandColorField() {
|
|
|
4208
4112
|
// Compact swatch-only variant (no hex input)
|
|
4209
4113
|
export function SwatchOnly() {
|
|
4210
4114
|
const [color, setColor] = useState("#16a34a");
|
|
4211
|
-
return <ColorPicker value={color}
|
|
4115
|
+
return <ColorPicker value={color} onValueChange={setColor} showHexInput={false} />;
|
|
4212
4116
|
}
|
|
4213
4117
|
|
|
4214
4118
|
// Disabled state
|
|
@@ -4323,7 +4227,7 @@ export function DisabledColor() {
|
|
|
4323
4227
|
"Read-only visual indicator \u2014 pass `disabled` with a controlled `value` to show a progress-style bar that cannot be interacted with."
|
|
4324
4228
|
],
|
|
4325
4229
|
related: [
|
|
4326
|
-
"
|
|
4230
|
+
"Progress \u2014 use Progress (not a disabled Slider) to show read-only progress; Slider with disabled is semantically a control, not a status indicator.",
|
|
4327
4231
|
"Input (type number) \u2014 use Input for free-form numeric entry; use Slider when the range is bounded and dragging is the expected UX.",
|
|
4328
4232
|
"Switch \u2014 for boolean on/off; Slider is for continuous or stepped numeric ranges.",
|
|
4329
4233
|
"RangeField (if present) \u2014 check the MCP first; if a composed range-input field exists, prefer it over wiring two Slider thumbs manually."
|
|
@@ -4719,7 +4623,7 @@ export function ReportRangeFilter() {
|
|
|
4719
4623
|
"Invoice creation forms in an accounting app that require a supplier or customer country, with `allowEmpty={false}` to guarantee a code is always present.",
|
|
4720
4624
|
"Optional 'country of origin' filter fields \u2014 use `allowEmpty={true}` so users can clear the selection back to 'no filter'.",
|
|
4721
4625
|
"Multi-step onboarding flows that must pre-select a country inferred from the user's locale, then let them correct it.",
|
|
4722
|
-
"Read-only display of a country name + flag in a
|
|
4626
|
+
"Read-only display of a country name + flag in a Descriptions or DataTable cell \u2014 use `CountryOptionLabel` directly (not CountrySelect) for non-interactive display."
|
|
4723
4627
|
],
|
|
4724
4628
|
related: [
|
|
4725
4629
|
"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.",
|
|
@@ -5107,7 +5011,7 @@ function AccountQuickPick({ onSelect }: { onSelect: (id: string) => void }) {
|
|
|
5107
5011
|
"RadioGroup \u2014 use when only ONE selection is allowed at a time (mutually exclusive). CheckboxGroup = multiple, RadioGroup = single.",
|
|
5108
5012
|
"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.",
|
|
5109
5013
|
"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.",
|
|
5110
|
-
"Switch /
|
|
5014
|
+
"Switch / ChoiceField \u2014 for a single binary on/off toggle with immediate effect (not a form submission value). Do not use CheckboxGroup to fake toggle rows.",
|
|
5111
5015
|
"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."
|
|
5112
5016
|
],
|
|
5113
5017
|
example: `import { CheckboxGroup } from "@godxjp/ui/data-entry";
|
|
@@ -5141,7 +5045,7 @@ export function ControlledExample() {
|
|
|
5141
5045
|
name="permissions"
|
|
5142
5046
|
options={PERMISSIONS}
|
|
5143
5047
|
value={selected}
|
|
5144
|
-
|
|
5048
|
+
onValueChange={setSelected}
|
|
5145
5049
|
/>
|
|
5146
5050
|
);
|
|
5147
5051
|
}`,
|
|
@@ -5218,7 +5122,7 @@ export function ControlledExample() {
|
|
|
5218
5122
|
],
|
|
5219
5123
|
related: [
|
|
5220
5124
|
"Checkbox.Group \u2014 use when users may select multiple options simultaneously; Radio.Group enforces single-selection only.",
|
|
5221
|
-
"Switch /
|
|
5125
|
+
"Switch / ChoiceField \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.",
|
|
5222
5126
|
"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."
|
|
5223
5127
|
],
|
|
5224
5128
|
example: `{\`import { Radio } from "@godxjp/ui/data-entry";
|
|
@@ -5378,7 +5282,7 @@ function LegacyAccountPicker({ value, onChange }) {
|
|
|
5378
5282
|
return (
|
|
5379
5283
|
<SearchSelect
|
|
5380
5284
|
value={value}
|
|
5381
|
-
|
|
5285
|
+
onValueChange={onChange}
|
|
5382
5286
|
options={[
|
|
5383
5287
|
{ value: "acc-001", label: "Cash", sublabel: "Current assets", group: "Assets" },
|
|
5384
5288
|
{ value: "acc-002", label: "Accounts Receivable", group: "Assets" },
|
|
@@ -5402,7 +5306,7 @@ function LegacyVendorPicker({ value, currentVendorName, onChange }) {
|
|
|
5402
5306
|
return (
|
|
5403
5307
|
<SearchSelect
|
|
5404
5308
|
value={value}
|
|
5405
|
-
|
|
5309
|
+
onValueChange={onChange}
|
|
5406
5310
|
loadOptions={fetchVendors}
|
|
5407
5311
|
selectedLabel={currentVendorName}
|
|
5408
5312
|
placeholder="Select vendor"
|
|
@@ -5491,7 +5395,7 @@ import { Autocomplete } from "@godxjp/ui/data-entry";
|
|
|
5491
5395
|
|
|
5492
5396
|
// \u2705 Replace with:
|
|
5493
5397
|
// import { Select } from "@godxjp/ui/data-entry";
|
|
5494
|
-
// <Select options={options} showSearch placeholder="Search\u2026"
|
|
5398
|
+
// <Select options={options} showSearch placeholder="Search\u2026" onValueChange={setValue} value={value} />
|
|
5495
5399
|
|
|
5496
5400
|
// Legacy usage (backward compat only):
|
|
5497
5401
|
import { Autocomplete } from "@godxjp/ui/data-entry";
|
|
@@ -5870,7 +5774,7 @@ export function FilterSection() {
|
|
|
5870
5774
|
],
|
|
5871
5775
|
related: [
|
|
5872
5776
|
"Timeline \u2014 use Timeline for chronological event sequences with timestamps; use TreeList for hierarchical parent-child structures.",
|
|
5873
|
-
"
|
|
5777
|
+
"Descriptions \u2014 use Descriptions for label/value pairs; use TreeList when items have a parent-child depth relationship.",
|
|
5874
5778
|
"DataTable \u2014 use DataTable for tabular data with columns, sorting, and selection; use TreeList for a single-column hierarchical list without those features.",
|
|
5875
5779
|
"EmptyState \u2014 pair with EmptyState when the items array may be empty; TreeList renders nothing (no empty row) when given an empty array."
|
|
5876
5780
|
],
|
|
@@ -5890,115 +5794,6 @@ export function ChartOfAccounts() {
|
|
|
5890
5794
|
storyPath: "data-display/TreeList.stories.tsx",
|
|
5891
5795
|
rules: [3, 6, 23, 31]
|
|
5892
5796
|
},
|
|
5893
|
-
{
|
|
5894
|
-
name: "ScanPanel",
|
|
5895
|
-
group: "data-display",
|
|
5896
|
-
tagline: "Square dashed placeholder panel with a scan-line icon \u2014 for empty/waiting states where content is scanned or uploaded; never hand-roll this pattern with a raw div.",
|
|
5897
|
-
props: [
|
|
5898
|
-
{
|
|
5899
|
-
name: "title",
|
|
5900
|
-
type: "string",
|
|
5901
|
-
required: true,
|
|
5902
|
-
description: "Primary label rendered below the scan-line icon in semibold large text."
|
|
5903
|
-
},
|
|
5904
|
-
{
|
|
5905
|
-
name: "description",
|
|
5906
|
-
type: "string",
|
|
5907
|
-
description: "Optional secondary line rendered below the title in muted small text. Omit if no additional context is needed."
|
|
5908
|
-
}
|
|
5909
|
-
],
|
|
5910
|
-
usage: [
|
|
5911
|
-
"DO use ScanPanel for any empty/awaiting-input area that represents a 'scan something here' or 'drop/upload file' prompt \u2014 it provides the icon, border, background tint, and typography in one call.",
|
|
5912
|
-
"DON'T hand-roll a dashed-border div with a lucide icon and centered text \u2014 ScanPanel is exactly that pattern and keeps visual consistency across the app.",
|
|
5913
|
-
"The panel renders 1:1 aspect-ratio (square) via CSS; constrain its width from the parent container (e.g. max-w-xs) rather than trying to override aspect-ratio.",
|
|
5914
|
-
"DO pass a concise `title` (required) that names the action or state (e.g. 'Scan invoice', 'No file selected'). Use `description` only for a secondary instruction or status line.",
|
|
5915
|
-
"DON'T put interactive controls inside ScanPanel \u2014 it is a pure display/empty-state element. Pair it with a nearby Button or file-input for the actual action.",
|
|
5916
|
-
"ScanPanel is NOT a general EmptyState \u2014 use godx-ui EmptyState for table/list zero-row states. ScanPanel is specifically for scan/upload affordance panels."
|
|
5917
|
-
],
|
|
5918
|
-
useCases: [
|
|
5919
|
-
"An invoice-scanning workflow step where the user must point a camera or upload an image \u2014 show ScanPanel while no file is selected.",
|
|
5920
|
-
"A QR-code / barcode reader pane rendered before the camera stream is active, indicating the scan target area.",
|
|
5921
|
-
"A document upload dropzone placeholder in an accounting app (expense receipts, purchase orders) before any file is chosen.",
|
|
5922
|
-
"An OCR processing panel shown while the system awaits a document scan input from a connected scanner device.",
|
|
5923
|
-
"A 'no attachment yet' state in an accounting record detail view that invites the user to attach a scanned document."
|
|
5924
|
-
],
|
|
5925
|
-
related: [
|
|
5926
|
-
"EmptyState \u2014 use for zero-row table/list states or generic no-data screens; ScanPanel is specifically scan/upload affordance with its own icon and square layout.",
|
|
5927
|
-
"DataState / InfiniteQueryState \u2014 use for TanStack Query lifecycle (loading/empty/error) in list views; not for scan prompts.",
|
|
5928
|
-
"SkeletonTable \u2014 use while data is loading into a table; not for scan/upload placeholder states."
|
|
5929
|
-
],
|
|
5930
|
-
example: `import { ScanPanel } from "@godxjp/ui/data-display";
|
|
5931
|
-
|
|
5932
|
-
export function InvoiceScanStep() {
|
|
5933
|
-
return (
|
|
5934
|
-
<div className="max-w-xs mx-auto">
|
|
5935
|
-
<ScanPanel
|
|
5936
|
-
title="Scan invoice"
|
|
5937
|
-
description="Point your camera at the invoice barcode or upload a file."
|
|
5938
|
-
/>
|
|
5939
|
-
</div>
|
|
5940
|
-
);
|
|
5941
|
-
}`,
|
|
5942
|
-
storyPath: "data-display/ScanPanel.stories.tsx",
|
|
5943
|
-
rules: [31]
|
|
5944
|
-
},
|
|
5945
|
-
{
|
|
5946
|
-
name: "CodeBadge",
|
|
5947
|
-
group: "data-display",
|
|
5948
|
-
tagline: "Compact labelled icon-chip for displaying typed reference codes (internal, seller, or carrier); never use Badge or a raw span for these domain codes.",
|
|
5949
|
-
props: [
|
|
5950
|
-
{
|
|
5951
|
-
name: "kind",
|
|
5952
|
-
type: '"internal" | "seller" | "yamato"',
|
|
5953
|
-
required: true,
|
|
5954
|
-
description: 'Determines the icon and abbreviated label prefix rendered inside the chip. "internal" \u2192 Hash icon + "INT"; "seller" \u2192 ShoppingBag + "SLR"; "yamato" \u2192 Truck + "YMT". Falls back to "internal" if an unknown kind is supplied.'
|
|
5955
|
-
},
|
|
5956
|
-
{
|
|
5957
|
-
name: "value",
|
|
5958
|
-
type: "string",
|
|
5959
|
-
required: true,
|
|
5960
|
-
description: "The actual reference code string to display after the icon (e.g. an order number, shipment tracking ID, or internal SKU)."
|
|
5961
|
-
}
|
|
5962
|
-
],
|
|
5963
|
-
usage: [
|
|
5964
|
-
"DO: always supply both `kind` and `value` \u2014 both are required; omitting either leaves the chip meaningless or broken.",
|
|
5965
|
-
'DO: use `kind="internal"` for internal system IDs/SKUs, `kind="seller"` for seller/merchant reference codes, and `kind="yamato"` for Yamato carrier/shipment tracking codes.',
|
|
5966
|
-
"DON'T: use a raw `<Badge>` or a plain `<span>` for domain reference codes \u2014 `CodeBadge` encodes the correct icon, label prefix, and semantic `data-kind` attribute that CSS/tests rely on.",
|
|
5967
|
-
"DON'T: pass display-formatted values (e.g. with leading/trailing whitespace or HTML entities) \u2014 `value` is rendered as plain text inside a `<span>`.",
|
|
5968
|
-
"DON'T: attempt to extend `kind` inline \u2014 if a new code type is needed, add it to `CodeBadgeKind` in the source and export; never pass an arbitrary string and expect the chip to render correctly (it silently falls back to `internal`).",
|
|
5969
|
-
'A11Y: the icon is rendered with `aria-hidden="true"` so screen readers see only the label prefix and value text \u2014 keep `value` human-readable (e.g. the actual code, not an opaque hash).'
|
|
5970
|
-
],
|
|
5971
|
-
useCases: [
|
|
5972
|
-
"Displaying an order's internal system reference code alongside seller and carrier codes in an order detail panel or DataTable column.",
|
|
5973
|
-
"Rendering a Yamato shipment tracking number in a logistics/fulfillment table so operators can visually distinguish it from internal IDs at a glance.",
|
|
5974
|
-
"Showing a seller's own reference code (e.g. merchant PO number) in an invoice or accounting line-item row.",
|
|
5975
|
-
"Pairing multiple CodeBadge instances in a KeyValueGrid row \u2014 e.g. INT code next to SLR code \u2014 to show all reference IDs for a single transaction.",
|
|
5976
|
-
"In a DataTable cell renderer, wrapping a code field so the column's kind is immediately obvious without a separate column header."
|
|
5977
|
-
],
|
|
5978
|
-
related: [
|
|
5979
|
-
"Badge \u2014 generic variant-styled pill (default/secondary/destructive/outline/success/warning). Use Badge for arbitrary status labels or counts. Use CodeBadge specifically for typed domain reference codes (internal/seller/yamato) that need a fixed icon+prefix.",
|
|
5980
|
-
'StatusBadge \u2014 displays a status string with a semantic tone. Use StatusBadge for workflow/order status chips (e.g. "Paid", "Pending"). Use CodeBadge for reference/ID codes, not status labels.'
|
|
5981
|
-
],
|
|
5982
|
-
example: `{\`import { CodeBadge } from "@godxjp/ui/data-display";
|
|
5983
|
-
|
|
5984
|
-
// Internal system reference code
|
|
5985
|
-
<CodeBadge kind="internal" value="ORD-00123" />
|
|
5986
|
-
|
|
5987
|
-
// Seller reference code
|
|
5988
|
-
<CodeBadge kind="seller" value="SLR-98765" />
|
|
5989
|
-
|
|
5990
|
-
// Yamato carrier tracking code
|
|
5991
|
-
<CodeBadge kind="yamato" value="YMT-4444-5555-6666" />
|
|
5992
|
-
|
|
5993
|
-
// Multiple codes for one order
|
|
5994
|
-
<div className="flex flex-wrap gap-2">
|
|
5995
|
-
<CodeBadge kind="internal" value="ORD-00123" />
|
|
5996
|
-
<CodeBadge kind="seller" value="SLR-98765" />
|
|
5997
|
-
<CodeBadge kind="yamato" value="YMT-4444-5555-6666" />
|
|
5998
|
-
</div>\`}`,
|
|
5999
|
-
storyPath: "data-display/CodeBadge.stories.tsx",
|
|
6000
|
-
rules: [3, 6, 13, 35]
|
|
6001
|
-
},
|
|
6002
5797
|
{
|
|
6003
5798
|
name: "PageHeader",
|
|
6004
5799
|
group: "navigation",
|
|
@@ -6114,10 +5909,10 @@ export function LegacyInvoiceHeader() {
|
|
|
6114
5909
|
],
|
|
6115
5910
|
useCases: [
|
|
6116
5911
|
"App shell / top-nav language switcher that persists the user's locale preference via AppProvider and localStorage without any extra state.",
|
|
6117
|
-
"Settings page 'Language' field where locale is part of a form submitted to the backend \u2014 use controlled mode: value={form.locale}
|
|
5912
|
+
"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)}.",
|
|
6118
5913
|
"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.",
|
|
6119
5914
|
"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.",
|
|
6120
|
-
"Storybook / test harness where AppProvider is not present \u2014 render in fully controlled mode: <LocalePicker value='en'
|
|
5915
|
+
"Storybook / test harness where AppProvider is not present \u2014 render in fully controlled mode: <LocalePicker value='en' onValueChange={fn} />.",
|
|
6121
5916
|
"Localization QA tool that cycles through locales programmatically \u2014 drive via controlled value to switch the UI language without user interaction."
|
|
6122
5917
|
],
|
|
6123
5918
|
related: [
|
|
@@ -6149,7 +5944,7 @@ export function LocaleField() {
|
|
|
6149
5944
|
return (
|
|
6150
5945
|
<div className="flex flex-col gap-1.5">
|
|
6151
5946
|
<label htmlFor="locale-picker">Language</label>
|
|
6152
|
-
<LocalePicker id="locale-picker" value={locale}
|
|
5947
|
+
<LocalePicker id="locale-picker" value={locale} onValueChange={setLocale} />
|
|
6153
5948
|
</div>
|
|
6154
5949
|
);
|
|
6155
5950
|
}\`}`,
|
|
@@ -6190,7 +5985,7 @@ export function LocaleField() {
|
|
|
6190
5985
|
],
|
|
6191
5986
|
usage: [
|
|
6192
5987
|
"DO: Wrap with <AppProvider> and omit value/onChange \u2014 the picker reads and writes context automatically. This is the canonical zero-prop usage: <TimezonePicker />.",
|
|
6193
|
-
"DO: Pass value + onChange for fully controlled standalone usage (e.g. a form field that posts the timezone string): <TimezonePicker value={tz}
|
|
5988
|
+
"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.",
|
|
6194
5989
|
"DON'T: Omit BOTH AppProvider context AND controlled props \u2014 the component throws at runtime: 'TimezonePicker requires <AppProvider> or controlled value + onChange'.",
|
|
6195
5990
|
"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.",
|
|
6196
5991
|
"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.",
|
|
@@ -6219,7 +6014,7 @@ export function TimezoneField() {
|
|
|
6219
6014
|
return (
|
|
6220
6015
|
<TimezonePicker
|
|
6221
6016
|
value={tz}
|
|
6222
|
-
|
|
6017
|
+
onValueChange={setTz}
|
|
6223
6018
|
options={["Asia/Tokyo", "Asia/Ho_Chi_Minh", "UTC"]}
|
|
6224
6019
|
/>
|
|
6225
6020
|
);
|
|
@@ -6229,7 +6024,7 @@ export function TimezoneField() {
|
|
|
6229
6024
|
import { AppProvider } from "@godxjp/ui/app";
|
|
6230
6025
|
import { APP_TIMEZONE_PRESET } from "@godxjp/ui/navigation";
|
|
6231
6026
|
|
|
6232
|
-
export function Shell({ children }: {
|
|
6027
|
+
export function Shell({ children }: { content: React.ReactNode }) {
|
|
6233
6028
|
return (
|
|
6234
6029
|
<AppProvider
|
|
6235
6030
|
defaultLocale="ja"
|
|
@@ -6327,7 +6122,7 @@ export function ExportDialog() {
|
|
|
6327
6122
|
return (
|
|
6328
6123
|
<div className="flex items-center gap-2">
|
|
6329
6124
|
<label htmlFor="export-fmt">Export date format</label>
|
|
6330
|
-
<DateFormatPicker id="export-fmt" value={fmt}
|
|
6125
|
+
<DateFormatPicker id="export-fmt" value={fmt} onValueChange={setFmt} />
|
|
6331
6126
|
</div>
|
|
6332
6127
|
);
|
|
6333
6128
|
}\`}`,
|
|
@@ -6367,7 +6162,7 @@ export function ExportDialog() {
|
|
|
6367
6162
|
],
|
|
6368
6163
|
usage: [
|
|
6369
6164
|
"DO use inside <AppProvider> with no extra props to let it read/write the global time-format automatically: <AppProvider defaultTimeFormat='24h'><TimeFormatPicker /></AppProvider>",
|
|
6370
|
-
"DO switch to fully controlled mode when you need to manage the value yourself \u2014 supply BOTH value and onChange, or the component will throw: <TimeFormatPicker value={fmt}
|
|
6165
|
+
"DO switch to fully controlled mode when you need to manage the value yourself \u2014 supply BOTH value and onChange, or the component will throw: <TimeFormatPicker value={fmt} onValueChange={setFmt} />",
|
|
6371
6166
|
"DON'T omit both AppProvider and controlled props \u2014 the component throws an Error at render time: 'TimeFormatPicker requires <AppProvider> or controlled value + onChange'. There is no silent fallback.",
|
|
6372
6167
|
"DON'T hand-roll a time-format <select> \u2014 the locale-aware labels (e.g. '24 gi\u1EDD' for vi, '24-hour' for en) are generated internally from the i18n layer; reinventing this loses those translations.",
|
|
6373
6168
|
"DO wire a <label htmlFor={id}> when using the id prop for accessibility; the SelectTrigger already sets aria-label from i18n but a visible label improves discoverability.",
|
|
@@ -6410,7 +6205,7 @@ export function SettingsForm() {
|
|
|
6410
6205
|
return (
|
|
6411
6206
|
<div>
|
|
6412
6207
|
<label htmlFor="time-fmt">Time format</label>
|
|
6413
|
-
<TimeFormatPicker id="time-fmt" value={fmt}
|
|
6208
|
+
<TimeFormatPicker id="time-fmt" value={fmt} onValueChange={setFmt} />
|
|
6414
6209
|
</div>
|
|
6415
6210
|
);
|
|
6416
6211
|
}\`}
|
|
@@ -6418,366 +6213,6 @@ export function SettingsForm() {
|
|
|
6418
6213
|
storyPath: "navigation/TimeFormatPicker.stories.tsx",
|
|
6419
6214
|
rules: [3, 5, 6, 13]
|
|
6420
6215
|
},
|
|
6421
|
-
{
|
|
6422
|
-
name: "TabsItems",
|
|
6423
|
-
group: "navigation",
|
|
6424
|
-
tagline: "Ant Design-style items-array tabs wrapper over Radix Tabs \u2014 pass a flat `items` array instead of composing TabsList/TabsTrigger/TabsContent by hand; first item is the default tab unless you specify otherwise.",
|
|
6425
|
-
props: [
|
|
6426
|
-
{
|
|
6427
|
-
name: "items",
|
|
6428
|
-
type: "TabItemProp[]",
|
|
6429
|
-
required: true,
|
|
6430
|
-
description: "Array of tab definitions. Each entry has: `key: string` (unique, used as the Radix value), `label: React.ReactNode` (trigger label), `children: React.ReactNode` (panel content), `disabled?: boolean`, `icon?: React.ReactNode` (rendered left of label inside the trigger)."
|
|
6431
|
-
},
|
|
6432
|
-
{
|
|
6433
|
-
name: "value",
|
|
6434
|
-
type: "string",
|
|
6435
|
-
description: "Controlled active tab key. When provided the component is fully controlled \u2014 you must also provide `onValueChange`. Do NOT combine with `defaultValue` in controlled mode."
|
|
6436
|
-
},
|
|
6437
|
-
{
|
|
6438
|
-
name: "defaultValue",
|
|
6439
|
-
type: "string",
|
|
6440
|
-
description: "Uncontrolled initial active tab key. Defaults to `items[0].key` when omitted. Ignored when `value` is provided."
|
|
6441
|
-
},
|
|
6442
|
-
{
|
|
6443
|
-
name: "onValueChange",
|
|
6444
|
-
type: "(key: string) => void",
|
|
6445
|
-
description: "Callback fired when the user switches tabs. Receives the selected item's `key`. Required in controlled mode."
|
|
6446
|
-
},
|
|
6447
|
-
{
|
|
6448
|
-
name: "variant",
|
|
6449
|
-
type: '"default" | "line" | "card"',
|
|
6450
|
-
defaultValue: '"default"',
|
|
6451
|
-
description: "Visual style. `default` = pill/box tabs (Radix default). `line` = underline-only tabs (border-b indicator, transparent background). `card` = card-style tabs with a subtle shadow on the active trigger."
|
|
6452
|
-
},
|
|
6453
|
-
{
|
|
6454
|
-
name: "className",
|
|
6455
|
-
type: "string",
|
|
6456
|
-
description: "Extra Tailwind classes applied to the outer Radix `Tabs` root element."
|
|
6457
|
-
},
|
|
6458
|
-
{
|
|
6459
|
-
name: "listClassName",
|
|
6460
|
-
type: "string",
|
|
6461
|
-
description: "Extra classes applied to the `TabsList` wrapper (the trigger bar). Useful to control width, alignment, or border overrides."
|
|
6462
|
-
},
|
|
6463
|
-
{
|
|
6464
|
-
name: "contentClassName",
|
|
6465
|
-
type: "string",
|
|
6466
|
-
description: "Extra classes applied to every `TabsContent` panel. Use for padding, min-height, or background overrides shared across all panels."
|
|
6467
|
-
}
|
|
6468
|
-
],
|
|
6469
|
-
usage: [
|
|
6470
|
-
"DO: pass a flat `items` array with unique `key` strings \u2014 TabsItems renders the full TabsList + all TabsContent panels for you. NEVER hand-compose TabsList/TabsTrigger/TabsContent inside TabsItems; they are rendered internally.",
|
|
6471
|
-
"DO: use `defaultValue` (uncontrolled) for simple local tab state. Use `value` + `onValueChange` together (controlled) when the active tab is driven by router state, query params, or parent state. DO NOT set both `value` and `defaultValue` simultaneously.",
|
|
6472
|
-
"DO: choose `variant='line'` for page-level section navigation (full-width underline style) and `variant='default'` for contained widget tabs (pill/box). `variant='card'` suits dashboard card contexts.",
|
|
6473
|
-
"DON'T: rely on tab position for the default tab \u2014 always supply `defaultValue` explicitly when the first item should not be active, or when items order may change.",
|
|
6474
|
-
"DO: use `icon` on each `TabItemProp` to prepend an icon node (e.g. a Lucide icon) inside the trigger. Icons are rendered in an inline-flex span with `mr-1.5` spacing; pass sized icon components, not raw SVG strings.",
|
|
6475
|
-
"DON'T: use TabsItems to replace a full Radix Tabs composition when you need per-panel extra attributes (e.g. `forceMount`, custom `TabsContent` event handlers) \u2014 drop down to the lower-level `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` primitives from the same import subpath."
|
|
6476
|
-
],
|
|
6477
|
-
useCases: [
|
|
6478
|
-
"Admin detail pages with multiple sections (Overview / Transactions / Attachments / Audit Log) where all content is known up-front and switching is purely client-side.",
|
|
6479
|
-
"Accounting invoice or journal-entry detail drawers that split metadata, line items, and comments into named tabs without needing URL routing.",
|
|
6480
|
-
"Dashboard widgets presenting the same data in different views (e.g. Chart / Table / Raw) using controlled `value` driven by a toolbar toggle.",
|
|
6481
|
-
"Settings pages with a vertical or horizontal tab strip (line variant) separating General / Notifications / Billing / Security sections.",
|
|
6482
|
-
"Entity profile pages (company, partner, employee) where each tab loads deferred or lazy content \u2014 combine with Inertia deferred props per panel.",
|
|
6483
|
-
"Any place where Ant Design-style `<Tabs items={[...]} />` would have been used \u2014 TabsItems is the direct godx-ui equivalent."
|
|
6484
|
-
],
|
|
6485
|
-
related: [
|
|
6486
|
-
"Tabs / TabsList / TabsTrigger / TabsContent (@godxjp/ui/navigation) \u2014 lower-level Radix primitives. Use these when you need per-panel forceMount, custom data attributes on individual panels, or full control over trigger/content rendering. TabsItems wraps these internally.",
|
|
6487
|
-
"Steps (@godxjp/ui/navigation) \u2014 sequential wizard/progress indicator. Use Steps for multi-step flows where order matters and completion state must be tracked; use TabsItems for non-sequential switchable views.",
|
|
6488
|
-
"FilterBar / FilterGroup (@godxjp/ui/navigation) \u2014 horizontal filter chip row. Use FilterBar when the 'tabs' are really dataset filters (not content panels). Visually similar to line-variant tabs but semantically and behaviourally different."
|
|
6489
|
-
],
|
|
6490
|
-
example: `{\`import { TabsItems } from "@godxjp/ui/navigation";
|
|
6491
|
-
import { FileText, List, BarChart2 } from "lucide-react";
|
|
6492
|
-
|
|
6493
|
-
// Uncontrolled \u2014 first item active by default
|
|
6494
|
-
export function InvoiceDetailTabs() {
|
|
6495
|
-
return (
|
|
6496
|
-
<TabsItems
|
|
6497
|
-
variant="line"
|
|
6498
|
-
items={[
|
|
6499
|
-
{
|
|
6500
|
-
key: "overview",
|
|
6501
|
-
label: "Overview",
|
|
6502
|
-
icon: <FileText size={14} />,
|
|
6503
|
-
children: <OverviewPanel />,
|
|
6504
|
-
},
|
|
6505
|
-
{
|
|
6506
|
-
key: "line-items",
|
|
6507
|
-
label: "Line Items",
|
|
6508
|
-
icon: <List size={14} />,
|
|
6509
|
-
children: <LineItemsTable />,
|
|
6510
|
-
},
|
|
6511
|
-
{
|
|
6512
|
-
key: "analytics",
|
|
6513
|
-
label: "Analytics",
|
|
6514
|
-
icon: <BarChart2 size={14} />,
|
|
6515
|
-
disabled: false,
|
|
6516
|
-
children: <AnalyticsChart />,
|
|
6517
|
-
},
|
|
6518
|
-
]}
|
|
6519
|
-
/>
|
|
6520
|
-
);
|
|
6521
|
-
}
|
|
6522
|
-
|
|
6523
|
-
// Controlled \u2014 active tab driven by URL search param
|
|
6524
|
-
export function ControlledTabs() {
|
|
6525
|
-
const [tab, setTab] = React.useState("overview");
|
|
6526
|
-
|
|
6527
|
-
return (
|
|
6528
|
-
<TabsItems
|
|
6529
|
-
variant="default"
|
|
6530
|
-
value={tab}
|
|
6531
|
-
onValueChange={setTab}
|
|
6532
|
-
items={[
|
|
6533
|
-
{ key: "overview", label: "Overview", children: <OverviewPanel /> },
|
|
6534
|
-
{ key: "history", label: "History", children: <HistoryPanel /> },
|
|
6535
|
-
]}
|
|
6536
|
-
/>
|
|
6537
|
-
);
|
|
6538
|
-
}\`}`,
|
|
6539
|
-
storyPath: "navigation/TabsItems.stories.tsx",
|
|
6540
|
-
rules: [3, 23, 31, 37]
|
|
6541
|
-
},
|
|
6542
|
-
{
|
|
6543
|
-
name: "Menu",
|
|
6544
|
-
group: "layout",
|
|
6545
|
-
tagline: "A simplified sidebar-backed navigation menu \u2014 pass sections with active flags and Menu resolves the activeId automatically; never set activeId manually.",
|
|
6546
|
-
props: [
|
|
6547
|
-
{
|
|
6548
|
-
name: "items",
|
|
6549
|
-
type: "MenuSection[]",
|
|
6550
|
-
required: true,
|
|
6551
|
-
description: "Array of navigation sections. Each section has an optional label and an array of MenuItem objects. The first item with active: true becomes the active route; if none is marked the very first item is treated as active."
|
|
6552
|
-
}
|
|
6553
|
-
],
|
|
6554
|
-
usage: [
|
|
6555
|
-
"DO: set `active: true` on the single MenuItem that matches the current route \u2014 Menu derives `activeId` from this flag automatically. DON'T try to pass `activeId` (it does not exist on Menu).",
|
|
6556
|
-
"DO: supply every item with a unique `id` string \u2014 it is required by the underlying SidebarItemProp. Duplicate ids cause incorrect active/highlight state.",
|
|
6557
|
-
"DO: provide a Lucide (or compatible SVG) icon component via the `icon` field on each MenuItem. The icon is required by SidebarItemProp and will cause a render error if omitted.",
|
|
6558
|
-
"DO: nest child pages under an item using `children: SidebarItemProp[]` \u2014 this renders a collapsible submenu that auto-opens when any child is active.",
|
|
6559
|
-
"DON'T use Menu when you need to control collapse state, show a product switcher, render a custom brand slot, or attach an onSelect handler \u2014 use Sidebar directly for those scenarios.",
|
|
6560
|
-
"MenuItem extends SidebarItem so you may also pass `badge` (ReactNode), `disabled` (boolean), and `children` (nested items) on each item."
|
|
6561
|
-
],
|
|
6562
|
-
useCases: [
|
|
6563
|
-
"App-shell left-rail navigation where the current route is already known from the router and active state can be derived from a simple boolean flag on each item.",
|
|
6564
|
-
"Admin dashboards with a small, fixed set of top-level nav entries (Dashboard, Invoices, Settings) grouped into labelled sections (e.g. 'Accounting', 'Admin').",
|
|
6565
|
-
"Multi-section sidebars where some sections have nested child pages (e.g. Reports > Income Statement, Balance Sheet) and you want collapsible submenu groups without wiring up Sidebar manually.",
|
|
6566
|
-
"Situations where the product branding chip shown in the sidebar is purely cosmetic and does not need a real switcher \u2014 Menu hard-codes a placeholder product; use Sidebar when the product chip must be interactive or the brand slot needs a custom node.",
|
|
6567
|
-
"Rapid prototyping: quickly render a working sidebar nav by passing a flat MenuSection array without needing to understand Sidebar's full API."
|
|
6568
|
-
],
|
|
6569
|
-
related: [
|
|
6570
|
-
"Sidebar \u2014 the underlying component Menu wraps. Use Sidebar directly when you need: a real product/brand switcher (onProductClick, brand slot, productMenu), an onSelect handler to intercept navigation, a collapsed/rail mode toggle, or a custom footer node. Menu is a thin convenience layer on top of Sidebar that trades configurability for simplicity.",
|
|
6571
|
-
"AppShell \u2014 the page-level shell that accepts a sidebar node. Pass a Menu (or Sidebar) as its sidebar prop.",
|
|
6572
|
-
"Topbar \u2014 the horizontal bar that pairs with AppShell; not a replacement for Menu/Sidebar nav."
|
|
6573
|
-
],
|
|
6574
|
-
example: `
|
|
6575
|
-
import { Menu } from "@godxjp/ui/layout";
|
|
6576
|
-
import { LayoutDashboard, FileText, Settings, Users } from "lucide-react";
|
|
6577
|
-
|
|
6578
|
-
const sections = [
|
|
6579
|
-
{
|
|
6580
|
-
label: "Accounting",
|
|
6581
|
-
items: [
|
|
6582
|
-
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard, active: true },
|
|
6583
|
-
{ id: "invoices", label: "Invoices", icon: FileText },
|
|
6584
|
-
],
|
|
6585
|
-
},
|
|
6586
|
-
{
|
|
6587
|
-
label: "Admin",
|
|
6588
|
-
items: [
|
|
6589
|
-
{
|
|
6590
|
-
id: "users",
|
|
6591
|
-
label: "Users",
|
|
6592
|
-
icon: Users,
|
|
6593
|
-
children: [
|
|
6594
|
-
{ id: "users-list", label: "All Users", icon: Users },
|
|
6595
|
-
{ id: "users-invite", label: "Invite", icon: Users },
|
|
6596
|
-
],
|
|
6597
|
-
},
|
|
6598
|
-
{ id: "settings", label: "Settings", icon: Settings },
|
|
6599
|
-
],
|
|
6600
|
-
},
|
|
6601
|
-
];
|
|
6602
|
-
|
|
6603
|
-
export default function AppSidebar() {
|
|
6604
|
-
return <Menu items={sections} />;
|
|
6605
|
-
}
|
|
6606
|
-
`,
|
|
6607
|
-
storyPath: "layout/Menu.stories.tsx",
|
|
6608
|
-
rules: [23]
|
|
6609
|
-
},
|
|
6610
|
-
{
|
|
6611
|
-
name: "ShellApp",
|
|
6612
|
-
group: "layout",
|
|
6613
|
-
tagline: "Opinionated full-page shell (AppShell + a hardcoded Topbar) \u2014 use AppShell directly if you need to configure the topbar.",
|
|
6614
|
-
props: [
|
|
6615
|
-
{
|
|
6616
|
-
name: "menu",
|
|
6617
|
-
type: "ReactNode",
|
|
6618
|
-
required: true,
|
|
6619
|
-
description: "Sidebar content \u2014 pass a configured <Sidebar> component here. Rendered inside the left rail."
|
|
6620
|
-
},
|
|
6621
|
-
{
|
|
6622
|
-
name: "breadcrumb",
|
|
6623
|
-
type: "ReactNode",
|
|
6624
|
-
description: "Optional breadcrumb strip rendered above the main content area, inside the app-breadcrumb div."
|
|
6625
|
-
},
|
|
6626
|
-
{
|
|
6627
|
-
name: "children",
|
|
6628
|
-
type: "ReactNode",
|
|
6629
|
-
required: true,
|
|
6630
|
-
description: "Main page content \u2014 rendered inside <main class='app-main'>. Typically a <PageContainer> or a list of page-level components."
|
|
6631
|
-
}
|
|
6632
|
-
],
|
|
6633
|
-
usage: [
|
|
6634
|
-
"DO pass a fully configured <Sidebar> (with activeId, onSelect, sections) as the `menu` prop \u2014 ShellApp only renders what you give it; it has no built-in nav state.",
|
|
6635
|
-
"DO use ShellApp when you want the default GodX Topbar (product chip 'GodX', no live search/notification handlers) and only need sidebar + breadcrumb configuration. For any custom topbar (entity switcher, real onSearchOpen, onNotificationsOpen, user slot) use <AppShell> directly with your own <Topbar> passed to its `topbar` prop.",
|
|
6636
|
-
"DO NOT nest a second shell or AppShell inside ShellApp's children \u2014 ShellApp renders the root app-root div with sidebar, header, and main; nesting shells breaks layout.",
|
|
6637
|
-
"DO NOT pass layout-wrapper divs as children expecting them to fill the full viewport \u2014 ShellApp's <main> already provides the scroll container; add padding via <PageContainer> or <PageInset> inside children.",
|
|
6638
|
-
"DO pass a <Breadcrumb> (from @godxjp/ui/layout) node to the `breadcrumb` prop for breadcrumb navigation \u2014 do not hand-roll a breadcrumb strip inside children.",
|
|
6639
|
-
"The built-in Topbar rendered by ShellApp has no-op handlers for search and notifications (both fire () => undefined). Wire those interactions by switching to AppShell + Topbar directly."
|
|
6640
|
-
],
|
|
6641
|
-
useCases: [
|
|
6642
|
-
"Rapid admin panel scaffolding where the default GodX branding topbar is acceptable and the only variable parts are the sidebar menu and page content.",
|
|
6643
|
-
"Internal tools and dashboards that need a consistent three-zone shell (sidebar / topbar / main) without customising the product chip or notification system.",
|
|
6644
|
-
"Prototyping or demo apps where you want a full AppShell layout with minimal boilerplate \u2014 three props (menu, children, optional breadcrumb) vs AppShell's seven.",
|
|
6645
|
-
"Multi-page Inertia SPA where the sidebar, topbar chrome, and layout are shared across all pages via a persistent layout and each page only provides its own <PageContainer> as children."
|
|
6646
|
-
],
|
|
6647
|
-
related: [
|
|
6648
|
-
"AppShell \u2014 the underlying primitive ShellApp wraps. Use AppShell directly when you need to supply your own <Topbar> (custom product chip, entity switcher via productMenu, real search/notification handlers, user avatar slot, rightSlot) or any of topbarLeft/topbarRight/logo/footer/sidebarCollapsed.",
|
|
6649
|
-
"Sidebar \u2014 pass as the `menu` prop; handles activeId, collapsing, section labels, nested groups with Collapsible, and collapsed popover flyouts.",
|
|
6650
|
-
"Topbar \u2014 ShellApp renders a frozen Topbar internally; import Topbar from @godxjp/ui/layout and pass it to AppShell's topbar prop when you need live handlers.",
|
|
6651
|
-
"PageContainer \u2014 the right component to use as the direct child of ShellApp to get a titled, padded page with actions/breadcrumb/footer.",
|
|
6652
|
-
"PageInset \u2014 use inside a flush PageContainer for padded strips (filter bars, intros) that sit above a full-bleed DataTable."
|
|
6653
|
-
],
|
|
6654
|
-
example: `{\`import { ShellApp, Sidebar, PageContainer, Breadcrumb } from "@godxjp/ui/layout";
|
|
6655
|
-
import { LayoutDashboard, FileText, Settings } from "lucide-react";
|
|
6656
|
-
|
|
6657
|
-
const NAV_SECTIONS = [
|
|
6658
|
-
{
|
|
6659
|
-
items: [
|
|
6660
|
-
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
|
6661
|
-
{ id: "invoices", label: "Invoices", icon: FileText },
|
|
6662
|
-
{ id: "settings", label: "Settings", icon: Settings },
|
|
6663
|
-
],
|
|
6664
|
-
},
|
|
6665
|
-
];
|
|
6666
|
-
|
|
6667
|
-
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
6668
|
-
const [activeId, setActiveId] = React.useState("dashboard");
|
|
6669
|
-
|
|
6670
|
-
return (
|
|
6671
|
-
<ShellApp
|
|
6672
|
-
menu={
|
|
6673
|
-
<Sidebar
|
|
6674
|
-
activeId={activeId}
|
|
6675
|
-
onSelect={setActiveId}
|
|
6676
|
-
sections={NAV_SECTIONS}
|
|
6677
|
-
product={{ name: "CoreBooks", color: "hsl(var(--attention))" }}
|
|
6678
|
-
/>
|
|
6679
|
-
}
|
|
6680
|
-
breadcrumb={
|
|
6681
|
-
<Breadcrumb items={[{ label: "Home", href: "/" }, { label: "Dashboard" }]} />
|
|
6682
|
-
}
|
|
6683
|
-
>
|
|
6684
|
-
{children}
|
|
6685
|
-
</ShellApp>
|
|
6686
|
-
);
|
|
6687
|
-
}\`}`,
|
|
6688
|
-
storyPath: "layout/ShellApp.stories.tsx",
|
|
6689
|
-
rules: [23, 24, 31, 35]
|
|
6690
|
-
},
|
|
6691
|
-
{
|
|
6692
|
-
name: "MobileFrame",
|
|
6693
|
-
group: "layout",
|
|
6694
|
-
tagline: "Simulated smartphone chrome (header + scrollable body + bottom nav) for handheld/PWA screens \u2014 it is a full-page layout shell, not a modal or decoration.",
|
|
6695
|
-
props: [
|
|
6696
|
-
{
|
|
6697
|
-
name: "title",
|
|
6698
|
-
type: "string",
|
|
6699
|
-
required: true,
|
|
6700
|
-
description: "Primary heading shown in the mobile header bar (e.g. page name or branch name)."
|
|
6701
|
-
},
|
|
6702
|
-
{
|
|
6703
|
-
name: "subtitle",
|
|
6704
|
-
type: "string",
|
|
6705
|
-
description: "Secondary line rendered beneath the title \u2014 typically a context descriptor such as location or mode."
|
|
6706
|
-
},
|
|
6707
|
-
{
|
|
6708
|
-
name: "status",
|
|
6709
|
-
type: "string",
|
|
6710
|
-
description: "Domain status string rendered as a secondary Badge in the header (e.g. 'Online', 'Offline'). Omit to suppress the badge."
|
|
6711
|
-
},
|
|
6712
|
-
{
|
|
6713
|
-
name: "children",
|
|
6714
|
-
type: "ReactNode",
|
|
6715
|
-
required: true,
|
|
6716
|
-
description: "Main body content rendered inside the scrollable <main> region of the frame."
|
|
6717
|
-
},
|
|
6718
|
-
{
|
|
6719
|
-
name: "navItems",
|
|
6720
|
-
type: "MobileFrameNavItem[]",
|
|
6721
|
-
defaultValue: "[]",
|
|
6722
|
-
description: "Array of bottom-navigation tab descriptors. Each item has { label: string; icon: ComponentType<SVGProps<SVGSVGElement>>; active?: boolean }. The footer is only rendered when the array is non-empty. Mark exactly one item active at a time to indicate the current tab."
|
|
6723
|
-
}
|
|
6724
|
-
],
|
|
6725
|
-
usage: [
|
|
6726
|
-
"DO: Use MobileFrame as a full-page layout shell for handheld/PWA screens \u2014 wrap the entire screen content in it, not a subsection of a desktop page.",
|
|
6727
|
-
"DO: Pass lucide-react (or equivalent) icon components directly as `icon` values in navItems; the frame renders them as SVG children and adds aria-hidden automatically.",
|
|
6728
|
-
"DO: Mark the active nav item with `active: true` on exactly one navItems entry; multiple or zero active states are valid but only the first receives the active highlight.",
|
|
6729
|
-
"DO NOT: Nest MobileFrame inside PageContainer or AppShell \u2014 it is an independent shell; mixing shells breaks the layout semantics.",
|
|
6730
|
-
"DO NOT: Hand-roll a phone frame with raw divs, CSS borders and overflow \u2014 MobileFrame already owns the stage/frame/header/main/nav CSS tokens (ui-mobile-stage, ui-mobile-frame, etc.).",
|
|
6731
|
-
"DO NOT: Use MobileFrame for desktop admin pages \u2014 it renders a narrow, phone-sized canvas. Use PageContainer + AppShell for desktop screens."
|
|
6732
|
-
],
|
|
6733
|
-
useCases: [
|
|
6734
|
-
"Warehouse handheld scanners: a picker/packer app where staff scan barcodes on a phone \u2014 title='Tokyo scan', subtitle='Branch handheld', status='Online', navItems for Scan/Receive/Pack/Flight tabs.",
|
|
6735
|
-
"Field-agent PWA screens where the same codebase serves a desktop admin shell (AppShell) and a mobile companion app (MobileFrame) rendered at the phone viewport.",
|
|
6736
|
-
"Storybook/design-review previews of a mobile screen at realistic size \u2014 set Storybook viewport to 'mobile1' and wrap the page in MobileFrame to see the chrome.",
|
|
6737
|
-
"Prototype or demo of a consumer-facing mobile app embedded inside an admin dashboard as a live preview widget.",
|
|
6738
|
-
"Delivery dispatch or order-tracking app rendered on a handheld device where bottom-tab navigation is the primary navigation affordance."
|
|
6739
|
-
],
|
|
6740
|
-
related: [
|
|
6741
|
-
"AppShell \u2014 the desktop counterpart (sidebar + topbar shell); use AppShell for admin/desktop layouts, MobileFrame for handheld/phone layouts. Never nest one inside the other.",
|
|
6742
|
-
"PageContainer \u2014 the page-level content wrapper used inside AppShell for desktop pages; do not substitute for MobileFrame on mobile screens.",
|
|
6743
|
-
"ShellApp \u2014 another desktop shell variant; same rule as AppShell \u2014 mutually exclusive with MobileFrame."
|
|
6744
|
-
],
|
|
6745
|
-
example: `import { Archive, CalendarClock, Package, ScanLine } from "lucide-react";
|
|
6746
|
-
import { MobileFrame, type MobileFrameNavItem } from "@godxjp/ui/layout";
|
|
6747
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
6748
|
-
import { Stack } from "@godxjp/ui/layout";
|
|
6749
|
-
|
|
6750
|
-
const navItems: MobileFrameNavItem[] = [
|
|
6751
|
-
{ label: "Scan", icon: ScanLine, active: true },
|
|
6752
|
-
{ label: "Receive", icon: Archive },
|
|
6753
|
-
{ label: "Pack", icon: Package },
|
|
6754
|
-
{ label: "Schedule", icon: CalendarClock },
|
|
6755
|
-
];
|
|
6756
|
-
|
|
6757
|
-
export default function HandheldScanPage() {
|
|
6758
|
-
return (
|
|
6759
|
-
<MobileFrame
|
|
6760
|
-
title="Tokyo scan"
|
|
6761
|
-
subtitle="Branch handheld"
|
|
6762
|
-
status="Online"
|
|
6763
|
-
navItems={navItems}
|
|
6764
|
-
>
|
|
6765
|
-
<Stack gap="md">
|
|
6766
|
-
<Card>
|
|
6767
|
-
<CardHeader banded>
|
|
6768
|
-
<CardTitle>Last scan</CardTitle>
|
|
6769
|
-
</CardHeader>
|
|
6770
|
-
<CardContent>
|
|
6771
|
-
Ready to scan vendor or internal code.
|
|
6772
|
-
</CardContent>
|
|
6773
|
-
</Card>
|
|
6774
|
-
</Stack>
|
|
6775
|
-
</MobileFrame>
|
|
6776
|
-
);
|
|
6777
|
-
}`,
|
|
6778
|
-
storyPath: "layout/MobileFrame.stories.tsx",
|
|
6779
|
-
rules: [2, 3, 23, 24]
|
|
6780
|
-
},
|
|
6781
6216
|
{
|
|
6782
6217
|
name: "Tooltip",
|
|
6783
6218
|
group: "feedback",
|
|
@@ -7044,7 +6479,7 @@ import { fetchInvoice } from "@/api/invoices";
|
|
|
7044
6479
|
"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.",
|
|
7045
6480
|
"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.",
|
|
7046
6481
|
"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.",
|
|
7047
|
-
"For i18n, pass a translated string as label or
|
|
6482
|
+
"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'."
|
|
7048
6483
|
],
|
|
7049
6484
|
useCases: [
|
|
7050
6485
|
"Toolbar 'Refresh' button on an invoice list page that re-fetches from the server without a full navigation.",
|
|
@@ -7077,6 +6512,118 @@ export function InvoiceListHeader() {
|
|
|
7077
6512
|
}\`}`,
|
|
7078
6513
|
storyPath: "data-display/QueryRefetchButton.stories.tsx",
|
|
7079
6514
|
rules: [3, 5, 6, 13]
|
|
6515
|
+
},
|
|
6516
|
+
{
|
|
6517
|
+
name: "Avatar",
|
|
6518
|
+
group: "data-display",
|
|
6519
|
+
tagline: "Radix Avatar wrapper with image and fallback slots for users, teams, and entities.",
|
|
6520
|
+
props: [
|
|
6521
|
+
{ name: "children", type: "ReactNode", description: "Compose AvatarImage and AvatarFallback." },
|
|
6522
|
+
{ name: "className", type: "string", description: "Extra classes on the avatar root." }
|
|
6523
|
+
],
|
|
6524
|
+
usage: [
|
|
6525
|
+
"DO compose Avatar > AvatarImage + AvatarFallback so broken or missing images still show a readable fallback.",
|
|
6526
|
+
"DON'T use Avatar for decorative thumbnails; use CardCover or an img when the image is content rather than identity."
|
|
6527
|
+
],
|
|
6528
|
+
useCases: ["User profile chips", "Team member lists", "Account owner cells in a DataTable"],
|
|
6529
|
+
related: ["Badge \u2014 use beside Avatar for role/status metadata."],
|
|
6530
|
+
example: `import { Avatar, AvatarFallback, AvatarImage } from "@godxjp/ui/data-display";
|
|
6531
|
+
|
|
6532
|
+
<Avatar>
|
|
6533
|
+
<AvatarImage src="/user.png" alt="User" />
|
|
6534
|
+
<AvatarFallback>UI</AvatarFallback>
|
|
6535
|
+
</Avatar>`,
|
|
6536
|
+
storyPath: "data-display/Avatar.stories.tsx",
|
|
6537
|
+
rules: [3, 35]
|
|
6538
|
+
},
|
|
6539
|
+
{
|
|
6540
|
+
name: "Separator",
|
|
6541
|
+
group: "layout",
|
|
6542
|
+
tagline: "Radix Separator wrapper for tokenized horizontal or vertical dividers.",
|
|
6543
|
+
props: [
|
|
6544
|
+
{ name: "orientation", type: '"horizontal" | "vertical"', defaultValue: '"horizontal"', description: "Divider direction." },
|
|
6545
|
+
{ name: "decorative", type: "boolean", defaultValue: "true", description: "Whether the separator is decorative for assistive tech." }
|
|
6546
|
+
],
|
|
6547
|
+
usage: ["DO use Separator for section dividers instead of raw border divs.", "DO set orientation='vertical' only when the parent gives it a stable height."],
|
|
6548
|
+
useCases: ["Separating toolbar groups", "Dividing stacked page sections", "Vertical split between metadata groups"],
|
|
6549
|
+
related: ["Stack \u2014 use for vertical spacing without a visible rule."],
|
|
6550
|
+
example: `import { Separator } from "@godxjp/ui/layout";
|
|
6551
|
+
|
|
6552
|
+
<Separator />`,
|
|
6553
|
+
storyPath: "layout/Separator.stories.tsx",
|
|
6554
|
+
rules: [2, 3]
|
|
6555
|
+
},
|
|
6556
|
+
{
|
|
6557
|
+
name: "Skeleton",
|
|
6558
|
+
group: "feedback",
|
|
6559
|
+
tagline: "Base pulsing skeleton block for custom loading placeholders.",
|
|
6560
|
+
props: [
|
|
6561
|
+
{ name: "className", type: "string", description: "Size and layout classes for the block." }
|
|
6562
|
+
],
|
|
6563
|
+
usage: ["DO use Skeleton for a custom block when SkeletonRows/Table/Card do not match the final layout.", "DON'T use a spinner overlay for skeletonable page content."],
|
|
6564
|
+
useCases: ["Single loading line", "Custom card media placeholder", "Inline metadata placeholder"],
|
|
6565
|
+
related: ["SkeletonRows", "SkeletonTable", "SkeletonCard"],
|
|
6566
|
+
example: `import { Skeleton } from "@godxjp/ui/feedback";
|
|
6567
|
+
|
|
6568
|
+
<Skeleton className="h-6 w-48" />`,
|
|
6569
|
+
storyPath: "feedback/Skeleton.stories.tsx",
|
|
6570
|
+
rules: [3, 31]
|
|
6571
|
+
},
|
|
6572
|
+
{
|
|
6573
|
+
name: "Toggle",
|
|
6574
|
+
group: "data-entry",
|
|
6575
|
+
tagline: "Radix Toggle wrapper with default/outline variants and tokenized sizes.",
|
|
6576
|
+
props: [
|
|
6577
|
+
{ name: "pressed", type: "boolean", description: "Controlled pressed state." },
|
|
6578
|
+
{ name: "onPressedChange", type: "(pressed: boolean) => void", description: "Pressed-state callback." },
|
|
6579
|
+
{ name: "variant", type: '"default" | "outline"', defaultValue: '"default"', description: "Visual style." },
|
|
6580
|
+
{ name: "size", type: '"sm" | "default" | "lg"', defaultValue: '"default"', description: "Control size." }
|
|
6581
|
+
],
|
|
6582
|
+
usage: ["DO provide an accessible label when the toggle only contains an icon.", "DON'T use Toggle for multi-option selection; use ToggleGroup."],
|
|
6583
|
+
useCases: ["Bold/italic toolbar buttons", "Pinned filter toggles", "Compact view mode buttons"],
|
|
6584
|
+
related: ["ToggleGroup", "Button"],
|
|
6585
|
+
example: `import { Toggle } from "@godxjp/ui/data-entry";
|
|
6586
|
+
|
|
6587
|
+
<Toggle aria-label="Bold">B</Toggle>`,
|
|
6588
|
+
storyPath: "data-entry/Toggle.stories.tsx",
|
|
6589
|
+
rules: [3, 13]
|
|
6590
|
+
},
|
|
6591
|
+
{
|
|
6592
|
+
name: "ToggleGroup",
|
|
6593
|
+
group: "data-entry",
|
|
6594
|
+
tagline: "Radix ToggleGroup wrapper for single or multiple toggle selection.",
|
|
6595
|
+
props: [
|
|
6596
|
+
{ name: "type", type: '"single" | "multiple"', required: true, description: "Selection mode." },
|
|
6597
|
+
{ name: "value", type: "string | string[]", description: "Controlled selected value(s)." },
|
|
6598
|
+
{ name: "onValueChange", type: "(value: string | string[]) => void", description: "Selection callback." }
|
|
6599
|
+
],
|
|
6600
|
+
usage: ["DO choose type='single' for mutually exclusive toolbar modes.", "DO choose type='multiple' for independent formatting toggles."],
|
|
6601
|
+
useCases: ["Text alignment selector", "Formatting toolbar", "View density switcher"],
|
|
6602
|
+
related: ["Toggle", "RadioGroup"],
|
|
6603
|
+
example: `import { ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
|
|
6604
|
+
|
|
6605
|
+
<ToggleGroup type="single">
|
|
6606
|
+
<ToggleGroupItem value="left">Left</ToggleGroupItem>
|
|
6607
|
+
</ToggleGroup>`,
|
|
6608
|
+
storyPath: "data-entry/ToggleGroup.stories.tsx",
|
|
6609
|
+
rules: [3, 13]
|
|
6610
|
+
},
|
|
6611
|
+
{
|
|
6612
|
+
name: "AspectRatio",
|
|
6613
|
+
group: "layout",
|
|
6614
|
+
tagline: "Radix AspectRatio wrapper for stable media and preview frames.",
|
|
6615
|
+
props: [
|
|
6616
|
+
{ name: "ratio", type: "number", defaultValue: "16 / 9", description: "Width divided by height." },
|
|
6617
|
+
{ name: "children", type: "ReactNode", description: "Content constrained to the ratio." }
|
|
6618
|
+
],
|
|
6619
|
+
usage: ["DO use AspectRatio for media, maps, charts, or previews that must not jump during load.", "DON'T use it for unconstrained text content."],
|
|
6620
|
+
useCases: ["Video embed frame", "Image preview slot", "Dashboard chart placeholder"],
|
|
6621
|
+
related: ["CardCover", "Skeleton"],
|
|
6622
|
+
example: `import { AspectRatio } from "@godxjp/ui/layout";
|
|
6623
|
+
|
|
6624
|
+
<AspectRatio ratio={16 / 9}>...</AspectRatio>`,
|
|
6625
|
+
storyPath: "layout/AspectRatio.stories.tsx",
|
|
6626
|
+
rules: [2, 3]
|
|
7080
6627
|
}
|
|
7081
6628
|
];
|
|
7082
6629
|
function findComponent(name) {
|
|
@@ -7090,194 +6637,85 @@ function componentsByGroup(group) {
|
|
|
7090
6637
|
// src/data/prop-vocabulary.ts
|
|
7091
6638
|
var PROP_VOCABULARY = [
|
|
7092
6639
|
{
|
|
7093
|
-
name: "
|
|
7094
|
-
concept: "
|
|
7095
|
-
values: ["
|
|
7096
|
-
usedBy: [
|
|
7097
|
-
"InputSize",
|
|
7098
|
-
"CheckboxGroupSize",
|
|
7099
|
-
"ColorPickerSize",
|
|
7100
|
-
"MediaUploadSize",
|
|
7101
|
-
"ProgressSize",
|
|
7102
|
-
"RadioGroupSize",
|
|
7103
|
-
"RateSize",
|
|
7104
|
-
"TransferSize",
|
|
7105
|
-
"SegmentedControlSize (subset)",
|
|
7106
|
-
"SpaceSize (+ number)",
|
|
7107
|
-
"FlexGap (+ number)",
|
|
7108
|
-
"GridGap (+ number)",
|
|
7109
|
-
"MasonryGap (+ number)"
|
|
7110
|
-
]
|
|
7111
|
-
},
|
|
7112
|
-
{
|
|
7113
|
-
name: "SizeWithXSProp",
|
|
7114
|
-
concept: 'Extension of `SizeProp` with `"x-small"` for compact icon-bar / table-row contexts.',
|
|
7115
|
-
values: ["x-small", "small", "default", "large"],
|
|
7116
|
-
usedBy: ["ButtonSize"]
|
|
7117
|
-
},
|
|
7118
|
-
{
|
|
7119
|
-
name: "IconSizeProp",
|
|
7120
|
-
concept: "Icon-symbol shaped primitives (visual axis = glyph size, not height).",
|
|
7121
|
-
values: ["sm", "md", "lg"],
|
|
7122
|
-
usedBy: ["SpinnerSize", "IconButtonSize"]
|
|
6640
|
+
name: "ValueProp<T = string>",
|
|
6641
|
+
concept: "Abstract controlled value.",
|
|
6642
|
+
values: ["generic"],
|
|
6643
|
+
usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Tabs", "SearchSelect"]
|
|
7123
6644
|
},
|
|
7124
6645
|
{
|
|
7125
|
-
name: "
|
|
7126
|
-
concept: "
|
|
7127
|
-
values: ["
|
|
7128
|
-
usedBy: ["
|
|
7129
|
-
notes: 'Form errors use `"error"` (not `"destructive"`) \u2014 different concern from destructive actions.'
|
|
6646
|
+
name: "DefaultValueProp<T = string>",
|
|
6647
|
+
concept: "Abstract uncontrolled initial value.",
|
|
6648
|
+
values: ["generic"],
|
|
6649
|
+
usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Tabs"]
|
|
7130
6650
|
},
|
|
7131
6651
|
{
|
|
7132
|
-
name: "
|
|
7133
|
-
concept: "
|
|
7134
|
-
values: ["
|
|
7135
|
-
usedBy: ["
|
|
7136
|
-
},
|
|
7137
|
-
{
|
|
7138
|
-
name: "HelpToneProp",
|
|
7139
|
-
concept: "Help-line / Alert colour ladder. Adds `info` + `warn` to StatusProp.",
|
|
7140
|
-
values: ["default", "info", "warn", "error", "success"],
|
|
7141
|
-
usedBy: ["FieldHelpTone"]
|
|
7142
|
-
},
|
|
7143
|
-
{
|
|
7144
|
-
name: "OrientationProp",
|
|
7145
|
-
concept: "Layout axis.",
|
|
7146
|
-
values: ["horizontal", "vertical"],
|
|
7147
|
-
usedBy: [
|
|
7148
|
-
"AnchorOrientation",
|
|
7149
|
-
"MenuOrientation",
|
|
7150
|
-
"RadioGroupOrientation",
|
|
7151
|
-
"CheckboxGroupOrientation",
|
|
7152
|
-
"SegmentedControlOrientation",
|
|
7153
|
-
"StepsOrientation",
|
|
7154
|
-
"TabsOrientation"
|
|
7155
|
-
]
|
|
6652
|
+
name: "OnValueChangeProp<T = string>",
|
|
6653
|
+
concept: "Callback for abstract value changes. DOM events continue to use onChange.",
|
|
6654
|
+
values: ["(value: T) => void"],
|
|
6655
|
+
usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Transfer", "settings pickers"]
|
|
7156
6656
|
},
|
|
7157
6657
|
{
|
|
7158
|
-
name: "
|
|
7159
|
-
concept: "
|
|
7160
|
-
values: ["
|
|
7161
|
-
usedBy: ["
|
|
6658
|
+
name: "OpenProp / DefaultOpenProp / OnOpenChangeProp",
|
|
6659
|
+
concept: "Disclosure state.",
|
|
6660
|
+
values: ["boolean", "(open: boolean) => void"],
|
|
6661
|
+
usedBy: ["Dialog", "Sheet", "Popover"]
|
|
7162
6662
|
},
|
|
7163
6663
|
{
|
|
7164
|
-
name: "
|
|
7165
|
-
concept: "
|
|
7166
|
-
values: ["
|
|
7167
|
-
usedBy: ["
|
|
7168
|
-
|
|
7169
|
-
{
|
|
7170
|
-
name: "PlacementProp",
|
|
7171
|
-
concept: "Extension of SideProp with a centred anchor (Tabs placement, Tour spotlight).",
|
|
7172
|
-
values: ["top", "right", "bottom", "left", "center"],
|
|
7173
|
-
usedBy: ["TourPlacement"]
|
|
7174
|
-
},
|
|
7175
|
-
{
|
|
7176
|
-
name: "PaddingProp",
|
|
7177
|
-
concept: "Outer gutter scale for surface containers.",
|
|
7178
|
-
values: ["tight", "default", "cozy", "none"],
|
|
7179
|
-
usedBy: ["CardPadding", "PageHeaderPadding", "PageContentPadding"]
|
|
6664
|
+
name: "SizeProp",
|
|
6665
|
+
concept: "Shared public size names.",
|
|
6666
|
+
values: ["xs", "sm", "md", "lg"],
|
|
6667
|
+
usedBy: ["Button", "Steps", "Switch"],
|
|
6668
|
+
notes: "Component-specific subsets must be documented. Old alias small is sm."
|
|
7180
6669
|
},
|
|
7181
6670
|
{
|
|
7182
|
-
name: "
|
|
7183
|
-
concept: "
|
|
7184
|
-
values: ["
|
|
7185
|
-
usedBy: ["
|
|
6671
|
+
name: "ToneProp",
|
|
6672
|
+
concept: "Semantic status/color intent.",
|
|
6673
|
+
values: ["default", "success", "warning", "destructive", "info", "muted", "neutral"],
|
|
6674
|
+
usedBy: ["Badge", "Alert"],
|
|
6675
|
+
notes: "Status values belong in tone, not variant."
|
|
7186
6676
|
},
|
|
7187
6677
|
{
|
|
7188
|
-
name: "
|
|
7189
|
-
concept: "
|
|
7190
|
-
values: ["
|
|
7191
|
-
usedBy: ["
|
|
7192
|
-
notes: "
|
|
6678
|
+
name: "GapProp",
|
|
6679
|
+
concept: "Shared layout gap scale.",
|
|
6680
|
+
values: ["xs", "sm", "md", "lg", "xl"],
|
|
6681
|
+
usedBy: ["Stack", "Inline"],
|
|
6682
|
+
notes: "Inline uses an Exclude<GapProp, 'xl'> subset."
|
|
7193
6683
|
},
|
|
7194
6684
|
{
|
|
7195
|
-
name: "
|
|
7196
|
-
concept: "
|
|
7197
|
-
values: ["
|
|
7198
|
-
usedBy: ["
|
|
6685
|
+
name: "TitleProp",
|
|
6686
|
+
concept: "Primary heading text.",
|
|
6687
|
+
values: ["React.ReactNode"],
|
|
6688
|
+
usedBy: ["PageContainer", "PageHeader", "EmptyState", "Dialog"]
|
|
7199
6689
|
},
|
|
7200
6690
|
{
|
|
7201
|
-
name: "
|
|
7202
|
-
concept: "
|
|
7203
|
-
values: ["
|
|
7204
|
-
usedBy: ["
|
|
7205
|
-
notes: 'Cascade: `<Form loading>` sets a default for every nested `<FormField>`. Per-field `loading` overrides Form\'s. `true` \u2192 spinner (default). `{ kind: "skeleton" }` \u2192 use for INITIAL fetch state. UX nuance: skeleton on init, spinner on save.'
|
|
6691
|
+
name: "DensityProp",
|
|
6692
|
+
concept: "Page/subtree density.",
|
|
6693
|
+
values: ["compact", "default", "comfortable"],
|
|
6694
|
+
usedBy: ["PageContainer"]
|
|
7206
6695
|
}
|
|
7207
6696
|
];
|
|
7208
6697
|
function findVocab(name) {
|
|
7209
|
-
const normalized = name.trim().toLowerCase().replace(/prop
|
|
7210
|
-
return PROP_VOCABULARY.find(
|
|
7211
|
-
(v) => v.name.toLowerCase().replace(/prop$/i, "") === normalized
|
|
7212
|
-
);
|
|
6698
|
+
const normalized = name.trim().toLowerCase().replace(/prop(?:<.*>)?$/i, "");
|
|
6699
|
+
return PROP_VOCABULARY.find((v) => v.name.toLowerCase().replace(/prop(?:<.*>)?$/i, "") === normalized);
|
|
7213
6700
|
}
|
|
7214
6701
|
|
|
7215
6702
|
// src/data/tokens.ts
|
|
7216
6703
|
var TOKENS = [
|
|
7217
|
-
|
|
7218
|
-
{ name: "--
|
|
7219
|
-
{ name: "--
|
|
7220
|
-
{ name: "--
|
|
7221
|
-
{ name: "--
|
|
7222
|
-
{ name: "--
|
|
7223
|
-
{ name: "--
|
|
7224
|
-
{ name: "--
|
|
7225
|
-
{ name: "--
|
|
7226
|
-
{ name: "--
|
|
7227
|
-
{ name: "--
|
|
7228
|
-
{ name: "--
|
|
7229
|
-
{ name: "--
|
|
7230
|
-
{ name: "--
|
|
7231
|
-
{ name: "--
|
|
7232
|
-
{ name: "--success", category: "color", role: "Success semantic slot" },
|
|
7233
|
-
{ name: "--warning", category: "color", role: "Warning semantic slot" },
|
|
7234
|
-
{ name: "--destructive", category: "color", role: "Danger / destructive action slot" },
|
|
7235
|
-
{ name: "--info", category: "color", role: "Info / neutral notice slot" },
|
|
7236
|
-
{ name: "--attention", category: "color", role: "Attention / non-destructive alert slot" },
|
|
7237
|
-
// Spacing — fixed scale
|
|
7238
|
-
{ name: "--spacing-1", category: "spacing", role: "4px", value: "0.25rem" },
|
|
7239
|
-
{ name: "--spacing-2", category: "spacing", role: "8px", value: "0.5rem" },
|
|
7240
|
-
{ name: "--spacing-3", category: "spacing", role: "12px", value: "0.75rem" },
|
|
7241
|
-
{ name: "--spacing-4", category: "spacing", role: "16px", value: "1rem" },
|
|
7242
|
-
{ name: "--spacing-5", category: "spacing", role: "20px", value: "1.25rem" },
|
|
7243
|
-
{ name: "--spacing-6", category: "spacing", role: "24px", value: "1.5rem" },
|
|
7244
|
-
{ name: "--spacing-8", category: "spacing", role: "32px", value: "2rem" },
|
|
7245
|
-
// Typography — fixed scale
|
|
7246
|
-
{ name: "--text-2xs", category: "typography", role: "10px", value: "0.625rem" },
|
|
7247
|
-
{ name: "--text-xs", category: "typography", role: "12px", value: "0.75rem" },
|
|
7248
|
-
{ name: "--text-sm", category: "typography", role: "14px", value: "0.875rem" },
|
|
7249
|
-
{ name: "--text-base", category: "typography", role: "16px", value: "1rem" },
|
|
7250
|
-
{ name: "--text-lg", category: "typography", role: "18px", value: "1.125rem" },
|
|
7251
|
-
{ name: "--text-xl", category: "typography", role: "20px", value: "1.25rem" },
|
|
7252
|
-
{ name: "--text-2xl", category: "typography", role: "24px", value: "1.5rem" },
|
|
7253
|
-
{ name: "--font-mono", category: "typography", role: "Monospace stack" },
|
|
7254
|
-
// Radius — fixed scale
|
|
7255
|
-
{ name: "--radius-sm", category: "radius", role: "Small (chips, inputs)", value: "0.25rem" },
|
|
7256
|
-
{ name: "--radius-md", category: "radius", role: "Medium (cards)", value: "0.5rem" },
|
|
7257
|
-
{ name: "--radius-lg", category: "radius", role: "Large (dialogs)", value: "0.75rem" },
|
|
7258
|
-
{ name: "--radius-full", category: "radius", role: "Pill / circle", value: "9999px" },
|
|
7259
|
-
// Breakpoints — mobile-first min-widths
|
|
7260
|
-
{ name: "--breakpoint-xs", category: "breakpoint", role: "Mobile-first base (\u22650px)", value: "0" },
|
|
7261
|
-
{ name: "--breakpoint-sm", category: "breakpoint", role: "Phone landscape / tablet portrait", value: "640px" },
|
|
7262
|
-
{ name: "--breakpoint-md", category: "breakpoint", role: "Tablet landscape", value: "768px" },
|
|
7263
|
-
{ name: "--breakpoint-lg", category: "breakpoint", role: "Laptop", value: "1024px" },
|
|
7264
|
-
{ name: "--breakpoint-xl", category: "breakpoint", role: "Desktop", value: "1280px" },
|
|
7265
|
-
{ name: "--breakpoint-xxl", category: "breakpoint", role: "Wide desktop", value: "1536px" },
|
|
7266
|
-
// Density — rebound by `data-density`
|
|
7267
|
-
{ name: "--density-element", category: "density", role: "Element height (Input/Button)", axis: "data-density" },
|
|
7268
|
-
{ name: "--density-element-sm", category: "density", role: "Small element", axis: "data-density" },
|
|
7269
|
-
{ name: "--density-element-lg", category: "density", role: "Large element", axis: "data-density" },
|
|
7270
|
-
{ name: "--density-card", category: "density", role: "Card padding", axis: "data-density" },
|
|
7271
|
-
{ name: "--density-page", category: "density", role: "Page (PageContent) padding", axis: "data-density" },
|
|
7272
|
-
{ name: "--density-section", category: "density", role: "Section padding (cozy variant)", axis: "data-density" },
|
|
7273
|
-
{ name: "--header-height", category: "density", role: "Topbar height", axis: "data-density" },
|
|
7274
|
-
{ name: "--sidebar-width", category: "density", role: "Sidebar width (expanded)", axis: "data-density" },
|
|
7275
|
-
{ name: "--sidebar-width-collapsed", category: "density", role: "Sidebar icon-only width", axis: "data-density" },
|
|
7276
|
-
{ name: "--touch-target-min", category: "density", role: "Mobile touch target (does NOT scale)", value: "44px" },
|
|
7277
|
-
// Motion — fixed timings
|
|
7278
|
-
{ name: "--transition-base", category: "motion", role: "Standard transition duration", value: "200ms" },
|
|
7279
|
-
{ name: "--ease-out", category: "motion", role: "Out easing curve", value: "cubic-bezier(0, 0, 0.2, 1)" },
|
|
7280
|
-
{ name: "--ease-in-out", category: "motion", role: "In-out easing", value: "cubic-bezier(0.4, 0, 0.2, 1)" }
|
|
6704
|
+
{ name: "--wa-*", category: "primitive", tier: "primitive", role: "Neutral decorative Japanese accent primitives for charts/tags/decoration only." },
|
|
6705
|
+
{ name: "--chart-1..6", category: "primitive", tier: "primitive", role: "Neutral decorative chart primitives; @theme chart colors reference these tokens." },
|
|
6706
|
+
{ name: "--space-0..12", category: "primitive", tier: "primitive", role: "Raw spacing scale." },
|
|
6707
|
+
{ name: "--font-size-*", category: "primitive", tier: "primitive", role: "Raw typography scale." },
|
|
6708
|
+
{ name: "--primary", category: "semantic", tier: "semantic", role: "Brand/action color role." },
|
|
6709
|
+
{ name: "--success", category: "semantic", tier: "semantic", role: "Success status role." },
|
|
6710
|
+
{ name: "--warning", category: "semantic", tier: "semantic", role: "Warning status role." },
|
|
6711
|
+
{ name: "--destructive", category: "semantic", tier: "semantic", role: "Destructive/error status role." },
|
|
6712
|
+
{ name: "--info", category: "semantic", tier: "semantic", role: "Information status role." },
|
|
6713
|
+
{ name: "--attention", category: "semantic", tier: "semantic", role: "Attention status role." },
|
|
6714
|
+
{ name: "--badge-space-*", category: "component", tier: "component", role: "Badge spacing." },
|
|
6715
|
+
{ name: "--card-*", category: "component", tier: "component", role: "Card surface, border, spacing, and typography." },
|
|
6716
|
+
{ name: "--control-*", category: "component", tier: "component", role: "Shared form control heights, padding, icons, and focus chrome." },
|
|
6717
|
+
{ name: "--table-*", category: "component", tier: "component", role: "Table row/cell sizing." },
|
|
6718
|
+
{ name: "--dialog-* / --alert-* / --skeleton-*", category: "component", tier: "component", role: "Feedback component sizing and spacing." }
|
|
7281
6719
|
];
|
|
7282
6720
|
function tokensByCategory(category) {
|
|
7283
6721
|
return TOKENS.filter((t) => t.category === category);
|
|
@@ -7318,9 +6756,9 @@ var CARDINAL_RULES = [
|
|
|
7318
6756
|
{ number: 31, title: "No nested wrapper / convenience primitives", body: "One Radix base = one framework primitive. `<SimpleX>` over `<X>` is forbidden; add a prop to `<X>` instead. Composites under `src/components/composites/` that combine multiple primitives are NOT wrappers." },
|
|
7319
6757
|
{ number: 32, title: "No redundant props", body: "Before adding a prop / item field / variant, grep the existing surface; if a field already covers the concept, use it. Top-level prop that re-expresses an item field (Timeline `pending` \u2194 `items[i].animate`) is rejected." },
|
|
7320
6758
|
{ number: 33, title: "Stories / source / docs name-synchronized", body: "No two names for the same export across the framework surface; no legacy aliases in stories / docs (source may keep an alias for a deprecation cycle, but the marketing surfaces use the canonical name only). Rename PR runs `grep -rn '<oldName>' src docs` and clears it." },
|
|
7321
|
-
{ number: 34, title: "Storybook source panel = real, copy-paste-ready code", body: 'Storybook\'s react-docgen serializer strips every function value (`cell: ({row}) => <JSX/>`, `render: ({field}) => <Input/>`, `rowClassName`, `renderItem`, \u2026) to `() => {}`. Any story whose `render` passes a function-valued prop, references a module-level helper (`
|
|
7322
|
-
{ number: 35, title: "Status chips never wrap", body: "A `
|
|
7323
|
-
{ number: 36, title: "
|
|
6759
|
+
{ number: 34, title: "Storybook source panel = real, copy-paste-ready code", body: 'Storybook\'s react-docgen serializer strips every function value (`cell: ({row}) => <JSX/>`, `render: ({field}) => <Input/>`, `rowClassName`, `renderItem`, \u2026) to `() => {}`. Any story whose `render` passes a function-valued prop, references a module-level helper (`Badge`, `EMPLOYEE_COLUMNS`, etc.), or uses a render-prop pattern MUST override `parameters.docs.source.code` with the literal copy-paste-ready snippet \u2014 type aliases, helper functions spelled out, column definitions with cell JSX visible, inline data array. The `render()` callback stays as-is (module-level constants are fine for runtime performance); `source.code` is the marketing surface. Skip ONLY for stories whose JSX is purely static primitives Storybook can serialize verbatim (`<Button variant="primary">Click</Button>`). The exemplar is `Table.Default` in `src/stories/data-display/Table.stories.tsx`.' },
|
|
6760
|
+
{ number: 35, title: "Status chips never wrap", body: "A `Badge` / `Badge` reads as one atomic unit. Its label must never break across lines \u2014 pin `white-space: nowrap` on the chip (done in `badge-layout.css`), especially inside narrow `DataTable` cells (\u30B9\u30B3\u30FC\u30D7 / \u30B9\u30C6\u30FC\u30BF\u30B9 columns). If a cell is too tight, widen the column or shorten the label; never let the chip wrap." },
|
|
6761
|
+
{ number: 36, title: "Badge tone/icon are the colour escape hatch", body: "`Badge` auto-maps a fixed set of English lifecycle keys (active, draft, pending, scheduled, cancelled, failed, \u2026) to tone + icon. For ANY other value \u2014 localized labels (\u516C\u958B\u4E2D, \u30A2\u30AF\u30C6\u30A3\u30D6) or categorical tiers (\u4F1A\u54E1\u30E9\u30F3\u30AF, \u5951\u7D04\u30D7\u30E9\u30F3) \u2014 pass `tone` explicitly (success | warning | destructive | info | neutral) and, for non-lifecycle tiers, `icon={null}` to drop the misleading glyph. Don't let domain labels fall back to neutral grey + \u25CB. Map domain\u2192tone in the CONSUMER layer; the framework only provides the props." },
|
|
7324
6762
|
{ number: 37, title: "DataTable is full-width \u2014 never inside a narrow grid column", body: "A multi-column `DataTable` occupies its OWN row at the page's full width: `<Card><CardContent flush><DataTable \u2026/></CardContent></Card>`. Never nest it in a `lg:col-span-2` of a `ResponsiveGrid columns={3}` beside a chart \u2014 the columns get squeezed until CJK text collapses to one character per line. Charts / KPI cards go in their own row ABOVE the table. (See the `inertia-list-page` pattern.)" },
|
|
7325
6763
|
{ number: 38, title: "FilterBar stays OUT of CardContent flush", body: "`CardContent flush` strips horizontal padding for edge-to-edge tables. A `FilterBar` placed inside it loses all padding and sticks to the card edge. Render `FilterBar` as a STANDALONE block above the table card; wrap ONLY the `DataTable` / `EmptyState` in the `Card` + `CardContent flush`. Order on a list page: KPIs \u2192 FilterBar \u2192 table card." },
|
|
7326
6764
|
{ number: 39, title: "Long text columns get an explicit width", body: "For columns whose value can be long (name / title / segment / address), set `col.width` to a Tailwind width class (e.g. `w-64`, `w-48`) so the column reserves space instead of shrinking and wrapping to many lines; leave numeric / status columns auto. Table cells default to `white-space: nowrap`, so an over-tight table scrolls horizontally rather than crushing \u2014 give the important columns real widths so the default layout reads well before any scroll." },
|
|
@@ -7334,7 +6772,7 @@ function findRule(num) {
|
|
|
7334
6772
|
var PATTERNS = [
|
|
7335
6773
|
{
|
|
7336
6774
|
name: "common-fixes",
|
|
7337
|
-
tagline: "Fix the most common @godxjp/ui consumer mistakes & visual bugs (
|
|
6775
|
+
tagline: "Fix the most common @godxjp/ui consumer mistakes & visual bugs (StatCard double-border, grey Badge, crushed/empty table headers, washed-out sidebar footer, Inertia layout crash, SSR hydration). Before \u2192 after.",
|
|
7338
6776
|
tags: ["fixes", "migration", "bug", "cardstat", "statusbadge", "datatable", "sidebar", "gotcha", "review"],
|
|
7339
6777
|
code: `// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7340
6778
|
// 0) \u2605 MOST COMMON: <Card> body has NO padding (content is flush against the edges)
|
|
@@ -7350,19 +6788,19 @@ var PATTERNS = [
|
|
|
7350
6788
|
// empty rows \u2192 DataTable's built-in empty / <EmptyState> (not a custom data.length===0 guard).
|
|
7351
6789
|
// If a primitive exists, USE it \u2014 don't reinvent it.
|
|
7352
6790
|
|
|
7353
|
-
// 1)
|
|
7354
|
-
// Cause:
|
|
7355
|
-
// \u274C <Card><CardContent><
|
|
7356
|
-
// \u2705 <ResponsiveGrid columns={4}><
|
|
6791
|
+
// 1) StatCard shows a DOUBLE border (too thick)
|
|
6792
|
+
// Cause: StatCard IS already a bordered Card. Don't wrap it.
|
|
6793
|
+
// \u274C <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>
|
|
6794
|
+
// \u2705 <ResponsiveGrid columns={4}><StatCard label="x" value="1" /></ResponsiveGrid>
|
|
7357
6795
|
// Need a section title? Use a heading, NOT a Card:
|
|
7358
6796
|
// \u2705 <Stack gap="sm"><div className="text-sm font-medium">KPI</div>
|
|
7359
|
-
// <ResponsiveGrid columns={4}><
|
|
6797
|
+
// <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Stack>
|
|
7360
6798
|
|
|
7361
|
-
// 2)
|
|
6799
|
+
// 2) Badge renders grey with a \u25CB (no colour) for localized/tier labels
|
|
7362
6800
|
// Cause: it auto-maps only English lifecycle keys. (@godxjp/ui >= 6.1)
|
|
7363
|
-
// \u274C <
|
|
7364
|
-
// \u2705 <
|
|
7365
|
-
// \u2705 <
|
|
6801
|
+
// \u274C <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" />
|
|
6802
|
+
// \u2705 <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} /> // tier \u2192 pill, no icon
|
|
6803
|
+
// \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
|
|
7366
6804
|
|
|
7367
6805
|
// 3) Table text collapses to one char per line, or a chip wraps
|
|
7368
6806
|
// Cause: pre-6.1.2. (@godxjp/ui >= 6.1.2 \u2192 cells + chips are nowrap)
|
|
@@ -7399,7 +6837,7 @@ var PATTERNS = [
|
|
|
7399
6837
|
|
|
7400
6838
|
// 10) Hide a column on mobile / sign-aware KPI delta (@godxjp/ui >= 6.2.0)
|
|
7401
6839
|
// \u2705 columns: [{ key: "email", header: "\u30E1\u30FC\u30EB", hiddenOnMobile: true }]
|
|
7402
|
-
// \u2705 <
|
|
6840
|
+
// \u2705 <StatCard label="\u58F2\u4E0A" value="\xA58.2M" delta="+12%" /> // + green / - red; inverse flips`
|
|
7403
6841
|
},
|
|
7404
6842
|
{
|
|
7405
6843
|
name: "signup-form",
|
|
@@ -7512,11 +6950,11 @@ export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolea
|
|
|
7512
6950
|
<DialogDescription>\u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002\u78BA\u8A8D\u306E\u305F\u3081\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D "{slug}" \u3068\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002</DialogDescription>
|
|
7513
6951
|
</DialogHeader>
|
|
7514
6952
|
<Stack gap="md">
|
|
7515
|
-
<Input value={confirm}
|
|
6953
|
+
<Input value={confirm} onValueChange={(e) => setConfirm(e.target.value)} placeholder={slug} />
|
|
7516
6954
|
</Stack>
|
|
7517
6955
|
<DialogFooter>
|
|
7518
6956
|
<Button variant="outline" onClick={() => onOpenChange(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
|
|
7519
|
-
<Button
|
|
6957
|
+
<Button tone="destructive" disabled={confirm !== slug} onClick={() => { toast.success("\u524A\u9664\u3057\u307E\u3057\u305F"); onOpenChange(false); }}>\u5B8C\u5168\u306B\u524A\u9664</Button>
|
|
7520
6958
|
</DialogFooter>
|
|
7521
6959
|
</DialogContent>
|
|
7522
6960
|
</Dialog>
|
|
@@ -7553,12 +6991,12 @@ export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
|
|
|
7553
6991
|
},
|
|
7554
6992
|
{
|
|
7555
6993
|
name: "inertia-list-page",
|
|
7556
|
-
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable +
|
|
6994
|
+
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable + Badge + Pagination (current primitive API).",
|
|
7557
6995
|
tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
|
|
7558
6996
|
code: `import { Head, router } from "@inertiajs/react"
|
|
7559
6997
|
import { useMemo, useState } from "react"
|
|
7560
6998
|
import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
|
|
7561
|
-
import { Card, CardContent,
|
|
6999
|
+
import { Card, CardContent, StatCard, DataTable, EmptyState, Badge, type ColumnDef } from "@godxjp/ui/data-display"
|
|
7562
7000
|
import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
|
|
7563
7001
|
import { FilterBar, FilterGroup, Pagination } from "@godxjp/ui/navigation"
|
|
7564
7002
|
import { formatDate } from "@godxjp/ui/datetime"
|
|
@@ -7582,8 +7020,8 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
7582
7020
|
// ColumnDef = { key, header, render?, align?: "left"|"center"|"right", sortable?, width? }
|
|
7583
7021
|
const columns: ColumnDef<Coupon>[] = [
|
|
7584
7022
|
{ key: "name", header: "\u30AF\u30FC\u30DD\u30F3\u540D", render: (c) => <span className="font-medium">{c.name}</span> },
|
|
7585
|
-
{ key: "scope", header: "\u30B9\u30B3\u30FC\u30D7", render: (c) => <
|
|
7586
|
-
{ key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (c) => <
|
|
7023
|
+
{ key: "scope", header: "\u30B9\u30B3\u30FC\u30D7", render: (c) => <Badge status={c.scope} tone="info" icon={null} /> },
|
|
7024
|
+
{ key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (c) => <Badge status={c.status} /> },
|
|
7587
7025
|
{ key: "valid", header: "\u6709\u52B9\u671F\u9593", render: (c) => \`\${formatDate(c.validFrom)} \u301C \${formatDate(c.validTo)}\` },
|
|
7588
7026
|
{ key: "usage", header: "\u5229\u7528\u6570", align: "right", render: (c) => c.usage.toLocaleString() },
|
|
7589
7027
|
]
|
|
@@ -7595,9 +7033,9 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
7595
7033
|
<PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
|
|
7596
7034
|
<Stack gap="lg">
|
|
7597
7035
|
<ResponsiveGrid columns={3}>
|
|
7598
|
-
<
|
|
7599
|
-
<
|
|
7600
|
-
<
|
|
7036
|
+
<StatCard label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
|
|
7037
|
+
<StatCard label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
|
|
7038
|
+
<StatCard label="\u4EF6\u6570" value={coupons.length} />
|
|
7601
7039
|
</ResponsiveGrid>
|
|
7602
7040
|
|
|
7603
7041
|
<FilterBar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
|
|
@@ -7624,7 +7062,7 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
7624
7062
|
</Card>
|
|
7625
7063
|
|
|
7626
7064
|
{filtered.length > PAGE_SIZE && (
|
|
7627
|
-
<Pagination current={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal
|
|
7065
|
+
<Pagination current={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />
|
|
7628
7066
|
)}
|
|
7629
7067
|
</Stack>
|
|
7630
7068
|
</PageContainer>
|
|
@@ -7637,11 +7075,11 @@ export default Coupons`
|
|
|
7637
7075
|
},
|
|
7638
7076
|
{
|
|
7639
7077
|
name: "inertia-detail-page",
|
|
7640
|
-
tagline: "Inertia detail page \u2014 receives {id} prop,
|
|
7078
|
+
tagline: "Inertia detail page \u2014 receives {id} prop, Descriptions (compound) + StatCard + EmptyState fallback.",
|
|
7641
7079
|
tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
|
|
7642
7080
|
code: `import { Head, router } from "@inertiajs/react"
|
|
7643
7081
|
import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
|
|
7644
|
-
import { Card, CardContent,
|
|
7082
|
+
import { Card, CardContent, StatCard, EmptyState, Descriptions, Badge } from "@godxjp/ui/data-display"
|
|
7645
7083
|
import { Button } from "@godxjp/ui/general"
|
|
7646
7084
|
import { formatDate } from "@godxjp/ui/datetime"
|
|
7647
7085
|
import { ArrowLeft } from "lucide-react"
|
|
@@ -7670,20 +7108,20 @@ function MemberShow({ id }: { id: string }) {
|
|
|
7670
7108
|
<PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
|
|
7671
7109
|
<Stack gap="lg">
|
|
7672
7110
|
<ResponsiveGrid columns={4}>
|
|
7673
|
-
<
|
|
7674
|
-
<
|
|
7675
|
-
<
|
|
7676
|
-
<
|
|
7111
|
+
<StatCard label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
|
|
7112
|
+
<StatCard label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
|
|
7113
|
+
<StatCard label="\u30DD\u30A4\u30F3\u30C8" value={member.points.toLocaleString()} />
|
|
7114
|
+
<StatCard label="LTV" value={\`\xA5\${member.ltv.toLocaleString()}\`} />
|
|
7677
7115
|
</ResponsiveGrid>
|
|
7678
7116
|
<Card>
|
|
7679
7117
|
<CardContent>
|
|
7680
|
-
{/*
|
|
7681
|
-
<
|
|
7682
|
-
<
|
|
7683
|
-
<
|
|
7684
|
-
<
|
|
7685
|
-
<
|
|
7686
|
-
</
|
|
7118
|
+
{/* Descriptions is COMPOUND \u2014 value goes in children, not a prop */}
|
|
7119
|
+
<Descriptions columns={2}>
|
|
7120
|
+
<Descriptions.Item label="\u6C0F\u540D">{member.name}</Descriptions.Item>
|
|
7121
|
+
<Descriptions.Item label="\u30E9\u30F3\u30AF"><Badge status={member.rank} tone="info" icon={null} /></Descriptions.Item>
|
|
7122
|
+
<Descriptions.Item label="\u30B9\u30C6\u30FC\u30BF\u30B9"><Badge status={member.status} /></Descriptions.Item>
|
|
7123
|
+
<Descriptions.Item label="\u767B\u9332\u65E5">{formatDate(member.registeredAt)}</Descriptions.Item>
|
|
7124
|
+
</Descriptions>
|
|
7687
7125
|
</CardContent>
|
|
7688
7126
|
</Card>
|
|
7689
7127
|
</Stack>
|
|
@@ -7729,31 +7167,31 @@ export const withCrmLayout = [CrmLayout] // \u2705 array \u2192 Inertia passes
|
|
|
7729
7167
|
const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; return x - Math.floor(x) }`
|
|
7730
7168
|
},
|
|
7731
7169
|
{
|
|
7732
|
-
name: "
|
|
7733
|
-
tagline: "Colour a
|
|
7170
|
+
name: "badge-coloring",
|
|
7171
|
+
tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props, @godxjp/ui \u2265 6.1).",
|
|
7734
7172
|
tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
|
|
7735
|
-
code: `import {
|
|
7173
|
+
code: `import { Badge } from "@godxjp/ui/data-display"
|
|
7736
7174
|
|
|
7737
|
-
//
|
|
7175
|
+
// Badge auto-colours a fixed set of English LIFECYCLE keys:
|
|
7738
7176
|
// active/completed (success \u2713) \xB7 draft (neutral \u25CB) \xB7 pending/temporary (warning \u23F1)
|
|
7739
7177
|
// scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
|
|
7740
7178
|
// Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
|
|
7741
7179
|
|
|
7742
7180
|
// 1) Lifecycle with localized text \u2014 map to the key, keep JP via \`label\` (icon stays):
|
|
7743
|
-
<
|
|
7181
|
+
<Badge status="active">\u516C\u958B\u4E2D</Badge> // green \u2713 \u516C\u958B\u4E2D
|
|
7744
7182
|
|
|
7745
7183
|
// 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
|
|
7746
|
-
<
|
|
7184
|
+
<Badge status="\u516C\u958B\u4E2D" tone="success" />
|
|
7747
7185
|
|
|
7748
7186
|
// 3) Tier / category \u2014 coloured pill, drop the misleading glyph with icon={null}:
|
|
7749
|
-
<
|
|
7750
|
-
<
|
|
7751
|
-
<
|
|
7187
|
+
<Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} />
|
|
7188
|
+
<Badge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
|
|
7189
|
+
<Badge status="\u6CD5\u4EBA\u5171\u901A" tone="info" icon={null} />
|
|
7752
7190
|
|
|
7753
|
-
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type
|
|
7191
|
+
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type BadgeTone)
|
|
7754
7192
|
// RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
|
|
7755
7193
|
// narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
|
|
7756
|
-
// import that instead of the raw
|
|
7194
|
+
// import that instead of the raw Badge across pages.`
|
|
7757
7195
|
}
|
|
7758
7196
|
];
|
|
7759
7197
|
function findPattern(name) {
|
|
@@ -8501,7 +7939,7 @@ content, system-bar safe area. The framework's \`useBreakpoint\`
|
|
|
8501
7939
|
body: `10+ tabs at the top of a screen, no priority. User has to read all
|
|
8502
7940
|
of them to find the right one. AI default: "more tabs = more
|
|
8503
7941
|
features = better".`,
|
|
8504
|
-
fix: `2-4 tabs max. If you have more categories, use a sidebar (
|
|
7942
|
+
fix: `2-4 tabs max. If you have more categories, use a sidebar (Sidebar),
|
|
8505
7943
|
or a Cascader / Tree picker. Tabs are for switching between PEERS
|
|
8506
7944
|
(2-4 mutually exclusive views of the same data).`
|
|
8507
7945
|
},
|