@godxjp/ui-mcp 0.7.0 → 0.9.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 +1179 -752
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59,6 +59,28 @@ var COMPONENTS = [
|
|
|
59
59
|
description: 'Pin footer to viewport bottom on scroll \u2014 pairs with variant="narrow".'
|
|
60
60
|
}
|
|
61
61
|
],
|
|
62
|
+
usage: [
|
|
63
|
+
"DO: Always wrap every page's content in PageContainer \u2014 it is the mandatory page shell. Pass `title` (required, rendered as `<h1>`) for every page; omitting it leaves the page without an accessible heading.",
|
|
64
|
+
"DO: Use the `extra` prop (not a sibling div, not a wrapper) for action buttons or controls that sit right of the title row \u2014 e.g. `extra={<Button>\u65B0\u898F\u4F5C\u6210</Button>}`. Use the `footer` prop for a pinned action bar below the body (e.g. Save/Cancel on a form page); combine with `stickyFooter` to pin it to the viewport bottom on scroll.",
|
|
65
|
+
"DO: Use `variant='flush'` when the page body contains a full-bleed component like DataTable. Inside a flush container, wrap any padded strips (FilterBar, intro text) in `<PageInset>` to align them with the header. Never add manual `px-*` or `p-*` padding to compensate \u2014 use PageInset.",
|
|
66
|
+
"DO: Pass `breadcrumb` as an ordered array of `{ label, to? }` objects from root to current page. The last item is automatically rendered without a link and receives `aria-current='page'`; earlier items with `to` become router `<Link>` elements. Never hand-roll a breadcrumb nav inside a PageContainer.",
|
|
67
|
+
"DON'T: Use `density` to change individual control sizes \u2014 it cascades spacing across the entire page subtree. Set it once per page (e.g. `density='compact'` for data-dense list pages) and let all child components inherit it. Do not apply density classes manually.",
|
|
68
|
+
"DON'T: Confuse PageContainer's prop names with the deprecated PageHeader's prop names \u2014 PageContainer uses `subtitle` (not `description`) and `extra` (not `actions`). If you see those legacy names in old code, it is PageHeader, not PageContainer."
|
|
69
|
+
],
|
|
70
|
+
useCases: [
|
|
71
|
+
"A master list page (e.g. invoices, journal entries, customers) where the header holds the page title, a 'New Invoice' button in `extra`, a breadcrumb trail, and a full-bleed DataTable as the body \u2014 use `variant='flush'` + `<PageInset>` for the FilterBar above the table.",
|
|
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
|
+
"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/StatCard sections beneath the page title.",
|
|
75
|
+
"Any deep-nav page in a multi-level admin (e.g. Accounting > Ledger > Journal Entry #42) where a 3-segment breadcrumb trail provides back-navigation without browser history dependence.",
|
|
76
|
+
"A high-density data reconciliation page where an analyst needs to see maximum rows \u2014 use `density='compact'` to tighten all spacing across the DataTable, FilterBar, and controls in a single prop."
|
|
77
|
+
],
|
|
78
|
+
related: [
|
|
79
|
+
"PageInset \u2014 use INSIDE a `variant='flush'` PageContainer to re-introduce horizontal padding for strips like FilterBar or intro text that should align with the page header, while the surrounding DataTable stays full-bleed. Not a standalone page shell.",
|
|
80
|
+
"PageHeader \u2014 DEPRECATED header-only predecessor to PageContainer. Has no `children`, `footer`, `variant`, `density`, or `stickyFooter` props. Use its prop-name aliases (`description` \u2192 `subtitle`, `actions` \u2192 `extra`) only when reading legacy code. Always prefer PageContainer for new pages.",
|
|
81
|
+
"AppShell \u2014 the outer shell that owns the sidebar/topbar layout grid; PageContainer lives inside AppShell's `children` slot. Do not put AppShell inside PageContainer \u2014 the nesting order is AppShell \u2192 PageContainer.",
|
|
82
|
+
"SplitPane \u2014 use instead of PageContainer when the page body needs a fixed-width aside panel alongside main content (e.g. a detail drawer next to a list). PageContainer has no aside slot; SplitPane fills that gap and can itself be placed inside PageContainer's children."
|
|
83
|
+
],
|
|
62
84
|
example: `import { PageContainer, Stack } from "@godxjp/ui/layout";
|
|
63
85
|
import { Button } from "@godxjp/ui/general";
|
|
64
86
|
|
|
@@ -91,6 +113,28 @@ export default function OrdersPage() {
|
|
|
91
113
|
{ name: "className", type: "string", description: "Extra classes merged via cn()." },
|
|
92
114
|
{ name: "children", type: "ReactNode", description: "Block-level children to stack." }
|
|
93
115
|
],
|
|
116
|
+
usage: [
|
|
117
|
+
'DO use `<Stack gap="lg">` (or sm/md/xl/xs) to separate major page sections \u2014 KPI row, FilterBar, table card \u2014 instead of writing `space-y-*` or `flex flex-col gap-*` utilities on a raw div. Token gap values (`xs`|`sm`|`md`|`lg`|`xl`) map to CSS custom properties; raw Tailwind spacing utilities (`space-y-4`) are forbidden on page layouts (rule 40).',
|
|
118
|
+
"DO pass `className` only for structural overrides (e.g., `w-full`, `min-h-0`) \u2014 NEVER to smuggle spacing utilities like `p-4` or `gap-6` that duplicate what the `gap` prop already does. Every spacing value must trace back to a design token.",
|
|
119
|
+
'DO NOT use Stack for horizontal arrangements \u2014 that is `Inline`. Stack is vertical only (`flex-col`). Mixing `className="flex-row"` on a Stack to force horizontal is wrong; use `Inline` instead.',
|
|
120
|
+
'DO NOT nest multiple bare Stack wrappers just to get different gaps \u2014 compose them: a top-level `<Stack gap="xl">` for page sections, an inner `<Stack gap="sm">` for tightly-related fields. Each level should correspond to a real semantic grouping.',
|
|
121
|
+
'DO import from `@godxjp/ui/layout` (not the root `@godxjp/ui` barrel) \u2014 `import { Stack } from "@godxjp/ui/layout"`.',
|
|
122
|
+
"Stack is a plain `<div>` with `React.HTMLAttributes<HTMLDivElement>` \u2014 all standard HTML div props (`data-*`, `id`, `aria-*`, `role`) pass through. There is no controlled/uncontrolled state and no form submission role; use `FormField` for form layout concerns."
|
|
123
|
+
],
|
|
124
|
+
useCases: [
|
|
125
|
+
'Top-level page body: wrap KPI `ResponsiveGrid`, standalone `FilterBar`, and a table `Card` in a `<Stack gap="lg">` \u2014 this is the canonical inertia-list-page structure (rule 38 + rule 40).',
|
|
126
|
+
"Detail/form pages: stack a `PageHeader` (or `PageContainer` header), a facts `Card` with `Descriptions`, and an action `Card` with `FormField` rows, each separated by a semantically meaningful gap level.",
|
|
127
|
+
'Card body with multiple related fields: use `<Stack gap="sm">` inside `<CardContent>` to separate labelled input rows without resorting to `space-y-2` utilities.',
|
|
128
|
+
'Dashboard shells: `<Stack gap="xl">` as the page root, containing a `<ResponsiveGrid columns={4}>` of `StatCard` items followed by chart cards and a table card \u2014 each row is a Stack child.',
|
|
129
|
+
'Modal/Sheet interiors: `<Stack gap="md">` inside `<Dialog>` or `<Sheet>` content area to separate sections (description, form, preview) with consistent token spacing.',
|
|
130
|
+
'Empty/loading placeholder layout: wrap `<SkeletonTable>` or `<DataState>` in a `<Stack gap="md">` alongside a header block so the skeleton occupies the same vertical rhythm as the loaded page.'
|
|
131
|
+
],
|
|
132
|
+
related: [
|
|
133
|
+
"Inline \u2014 horizontal counterpart; use for button groups, icon+label rows, badge clusters. When children should sit side-by-side, use Inline. When they should stack top-to-bottom, use Stack. Never use Stack with a flex-row className override.",
|
|
134
|
+
"ResponsiveGrid \u2014 use when children are card-shaped items that should tile into 2/3/4 columns on desktop and collapse to 1 column on mobile (e.g., StatCard KPI rows). Stack does not do multi-column layouts.",
|
|
135
|
+
"PageContainer \u2014 outer page wrapper that provides title, breadcrumb, and padding context. Stack is the layout primitive used INSIDE PageContainer's children area, not a replacement for it.",
|
|
136
|
+
"CardContent \u2014 for spacing inside a Card, always use CardContent (which adds consistent padding); don't wrap Card body in a bare Stack with padding classes. A Stack inside CardContent is correct when you need multiple vertically spaced sections within the card body."
|
|
137
|
+
],
|
|
94
138
|
example: `import { Stack } from "@godxjp/ui/layout";
|
|
95
139
|
|
|
96
140
|
<Stack gap="lg">
|
|
@@ -115,6 +159,28 @@ export default function OrdersPage() {
|
|
|
115
159
|
{ name: "className", type: "string", description: "Extra classes merged via cn()." },
|
|
116
160
|
{ name: "children", type: "ReactNode", description: "Inline children in a row." }
|
|
117
161
|
],
|
|
162
|
+
usage: [
|
|
163
|
+
'DO use `<Inline gap="sm">` instead of hand-rolling `flex gap-2 items-center` \u2014 the component bakes in `display:flex; flex-wrap:wrap; align-items:center` via the token class `ui-inline-*`, so you get consistent spacing and wrapping behavior without raw Tailwind utilities (rule 2).',
|
|
164
|
+
'DO pass extra layout modifiers through `className` (e.g. `className="justify-between"` to push items to opposite ends, or `className="flex-nowrap"` to prevent wrapping when overflow must be clipped) \u2014 `Inline` spreads all HTML div attributes, so className is merged via `cn()`.',
|
|
165
|
+
"DON'T use `Inline` for vertical stacking \u2014 it is always a row. For vertical spacing between block elements use `Stack` instead. The most common mistake is reaching for `Inline` when the children should stack, or reaching for `Stack` when the children should sit side-by-side.",
|
|
166
|
+
"DON'T nest an `Inline` just to group icon + label inside a `Button` \u2014 Button already handles its own internal row layout. Use `Inline` at the call-site level to space multiple sibling Buttons or controls apart.",
|
|
167
|
+
'DO use `gap="xs"` for tight icon+label pairs (e.g. flag + country name in CountrySelect) and `gap="sm"` (default) for button groups and toolbar-level groupings. `gap="md"` / `gap="lg"` suit section-header clusters with more breathing room.',
|
|
168
|
+
"NOTE: `Inline` has no alignment, justify, or wrap props beyond `gap` \u2014 any extra layout intent must come through `className`. There is no controlled/form-submission aspect; it is a pure layout shell with no ARIA role of its own."
|
|
169
|
+
],
|
|
170
|
+
useCases: [
|
|
171
|
+
"Action toolbar: grouping a primary Button and a secondary outline Button side-by-side at the bottom of a form or dialog (the catalog example shows exactly this).",
|
|
172
|
+
"Table/list toolbar: placing a `SearchInput` next to a `FilterBar` trigger or a `DateRangePicker` at the top of a DataTable page, so controls sit in a wrapping row with consistent gap.",
|
|
173
|
+
"Status chip row: rendering a cluster of `Badge` or `Badge` components inline (e.g. invoice tags: Paid + Overdue + Draft) without writing ad-hoc flex classes.",
|
|
174
|
+
'Icon + text label pair: wrapping a flag icon and country name in `CountrySelect`, or a lucide icon and a span, keeping them vertically centered with a tight `gap="xs"`.',
|
|
175
|
+
'Card header actions: grouping a `Button variant="ghost"` edit action and a `DropdownMenu` trigger on the right side of a `CardHeader`, using `className="justify-end"` on the Inline.',
|
|
176
|
+
'Alert action row: pairing two action links/buttons inside an `Alert` body (the godx-ui Alert component itself uses `<Inline gap="xs">` internally for its action cluster).'
|
|
177
|
+
],
|
|
178
|
+
related: [
|
|
179
|
+
"Stack \u2014 use Stack when children should be arranged vertically (column direction). Inline is horizontal/row; Stack is vertical/column. They are the two axis-specific layout primitives and are often composed together (Stack of Inlines).",
|
|
180
|
+
"PageInset \u2014 use PageInset when you need a padded horizontal strip inside a flush PageContainer (e.g. for FilterBar or intro text above a full-bleed DataTable). PageInset adds top/side padding to a section; Inline only arranges children in a row with a gap.",
|
|
181
|
+
"FilterBar \u2014 use FilterBar (with FilterGroup) when the horizontal group of controls is a page-level filter row that semantically belongs together and may include responsive collapse behaviour. Use Inline for ad-hoc groupings of buttons or badges that don't need filter semantics.",
|
|
182
|
+
"ResponsiveGrid \u2014 use ResponsiveGrid when you need a multi-column grid of cards that collapses on mobile. Use Inline when children must stay in a single wrapping row at all breakpoints without explicit column counts."
|
|
183
|
+
],
|
|
118
184
|
example: `import { Inline } from "@godxjp/ui/layout";
|
|
119
185
|
import { Button } from "@godxjp/ui/general";
|
|
120
186
|
|
|
@@ -140,17 +206,39 @@ import { Button } from "@godxjp/ui/general";
|
|
|
140
206
|
name: "children",
|
|
141
207
|
type: "ReactNode",
|
|
142
208
|
required: true,
|
|
143
|
-
description: "Grid items \u2014 typically Card or
|
|
209
|
+
description: "Grid items \u2014 typically Card or StatCard."
|
|
144
210
|
}
|
|
145
211
|
],
|
|
212
|
+
usage: [
|
|
213
|
+
"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
|
+
"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
|
+
"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
|
+
"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 StatCard tiles while KPIs are loading \u2014 same columns prop, same count as the real tiles. Switch to real StatCard once data resolves.",
|
|
218
|
+
"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
|
+
],
|
|
220
|
+
useCases: [
|
|
221
|
+
"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.",
|
|
222
|
+
"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.",
|
|
223
|
+
"Accounting period overview: 4 StatCard tiles (opening balance, total debits, total credits, closing balance) that collapse gracefully on narrow viewports without any custom CSS.",
|
|
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 StatCard tiles once resolved.",
|
|
225
|
+
"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
|
+
"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
|
+
],
|
|
228
|
+
related: [
|
|
229
|
+
"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
|
+
"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
|
+
"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.",
|
|
232
|
+
"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
|
+
],
|
|
146
234
|
example: `import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
147
|
-
import {
|
|
235
|
+
import { StatCard } from "@godxjp/ui/data-display";
|
|
148
236
|
|
|
149
237
|
<ResponsiveGrid columns={4}>
|
|
150
|
-
<
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
<
|
|
238
|
+
<StatCard label="\u7DCF\u4F1A\u54E1\u6570" value="12,400" />
|
|
239
|
+
<StatCard label="\u516C\u958B\u4E2D\u30AF\u30FC\u30DD\u30F3" value="8" />
|
|
240
|
+
<StatCard label="\u6708\u9593\u5229\u7528\u6570" value="3,210" />
|
|
241
|
+
<StatCard label="\u5272\u5F15\u7DCF\u984D" value="\xA5480,000" />
|
|
154
242
|
</ResponsiveGrid>`,
|
|
155
243
|
storyPath: "layout/ResponsiveGrid.stories.tsx",
|
|
156
244
|
rules: [24, 40]
|
|
@@ -204,6 +292,28 @@ import { CardStat } from "@godxjp/ui/data-display";
|
|
|
204
292
|
description: "App-level footer outside the main content area."
|
|
205
293
|
}
|
|
206
294
|
],
|
|
295
|
+
usage: [
|
|
296
|
+
"DO pass a <Sidebar> node to `sidebar` (required) and page content to `children` (required) \u2014 these are the only two required props. Everything else is optional and omitting optional slots simply removes that zone from the rendered DOM.",
|
|
297
|
+
"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
|
+
"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
|
+
"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 AppShell inside AppShell's children \u2014 AppShell renders the root `app-root` div; nesting shells breaks the CSS grid layout.",
|
|
301
|
+
"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
|
+
],
|
|
303
|
+
useCases: [
|
|
304
|
+
"Full admin SPA shell: AppShell wraps a <Sidebar> nav rail and a <Topbar> (with productMenu entity-switcher, onSearchOpen, onNotificationsOpen, user avatar) and every Inertia page renders as children inside a <PageContainer>.",
|
|
305
|
+
"Collapsible-sidebar layout: maintain a `collapsed` boolean in a persistent Inertia layout component, pass it to both AppShell's `sidebarCollapsed` and Sidebar's `collapsed`, wire Topbar's `onToggleCollapsed` to flip it \u2014 AppShell handles the CSS transition automatically.",
|
|
306
|
+
"Multi-tenant accounting app: pass a <Topbar productMenu={<DropdownMenuContent>\u2026</DropdownMenuContent>}> to AppShell's `topbar` slot so the legal-entity chip opens an inline switcher without a modal.",
|
|
307
|
+
"App-level footer (e.g. version/build info, compliance notice): pass a <footer> node to AppShell's `footer` prop \u2014 it renders outside `<main>` so it stays pinned below the scroll area.",
|
|
308
|
+
"Rapid prototype or internal tool where you want a branded shell with minimal topbar: skip the `topbar` prop entirely and use `logo`, `topbarLeft`, `topbarRight` to build the rail declaratively without instantiating <Topbar>.",
|
|
309
|
+
"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
|
+
],
|
|
311
|
+
related: [
|
|
312
|
+
"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
|
+
"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
|
+
"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
|
+
"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."
|
|
316
|
+
],
|
|
207
317
|
example: `import { AppShell, Sidebar } from "@godxjp/ui/layout";
|
|
208
318
|
import { LayoutDashboard, Users } from "lucide-react";
|
|
209
319
|
import { router } from "@inertiajs/react";
|
|
@@ -220,7 +330,7 @@ const sidebar = (
|
|
|
220
330
|
/>
|
|
221
331
|
);
|
|
222
332
|
|
|
223
|
-
export function CrmLayout({ children }: {
|
|
333
|
+
export function CrmLayout({ children }: { content: React.ReactNode }) {
|
|
224
334
|
return <AppShell sidebar={sidebar}>{children}</AppShell>;
|
|
225
335
|
}`,
|
|
226
336
|
storyPath: "layout/AppShell.stories.tsx",
|
|
@@ -277,7 +387,7 @@ export function CrmLayout({ children }: { children: React.ReactNode }) {
|
|
|
277
387
|
],
|
|
278
388
|
usage: [
|
|
279
389
|
"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.",
|
|
280
|
-
"DO: Add
|
|
390
|
+
"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.",
|
|
281
391
|
"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.",
|
|
282
392
|
"DO: Use the footer prop for user info or status \u2014 it is pinned below the scroll area and does not scroll away.",
|
|
283
393
|
"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.",
|
|
@@ -311,7 +421,7 @@ const sections: SidebarSection[] = [
|
|
|
311
421
|
id: "ledger",
|
|
312
422
|
label: "Ledger",
|
|
313
423
|
icon: BookOpen,
|
|
314
|
-
|
|
424
|
+
content: [
|
|
315
425
|
{ id: "journal", label: "Journal", icon: FileText },
|
|
316
426
|
{ id: "chart-of-accounts", label: "Chart of Accounts", icon: CreditCard },
|
|
317
427
|
],
|
|
@@ -370,7 +480,7 @@ export default function Shell() {
|
|
|
370
480
|
{
|
|
371
481
|
name: "Topbar",
|
|
372
482
|
group: "layout",
|
|
373
|
-
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
|
|
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 projectSidebar is set.",
|
|
374
484
|
props: [
|
|
375
485
|
{
|
|
376
486
|
name: "product",
|
|
@@ -477,7 +587,7 @@ export default function Shell() {
|
|
|
477
587
|
"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.",
|
|
478
588
|
"Sidebar \u2014 the companion left-rail nav; pair with Topbar's `collapsed`/`onToggleCollapsed` to keep sidebar and topbar toggle state in sync.",
|
|
479
589
|
"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.",
|
|
480
|
-
"
|
|
590
|
+
"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."
|
|
481
591
|
],
|
|
482
592
|
example: `import { Topbar } from "@godxjp/ui/layout";
|
|
483
593
|
import { AppShell } from "@godxjp/ui/layout";
|
|
@@ -488,7 +598,7 @@ import {
|
|
|
488
598
|
|
|
489
599
|
// Entity-switcher example: product chip opens an entity dropdown,
|
|
490
600
|
// project chip is hidden (no project concept in this app).
|
|
491
|
-
function MyShell({ children }: {
|
|
601
|
+
function MyShell({ children }: { content: React.ReactNode }) {
|
|
492
602
|
const [collapsed, setCollapsed] = React.useState(false);
|
|
493
603
|
const [unread, setUnread] = React.useState(true);
|
|
494
604
|
|
|
@@ -537,6 +647,28 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
537
647
|
},
|
|
538
648
|
{ name: "className", type: "string", description: "Extra classes." }
|
|
539
649
|
],
|
|
650
|
+
usage: [
|
|
651
|
+
'DO use PageInset exclusively inside a `<PageContainer variant="flush">` \u2014 flush strips the left/right padding from the container body, and PageInset re-applies the exact same `--space-page-active-x` token so content aligns pixel-for-pixel with the page header and footer. Outside of a flush container it doubles the padding.',
|
|
652
|
+
"DO NOT use PageInset to add padding inside a standard (non-flush) PageContainer \u2014 the container body already has `--space-page-active-x` padding on both sides; wrapping content in PageInset produces doubled inset.",
|
|
653
|
+
'DO NOT hand-roll the padding with `className="px-6"` or `className="px-page"` \u2014 those values are not guaranteed to match the design token. PageInset is the only correct way to reproduce the page-header alignment inside a flush body.',
|
|
654
|
+
"DO place PageInset as a direct child of PageContainer's body (before or between full-bleed children such as DataTable or Table) \u2014 it is a plain `<div>` that wraps one cohesive strip (e.g., a FilterBar, a summary intro, or a quick-action toolbar) and does not provide vertical spacing itself.",
|
|
655
|
+
"DO accept all standard HTMLDivElement attributes (id, aria-*, data-*, style) via the spread \u2014 PageInsetProp extends React.HTMLAttributes<HTMLDivElement>, so accessibility attributes and test hooks pass through without extra wrappers.",
|
|
656
|
+
"COMMON MISTAKE: adding a PageInset around a DataTable inside a flush PageContainer \u2014 DataTable must remain full-bleed (no PageInset) so its borders reach the edges; only the filter/intro strips above or below the table need PageInset."
|
|
657
|
+
],
|
|
658
|
+
useCases: [
|
|
659
|
+
"Flush list page with a FilterBar above a full-bleed DataTable: wrap the FilterBar (or FilterBar + FilterGroup) in PageInset so it aligns with the page title, while DataTable sits edge-to-edge beneath it.",
|
|
660
|
+
"Invoice or transaction index where a SearchInput + date range picker toolbar sits above a borderless table \u2014 PageInset keeps the toolbar inset-aligned without breaking the table's flush left/right edges.",
|
|
661
|
+
"Dashboard section inside a flush PageContainer that shows a brief intro paragraph or status summary strip before a full-width chart or DataTable.",
|
|
662
|
+
"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
|
+
'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 Descriptions strip needs the same left-edge as the page header while the entry rows below are full-bleed."
|
|
665
|
+
],
|
|
666
|
+
related: [
|
|
667
|
+
'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).',
|
|
668
|
+
"FilterBar \u2014 the most common direct child of PageInset; FilterBar itself has no page-level padding, so always wrap it in PageInset when inside a flush PageContainer.",
|
|
669
|
+
'CardContent \u2014 serves a similar "add-the-missing-padding" role but inside a Card, not a flush PageContainer. Use CardContent to pad content inside a Card; use PageInset to pad content inside a flush page body.',
|
|
670
|
+
"Stack \u2014 a vertical-spacing primitive that can wrap multiple PageInset strips but has no padding of its own; Stack and PageInset compose orthogonally (Stack for gaps, PageInset for horizontal alignment)."
|
|
671
|
+
],
|
|
540
672
|
example: `import { PageContainer, PageInset } from "@godxjp/ui/layout";
|
|
541
673
|
|
|
542
674
|
<PageContainer title="\u5546\u54C1\u4E00\u89A7" variant="flush">
|
|
@@ -565,6 +697,27 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
565
697
|
description: "Width preset for the aside column."
|
|
566
698
|
}
|
|
567
699
|
],
|
|
700
|
+
usage: [
|
|
701
|
+
"DO: pass all right-panel content via the `aside` prop \u2014 it renders inside a semantic `<aside>` element at a fixed rem width (sm=20rem, md=22rem). The `children` prop fills the main `1fr` column. Both accept any ReactNode.",
|
|
702
|
+
'DO: choose `asideWidth="sm"` for compact detail panels (filters, quick stats, key-value summaries) and the default `asideWidth="md"` for richer panels (forms, timelines, long metadata lists).',
|
|
703
|
+
"DO: wrap SplitPane inside `PageContainer` or `PageInset` \u2014 SplitPane provides no page padding of its own. It is a grid primitive, not a page scaffold.",
|
|
704
|
+
"DON'T: expect two columns below 1080px. Below that breakpoint SplitPane stacks to a single column (main on top, aside below). Never use it for layouts that must remain side-by-side on tablet or mobile \u2014 use CSS Grid or `ResponsiveGrid` instead.",
|
|
705
|
+
"DON'T: add a CSS `overflow: hidden` or fixed height on the SplitPane wrapper; both columns carry `min-width: 0` to handle overflow correctly, and the grid uses `minmax(0, 1fr)` \u2014 adding external constraints will break the overflow contract.",
|
|
706
|
+
"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
|
+
],
|
|
708
|
+
useCases: [
|
|
709
|
+
"Invoice / transaction detail page: list of records in `children` (DataTable), selected-record detail panel in `aside` (Descriptions + Timeline).",
|
|
710
|
+
'Accounting ledger drill-down: account list on the left, chart-of-accounts metadata or running balance breakdown on the right using `asideWidth="sm"`.',
|
|
711
|
+
"Document review workflow: PDF or rich-text viewer in `children`, approval form or annotation panel in `aside`.",
|
|
712
|
+
"Settings page with a category list or Steps navigator in `children` and a live preview or summary card in `aside`.",
|
|
713
|
+
"Kanban or task board where the main area holds the board columns and the aside shows the focused task detail without navigating away."
|
|
714
|
+
],
|
|
715
|
+
related: [
|
|
716
|
+
"ResponsiveGrid \u2014 use when you need more than two columns, or when both columns must have equal or percentage-based widths rather than a fixed-rem aside. SplitPane always gives main a `1fr` and aside a fixed rem width.",
|
|
717
|
+
"PageContainer \u2014 use as the outer scaffold that provides page padding and vertical rhythm; nest SplitPane inside PageContainer, not the other way around.",
|
|
718
|
+
"Sheet \u2014 use when the detail/context panel should slide in as an overlay (drawer) rather than sitting permanently beside the main content. Prefer Sheet on mobile or when the aside content is secondary and on-demand.",
|
|
719
|
+
"Stack \u2014 use when content is purely vertical (single column, sequential sections). SplitPane is the right pick only when a persistent side panel is needed at the same hierarchy level as the main content."
|
|
720
|
+
],
|
|
568
721
|
example: `import { SplitPane } from "@godxjp/ui/layout";
|
|
569
722
|
|
|
570
723
|
<SplitPane aside={<DetailPanel />} asideWidth="sm">
|
|
@@ -585,6 +738,28 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
585
738
|
description: "Array of { label, to? } \u2014 omit `to` on the last (current) segment."
|
|
586
739
|
}
|
|
587
740
|
],
|
|
741
|
+
usage: [
|
|
742
|
+
"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
|
+
'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 `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
|
+
'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
|
+
"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
|
+
"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
|
+
],
|
|
749
|
+
useCases: [
|
|
750
|
+
"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 `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
|
+
"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
|
+
"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
|
+
"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.",
|
|
755
|
+
"Prefetch pairing: wrap ancestor segments' `to` values with `PrefetchLink` semantics by putting them in `items` \u2014 each non-last item with `to` is already rendered as a router `<Link>`, so hovering naturally prefetches if `PrefetchLink` is used elsewhere on the same route."
|
|
756
|
+
],
|
|
757
|
+
related: [
|
|
758
|
+
"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
|
+
"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
|
+
"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
|
+
"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
|
+
],
|
|
588
763
|
example: `import { Breadcrumb } from "@godxjp/ui/layout";
|
|
589
764
|
|
|
590
765
|
<Breadcrumb items={[
|
|
@@ -626,6 +801,28 @@ function MyShell({ children }: { children: React.ReactNode }) {
|
|
|
626
801
|
description: "Click handler."
|
|
627
802
|
}
|
|
628
803
|
],
|
|
804
|
+
usage: [
|
|
805
|
+
"DO pick the right variant for intent: `default` (primary CTA, one per section), `destructive` (irreversible actions like delete/revoke), `outline` (secondary actions alongside a primary), `secondary` (less prominent actions), `ghost` (toolbar icon-only actions), `link` (inline text-style navigation without an underline by default).",
|
|
806
|
+
"DO use icon-only sizes (`icon`, `icon-xs`, `icon-sm`, `icon-lg`) exclusively for buttons that contain only an SVG \u2014 these sizes set equal width/height. For text+icon buttons use `default|sm|lg|xs` sizes; icons inside are auto-sized to 1rem via `[&_svg:not([class*='size-'])]:size-4`.",
|
|
807
|
+
"DO use `asChild` to render the button as a React Router/Inertia `<Link>` or native `<a>` while keeping all button styling and a11y: `<Button asChild variant=\"outline\"><Link href={route('invoices.show', id)}>\u8A73\u7D30</Link></Button>`. Never wrap a `<button>` around an `<a>` \u2014 that is invalid HTML.",
|
|
808
|
+
"DON'T use raw `<button>` elements anywhere in the UI \u2014 always use this `Button`. The only exception is an `aria-hidden` native control used as an e2e/a11y hook paired with a visible godx-ui control.",
|
|
809
|
+
'DO set `type="submit"` explicitly on form submit buttons (the default HTML button type inside `<form>` is already `submit`, but being explicit prevents accidental double-submissions when a `type="button"` sibling exists). For cancel/reset actions set `type="button"` to avoid accidental form submission.',
|
|
810
|
+
"DON'T apply raw padding, height, or `rounded-*` overrides to `Button` via `className` \u2014 the size variants encode the full box model. If a custom size is truly needed, use `buttonVariants` from `@godxjp/ui/general` to compose a new cva class rather than fighting the existing ones."
|
|
811
|
+
],
|
|
812
|
+
useCases: [
|
|
813
|
+
'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 `variant="destructive"` Button as the confirm action and `variant="outline"` as Cancel; never use `variant="default"` for a delete action.',
|
|
815
|
+
'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
|
+
"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
|
+
"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.",
|
|
818
|
+
"Refetch / retry trigger when NOT using TanStack Query \u2014 for manual cache refresh inside a TanStack Query context use `QueryRefetchButton` instead, which owns its own `disabled`/`onClick` lifecycle."
|
|
819
|
+
],
|
|
820
|
+
related: [
|
|
821
|
+
"DropdownMenu \u2014 when a button needs to reveal a list of actions (e.g. 'Actions \u25BE' in a DataTable row), wrap the Button as a `DropdownMenuTrigger` inside a `DropdownMenu` compound; don't open a Sheet/Dialog just to show a list of options.",
|
|
822
|
+
"QueryRefetchButton \u2014 a pre-wired Button variant from `@godxjp/ui/query` that binds directly to a TanStack Query result (shows spinner, auto-disables while fetching, retries on click). Use it instead of a raw Button whenever the action is a query refetch \u2014 do not pass `onClick`/`disabled` to it manually.",
|
|
823
|
+
"MutationFeedback \u2014 for surfacing mutation errors and a retry action; it renders its own retry Button internally. Do not add a separate Button alongside MutationFeedback for the same mutation.",
|
|
824
|
+
"PrefetchLink \u2014 use when the goal is purely navigation with hover-prefetch (Inertia v3 prefetch); it renders as an `<a>` not a button. Only reach for `Button asChild + Link` when the navigation control must look like a button (primary CTA style)."
|
|
825
|
+
],
|
|
629
826
|
example: `import { Button } from "@godxjp/ui/general";
|
|
630
827
|
import { Trash2 } from "lucide-react";
|
|
631
828
|
|
|
@@ -653,7 +850,7 @@ import { Trash2 } from "lucide-react";
|
|
|
653
850
|
name: "columns",
|
|
654
851
|
type: "ColumnDef<T>[]",
|
|
655
852
|
required: true,
|
|
656
|
-
description: "Column definitions. Each column: {
|
|
853
|
+
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."
|
|
657
854
|
},
|
|
658
855
|
{
|
|
659
856
|
name: "getRowId",
|
|
@@ -695,12 +892,12 @@ import { Trash2 } from "lucide-react";
|
|
|
695
892
|
},
|
|
696
893
|
{
|
|
697
894
|
name: "sort",
|
|
698
|
-
type: "{
|
|
895
|
+
type: "{ value: string; direction: 'asc' | 'desc' }",
|
|
699
896
|
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))."
|
|
700
897
|
},
|
|
701
898
|
{
|
|
702
899
|
name: "onSortChange",
|
|
703
|
-
type: "(sort: {
|
|
900
|
+
type: "(sort: { value: string; direction: 'asc' | 'desc' } | undefined) => void",
|
|
704
901
|
description: "Called when a sortable column header is clicked. Receives undefined when sort is cleared (third click on same column)."
|
|
705
902
|
},
|
|
706
903
|
{
|
|
@@ -759,10 +956,10 @@ type Invoice = {
|
|
|
759
956
|
};
|
|
760
957
|
|
|
761
958
|
const columns: ColumnDef<Invoice>[] = [
|
|
762
|
-
{
|
|
763
|
-
{
|
|
959
|
+
{ value: "id", header: "Invoice #", width: "w-32" },
|
|
960
|
+
{ value: "customer", header: "Customer" },
|
|
764
961
|
{
|
|
765
|
-
|
|
962
|
+
value: "status",
|
|
766
963
|
header: "Status",
|
|
767
964
|
render: (row) => (
|
|
768
965
|
<Badge
|
|
@@ -774,7 +971,7 @@ const columns: ColumnDef<Invoice>[] = [
|
|
|
774
971
|
</Badge>
|
|
775
972
|
),
|
|
776
973
|
},
|
|
777
|
-
{
|
|
974
|
+
{ value: "amount", header: "Amount", align: "right", sortable: true },
|
|
778
975
|
];
|
|
779
976
|
|
|
780
977
|
export default function InvoiceList({
|
|
@@ -785,7 +982,7 @@ export default function InvoiceList({
|
|
|
785
982
|
loading: boolean;
|
|
786
983
|
}) {
|
|
787
984
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
788
|
-
const [sort, setSort] = useState<{
|
|
985
|
+
const [sort, setSort] = useState<{ value: string; direction: "asc" | "desc" } | undefined>();
|
|
789
986
|
|
|
790
987
|
return (
|
|
791
988
|
<DataTable
|
|
@@ -847,6 +1044,28 @@ export default function InvoiceList({
|
|
|
847
1044
|
description: "Internal padding density (base 16 / tight 12 / cozy 20)."
|
|
848
1045
|
}
|
|
849
1046
|
],
|
|
1047
|
+
usage: [
|
|
1048
|
+
'DO always wrap body content in <CardContent> \u2014 the bare <Card> div has zero inner padding; content renders flush against card edges without it. Never add className="p-4" directly on <Card> as a substitute.',
|
|
1049
|
+
"DO put titles/descriptions in <CardHeader>/<CardTitle>/<CardDescription>. Use <CardHeader banded> for a visually separated muted-background header band (mirrors <CardFooter separated>). Pair with <CardAction> inside a flex-row CardHeader for header-level action buttons.",
|
|
1050
|
+
"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
|
+
"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
|
+
"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 <StatCard> (label, value, hint, delta, layout, inverse props) which is already a Card internally with correct token-driven layout."
|
|
1054
|
+
],
|
|
1055
|
+
useCases: [
|
|
1056
|
+
'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.',
|
|
1057
|
+
'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
|
+
"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
|
+
"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
|
+
'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
|
+
"Media/cover card (e.g. entity profile): <CardCover> first (full-bleed image), then <CardHeader> + <CardContent> below it for structured metadata."
|
|
1062
|
+
],
|
|
1063
|
+
related: [
|
|
1064
|
+
"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
|
+
"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
|
+
"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
|
+
"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
|
+
],
|
|
850
1069
|
example: `import { Card, CardHeader, CardTitle, CardContent } from "@godxjp/ui/data-display";
|
|
851
1070
|
|
|
852
1071
|
<Card accent="success">
|
|
@@ -877,6 +1096,28 @@ export default function InvoiceList({
|
|
|
877
1096
|
description: "No header above: top padding matches the card shell."
|
|
878
1097
|
}
|
|
879
1098
|
],
|
|
1099
|
+
usage: [
|
|
1100
|
+
"DO: Always wrap body content in <CardContent> \u2014 a bare <Card> has no internal padding, so any child placed directly inside it renders flush against the card edges.",
|
|
1101
|
+
"DO: Use <CardContent flush> for DataTable, Table, or Tabs \u2014 the flush prop removes horizontal padding so the content spans edge-to-edge inside the card border. Never add manual p-0 on the Card itself instead.",
|
|
1102
|
+
"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
|
+
"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
|
+
"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 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
|
+
],
|
|
1107
|
+
useCases: [
|
|
1108
|
+
"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
|
+
"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
|
+
"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 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
|
+
"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
|
+
"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
|
+
],
|
|
1115
|
+
related: [
|
|
1116
|
+
"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
|
+
"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
|
+
"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
|
+
"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
|
+
],
|
|
880
1121
|
example: `import { Card, CardContent, DataTable } from "@godxjp/ui/data-display";
|
|
881
1122
|
|
|
882
1123
|
<Card>
|
|
@@ -888,9 +1129,9 @@ export default function InvoiceList({
|
|
|
888
1129
|
rules: [37, 38]
|
|
889
1130
|
},
|
|
890
1131
|
{
|
|
891
|
-
name: "
|
|
1132
|
+
name: "StatCard",
|
|
892
1133
|
group: "data-display",
|
|
893
|
-
tagline: "KPI tile. \u26A0\uFE0F
|
|
1134
|
+
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).",
|
|
894
1135
|
props: [
|
|
895
1136
|
{ name: "label", type: "ReactNode", required: true, description: "Metric name." },
|
|
896
1137
|
{
|
|
@@ -913,82 +1154,96 @@ export default function InvoiceList({
|
|
|
913
1154
|
},
|
|
914
1155
|
{ name: "align", type: '"start" | "end"', description: "Align the metric group." }
|
|
915
1156
|
],
|
|
916
|
-
|
|
1157
|
+
usage: [
|
|
1158
|
+
"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
|
+
"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
|
+
"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 StatCard does not expose it. Passing accent has no effect and creates a false expectation.",
|
|
1162
|
+
"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.",
|
|
1163
|
+
"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
|
+
],
|
|
1165
|
+
useCases: [
|
|
1166
|
+
"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.",
|
|
1167
|
+
"Accounting summary header: total debits, total credits, and net balance for a journal entry list page, each with a hint showing the date range in scope.",
|
|
1168
|
+
"Coupon/membership admin overview: active members, live coupons, monthly redemptions, and total discount amount \u2014 the canonical example in the catalog.",
|
|
1169
|
+
"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
|
+
"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 StatCard tiles once data resolves."
|
|
1172
|
+
],
|
|
1173
|
+
related: [
|
|
1174
|
+
"ResponsiveGrid \u2014 required layout wrapper for StatCard grids; controls column count and responsive breakpoints. Always pair them together.",
|
|
1175
|
+
"SkeletonCard \u2014 exact loading placeholder shaped like a StatCard tile; swap in while KPI data is fetching, then replace with the real StatCard.",
|
|
1176
|
+
"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.",
|
|
1177
|
+
"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
|
+
],
|
|
1179
|
+
example: `import { StatCard } from "@godxjp/ui/data-display";
|
|
917
1180
|
import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
918
1181
|
|
|
919
|
-
// \u2705
|
|
1182
|
+
// \u2705 StatCard sits directly in the grid \u2014 it draws its own card + border.
|
|
920
1183
|
<ResponsiveGrid columns={3}>
|
|
921
|
-
<
|
|
922
|
-
<
|
|
923
|
-
<
|
|
1184
|
+
<StatCard label="\u7DCF\u4F1A\u54E1\u6570" value="12,450" hint="\u5148\u6708\u6BD4 +3%" />
|
|
1185
|
+
<StatCard label="\u6708\u6B21\u58F2\u4E0A" value="\xA58,200,000" delta="+12%" />
|
|
1186
|
+
<StatCard label="\u5229\u7528\u7387" value="68.4%" />
|
|
924
1187
|
</ResponsiveGrid>
|
|
925
1188
|
|
|
926
|
-
// \u274C Double border \u2014 do NOT wrap
|
|
927
|
-
// <Card><CardContent><
|
|
928
|
-
storyPath: "data-display/
|
|
1189
|
+
// \u274C Double border \u2014 do NOT wrap StatCard in a Card:
|
|
1190
|
+
// <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>`,
|
|
1191
|
+
storyPath: "data-display/StatCard.stories.tsx",
|
|
929
1192
|
rules: []
|
|
930
1193
|
},
|
|
931
1194
|
{
|
|
932
|
-
name: "
|
|
1195
|
+
name: "Badge",
|
|
933
1196
|
group: "data-display",
|
|
934
|
-
tagline: "
|
|
1197
|
+
tagline: "Plain or lifecycle badge. Use `variant` for static chips, or `status` to auto-map lifecycle keys to semantic tone + icon. Labels never wrap.",
|
|
935
1198
|
props: [
|
|
936
1199
|
{
|
|
937
|
-
name: "
|
|
938
|
-
type: "
|
|
939
|
-
|
|
940
|
-
description: "
|
|
1200
|
+
name: "variant",
|
|
1201
|
+
type: '"default" | "secondary" | "outline" | "success" | "warning" | "destructive" | "info" | "neutral"',
|
|
1202
|
+
defaultValue: '"default"',
|
|
1203
|
+
description: "Visual variant. Overrides the auto-mapped status tone when status is provided."
|
|
941
1204
|
},
|
|
942
1205
|
{
|
|
943
|
-
name: "
|
|
944
|
-
type:
|
|
945
|
-
description: "
|
|
1206
|
+
name: "status",
|
|
1207
|
+
type: "string",
|
|
1208
|
+
description: "Lifecycle key. Known keys auto-map to variant + icon + i18n label; unknown keys fall back to neutral."
|
|
946
1209
|
},
|
|
947
1210
|
{
|
|
948
1211
|
name: "icon",
|
|
949
|
-
type: "
|
|
950
|
-
description: "
|
|
1212
|
+
type: "React.ComponentType<{ className?: string }> | null",
|
|
1213
|
+
description: "Leading icon override. Pass null to suppress the auto status icon."
|
|
951
1214
|
},
|
|
952
|
-
{
|
|
953
|
-
name: "label",
|
|
954
|
-
type: "ReactNode",
|
|
955
|
-
description: "Override display text (default: i18n of key, or raw status)."
|
|
956
|
-
}
|
|
1215
|
+
{ name: "children", type: "ReactNode", description: "Badge label. When omitted with status, Badge renders the translated lifecycle label or raw status." }
|
|
957
1216
|
],
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
type: '"default" | "secondary" | "destructive" | "outline" | "success" | "warning"',
|
|
976
|
-
defaultValue: '"default"',
|
|
977
|
-
description: "Visual variant."
|
|
978
|
-
},
|
|
979
|
-
{ name: "children", type: "ReactNode", required: true, description: "Badge text/content." }
|
|
1217
|
+
usage: [
|
|
1218
|
+
"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.",
|
|
1219
|
+
"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.",
|
|
1220
|
+
"DO pass `variant` explicitly for localized labels or categorical tiers, and pass `icon={null}` when a lifecycle glyph would be misleading.",
|
|
1221
|
+
"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.",
|
|
1222
|
+
"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).",
|
|
1223
|
+
"Use semantic tokens for any className overrides (`text-muted-foreground`, `bg-destructive`) \u2014 never raw Tailwind palette classes like `bg-green-500`."
|
|
1224
|
+
],
|
|
1225
|
+
useCases: [
|
|
1226
|
+
'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>`).',
|
|
1227
|
+
'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 variant="success">\u627F\u8A8D\u6E08</Badge>` or `<Badge variant="warning">\u8981\u78BA\u8A8D</Badge>`.',
|
|
1228
|
+
"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.",
|
|
1229
|
+
'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.',
|
|
1230
|
+
"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."
|
|
1231
|
+
],
|
|
1232
|
+
related: [
|
|
1233
|
+
"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."
|
|
980
1234
|
],
|
|
981
1235
|
example: `import { Badge } from "@godxjp/ui/data-display";
|
|
982
1236
|
|
|
983
1237
|
<Badge variant="secondary">A/B</Badge>
|
|
984
|
-
<Badge
|
|
1238
|
+
<Badge status="active">\u516C\u958B\u4E2D</Badge>
|
|
1239
|
+
<Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" variant="success" icon={null}>\u30D7\u30EC\u30DF\u30A2\u30E0</Badge>`,
|
|
985
1240
|
storyPath: "data-display/Badge.stories.tsx",
|
|
986
1241
|
rules: [35]
|
|
987
1242
|
},
|
|
988
1243
|
{
|
|
989
|
-
name: "
|
|
1244
|
+
name: "Descriptions",
|
|
990
1245
|
group: "data-display",
|
|
991
|
-
tagline: "Responsive definition grid for detail-page metadata. COMPOUND \u2014 value goes in
|
|
1246
|
+
tagline: "Responsive definition grid for detail-page metadata. COMPOUND \u2014 value goes in Descriptions.Item children.",
|
|
992
1247
|
props: [
|
|
993
1248
|
{
|
|
994
1249
|
name: "columns",
|
|
@@ -1000,17 +1255,39 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1000
1255
|
name: "children",
|
|
1001
1256
|
type: "ReactNode",
|
|
1002
1257
|
required: true,
|
|
1003
|
-
description: "
|
|
1258
|
+
description: "Descriptions.Item elements."
|
|
1004
1259
|
}
|
|
1005
1260
|
],
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1261
|
+
usage: [
|
|
1262
|
+
'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>.',
|
|
1263
|
+
"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.",
|
|
1264
|
+
"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.",
|
|
1265
|
+
"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.",
|
|
1266
|
+
"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>.",
|
|
1267
|
+
"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."
|
|
1268
|
+
],
|
|
1269
|
+
useCases: [
|
|
1270
|
+
"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.",
|
|
1271
|
+
"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.",
|
|
1272
|
+
"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.",
|
|
1273
|
+
"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.",
|
|
1274
|
+
"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.",
|
|
1275
|
+
"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."
|
|
1276
|
+
],
|
|
1277
|
+
related: [
|
|
1278
|
+
"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.",
|
|
1279
|
+
"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).",
|
|
1280
|
+
"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).",
|
|
1281
|
+
"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."
|
|
1282
|
+
],
|
|
1283
|
+
example: `import { Descriptions } from "@godxjp/ui/data-display";
|
|
1284
|
+
|
|
1285
|
+
<Descriptions columns={2}>
|
|
1286
|
+
<Descriptions.Item label="\u4F1A\u54E1ID" mono>{member.id}</Descriptions.Item>
|
|
1287
|
+
<Descriptions.Item label="\u30D7\u30E9\u30F3">{member.plan}</Descriptions.Item>
|
|
1288
|
+
<Descriptions.Item label="\u30E1\u30E2" span={2}>{member.note}</Descriptions.Item>
|
|
1289
|
+
</Descriptions>`,
|
|
1290
|
+
storyPath: "data-display/Descriptions.stories.tsx",
|
|
1014
1291
|
rules: []
|
|
1015
1292
|
},
|
|
1016
1293
|
{
|
|
@@ -1023,6 +1300,28 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1023
1300
|
{ name: "icon", type: "LucideIcon", description: "Icon above the title." },
|
|
1024
1301
|
{ name: "action", type: "ReactNode", description: "CTA element (e.g. a Button)." }
|
|
1025
1302
|
],
|
|
1303
|
+
usage: [
|
|
1304
|
+
"DO always pass `title` \u2014 it is the only required prop and renders an `<h3>`; omitting it causes a blank silent render with no visible error.",
|
|
1305
|
+
"DO use the `icon` prop (a Lucide icon component, not a JSX element) to give visual context \u2014 e.g. `icon={InboxIcon}` for empty inboxes, `icon={SearchIcon}` after a failed search. Pass the component reference, not `<InboxIcon />`.",
|
|
1306
|
+
"DO use `action` (a `ReactNode`, typically a `<Button>`) for actionable zero-states \u2014 e.g. 'Create first invoice' \u2014 so users have a clear next step instead of a dead end.",
|
|
1307
|
+
"DO NOT hand-roll a `data.length === 0 ? <EmptyState /> : <DataTable />` conditional \u2014 `DataTable` already embeds an `EmptyState` in its body when `data` is empty. Use the `empty=` prop on `DataTable` to customise it, not a wrapper conditional.",
|
|
1308
|
+
"DO NOT use EmptyState inside a `DataState` or `InfiniteQueryState` for the loading or error states \u2014 those widgets handle skeleton/error themselves; pass `EmptyState` only to their `empty=` prop for the zero-items case.",
|
|
1309
|
+
"DO NOT add padding directly on `EmptyState` via `className` when placing it inside a `Card` \u2014 wrap it in `<CardContent>` first; EmptyState is a self-contained block with its own internal spacing via `ui-empty-state` styles."
|
|
1310
|
+
],
|
|
1311
|
+
useCases: [
|
|
1312
|
+
"Zero-row admin list pages (invoices, accounts, transactions) that are NOT backed by a `DataTable` \u2014 e.g. a card-grid or custom list layout where DataTable's built-in empty state doesn't apply.",
|
|
1313
|
+
"Post-filter / post-search zero results \u2014 show `icon={SearchIcon}` + a `description` explaining what was searched and an `action` to clear filters.",
|
|
1314
|
+
"First-run onboarding screens where no data has been created yet \u2014 e.g. 'No entities added yet' with an action button to create the first legal entity.",
|
|
1315
|
+
"Passed as the `empty=` prop inside `DataState` or `InfiniteQueryState` to satisfy the TanStack Query lifecycle widget's zero-items slot without hand-rolling markup.",
|
|
1316
|
+
"Standalone section within a `CardContent` to indicate a sub-section (e.g. attachments, comments, related records) has no entries yet, separate from the page-level list.",
|
|
1317
|
+
"Error-adjacent zero states where the page loaded successfully but the filtered result set is empty \u2014 distinct from an error state handled by `DataState`/`MutationFeedback`."
|
|
1318
|
+
],
|
|
1319
|
+
related: [
|
|
1320
|
+
"DataTable \u2014 already embeds an EmptyState automatically when `data` is empty; customise via the `empty=` prop. Do NOT wrap DataTable in a `data.length === 0` guard that renders EmptyState separately.",
|
|
1321
|
+
"DataState \u2014 TanStack Query lifecycle widget (`@godxjp/ui/query`). Pass `<EmptyState />` to its `empty=` prop for zero-items; DataState itself covers loading/error \u2014 do not use EmptyState for those states.",
|
|
1322
|
+
"InfiniteQueryState \u2014 same pattern as DataState but for `useInfiniteQuery`; pass EmptyState to `empty=` when the flattened list is empty.",
|
|
1323
|
+
"SkeletonTable \u2014 use for the loading skeleton before data arrives (pass to DataState's `skeleton=` or DataTable's `loading=`). EmptyState is for after data arrives and is empty, not while loading."
|
|
1324
|
+
],
|
|
1026
1325
|
example: `import { EmptyState } from "@godxjp/ui/data-display";
|
|
1027
1326
|
|
|
1028
1327
|
<EmptyState title="\u8A72\u5F53\u30C7\u30FC\u30BF\u304C\u3042\u308A\u307E\u305B\u3093" description="\u691C\u7D22\u6761\u4EF6\u3092\u5909\u66F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002" />`,
|
|
@@ -1030,7 +1329,7 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1030
1329
|
rules: []
|
|
1031
1330
|
},
|
|
1032
1331
|
{
|
|
1033
|
-
name: "
|
|
1332
|
+
name: "Progress",
|
|
1034
1333
|
group: "data-display",
|
|
1035
1334
|
tagline: "Horizontal progress bar 0\u2013100 with optional label and semantic tone.",
|
|
1036
1335
|
props: [
|
|
@@ -1048,10 +1347,32 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1048
1347
|
description: "Bar colour tone."
|
|
1049
1348
|
}
|
|
1050
1349
|
],
|
|
1051
|
-
|
|
1350
|
+
usage: [
|
|
1351
|
+
'DO import from `@godxjp/ui/data-display`, not from a generic UI path: `import { Progress } from "@godxjp/ui/data-display";`',
|
|
1352
|
+
"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.",
|
|
1353
|
+
'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.',
|
|
1354
|
+
"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.",
|
|
1355
|
+
"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.",
|
|
1356
|
+
"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."
|
|
1357
|
+
],
|
|
1358
|
+
useCases: [
|
|
1359
|
+
'Budget utilisation in an accounting dashboard \u2014 show how much of a monthly budget has been consumed, switching to `variant="warning"` when the figure crosses 80%.',
|
|
1360
|
+
'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`.',
|
|
1361
|
+
"Storage or quota indicator in an admin panel \u2014 visualise disk usage, API quota, or seat licence consumption against a fixed limit.",
|
|
1362
|
+
"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.",
|
|
1363
|
+
"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.",
|
|
1364
|
+
"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."
|
|
1365
|
+
],
|
|
1366
|
+
related: [
|
|
1367
|
+
"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.",
|
|
1368
|
+
"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.",
|
|
1369
|
+
'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.',
|
|
1370
|
+
"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."
|
|
1371
|
+
],
|
|
1372
|
+
example: `import { Progress } from "@godxjp/ui/data-display";
|
|
1052
1373
|
|
|
1053
|
-
<
|
|
1054
|
-
storyPath: "data-display/
|
|
1374
|
+
<Progress value={pct} label={pct + "% \u4F7F\u7528\u4E2D"} variant={pct >= 80 ? "warning" : "success"} />`,
|
|
1375
|
+
storyPath: "data-display/Progress.stories.tsx",
|
|
1055
1376
|
rules: []
|
|
1056
1377
|
},
|
|
1057
1378
|
{
|
|
@@ -1066,6 +1387,28 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1066
1387
|
description: "Array of { title, location?, time?, note?, current? }."
|
|
1067
1388
|
}
|
|
1068
1389
|
],
|
|
1390
|
+
usage: [
|
|
1391
|
+
"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.",
|
|
1392
|
+
"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.",
|
|
1393
|
+
"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`.",
|
|
1394
|
+
"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.",
|
|
1395
|
+
"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'.",
|
|
1396
|
+
"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`."
|
|
1397
|
+
],
|
|
1398
|
+
useCases: [
|
|
1399
|
+
"Shipment / delivery tracking \u2014 showing a parcel's journey through 'Order placed \u2192 Packed \u2192 In transit \u2192 Delivered' with timestamps and a current-stop indicator.",
|
|
1400
|
+
"Accounting document audit trail \u2014 rendering the lifecycle of an invoice or payment (Draft \u2192 Submitted \u2192 Approved \u2192 Paid) with the current approval stage highlighted.",
|
|
1401
|
+
"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`.",
|
|
1402
|
+
"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.",
|
|
1403
|
+
"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.",
|
|
1404
|
+
"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."
|
|
1405
|
+
],
|
|
1406
|
+
related: [
|
|
1407
|
+
"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.",
|
|
1408
|
+
"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.",
|
|
1409
|
+
"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.",
|
|
1410
|
+
"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."
|
|
1411
|
+
],
|
|
1069
1412
|
example: `import { Timeline } from "@godxjp/ui/data-display";
|
|
1070
1413
|
|
|
1071
1414
|
<Timeline items={[
|
|
@@ -1089,6 +1432,27 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1089
1432
|
},
|
|
1090
1433
|
{ name: "className", type: "string", description: "Extra classes on the table element." }
|
|
1091
1434
|
],
|
|
1435
|
+
usage: [
|
|
1436
|
+
"DO compose all six sub-parts in order: wrap with `<Table>`, then `<TableHeader>` containing `<TableRow><TableHead>\u2026</TableRow>`, then `<TableBody>` containing one or more `<TableRow><TableCell>\u2026` rows. Skipping any layer (e.g. bare `<th>` inside `<Table>`) bypasses the design tokens and hover/border styles.",
|
|
1437
|
+
'DO use `TableHead` (not `TableCell`) for header cells \u2014 it renders `<th>` with `data-slot="table-head"` and the `--table-row-height` CSS variable for consistent header sizing across the design system. `TableCell` renders `<td>` with `data-slot="table-cell"` and is for body rows only.',
|
|
1438
|
+
'DO apply numeric alignment via `className` on individual `TableHead`/`TableCell` elements (e.g. `className="text-right"`). There are no built-in alignment props \u2014 all styling goes through Tailwind class overrides.',
|
|
1439
|
+
"DO NOT hand-roll empty-state handling inside a Table composition. When data can be empty, switch to `DataTable` (which has a built-in empty state) or wrap the `<Table>` with a conditional that renders `<EmptyState>` \u2014 never leave a table with only a header and zero rows.",
|
|
1440
|
+
"DO NOT use Table for lists that need sorting, filtering, pagination, or row selection \u2014 those features are only in `DataTable`. Table is intentionally stateless: it owns no TanStack Table instance, no column definitions, and no toolbar.",
|
|
1441
|
+
"DO place `<Table>` inside a `<CardContent flush>` (or `p-0` card) when embedding in a Card, so the built-in `overflow-auto` wrapper sits flush to the card edges. Wrapping with plain `<CardContent>` adds padding that clips the horizontal scroll shadow."
|
|
1442
|
+
],
|
|
1443
|
+
useCases: [
|
|
1444
|
+
"Invoice line-item breakdowns \u2014 a fixed, read-only list of product/quantity/unit-price/total rows where columns are predefined and will never need sort or filter controls.",
|
|
1445
|
+
"Summary/comparison tables inside a detail panel or Dialog, such as showing two payment plans side-by-side, where the structure is hand-authored and not driven by a data array.",
|
|
1446
|
+
"Print or PDF-export views where a minimal, stateless `<table>` element with predictable markup is required and DataTable's JS-driven features would interfere with server-side rendering or CSS print rules.",
|
|
1447
|
+
"Embedded sub-tables inside a DataTable expanded row (the inner table uses Table primitives because nesting a full DataTable instance inside another is unsupported).",
|
|
1448
|
+
"Static reference tables in documentation, onboarding, or settings pages \u2014 e.g. a permission matrix or feature comparison \u2014 where every cell is literal JSX content, not from a data array."
|
|
1449
|
+
],
|
|
1450
|
+
related: [
|
|
1451
|
+
"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.",
|
|
1452
|
+
"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.",
|
|
1453
|
+
"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.",
|
|
1454
|
+
"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."
|
|
1455
|
+
],
|
|
1092
1456
|
example: `import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@godxjp/ui/data-display";
|
|
1093
1457
|
|
|
1094
1458
|
<Table>
|
|
@@ -1119,6 +1483,27 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1119
1483
|
{ name: "empty", type: "ReactNode", description: "Shown when isEmpty(data) is true." },
|
|
1120
1484
|
{ name: "isEmpty", type: "(data) => boolean", description: "Custom empty check." }
|
|
1121
1485
|
],
|
|
1486
|
+
usage: [
|
|
1487
|
+
"DO: pass a `UseQueryResult<T>` directly from `useQuery` \u2014 DataState reads `isPending`, `isError`, `isFetching`, `data`, and `error` off it; never destructure those fields manually and branch yourself.",
|
|
1488
|
+
"DO: always provide a `skeleton` \u2014 it renders during both the initial pending phase and during a re-fetch after an error; pass `<SkeletonTable />` for tabular data or `<SkeletonCard />` for card lists \u2014 never `null` or a spinner div.",
|
|
1489
|
+
'DO: provide `empty` + `isEmpty` together when the data can legitimately return 0 items \u2014 e.g. `isEmpty={(d) => d.items.length === 0}` paired with `empty={<EmptyState title="\u2026" />}`. Omitting `empty` means an empty array still falls through to `children`, silently rendering a blank table.',
|
|
1490
|
+
"DON'T: wrap DataState in your own conditional \u2014 e.g. `{query.isSuccess && <DataState \u2026>}`. DataState IS the conditional; the outer guard is redundant and breaks the retry/refetch skeleton.",
|
|
1491
|
+
"DON'T: use DataState for `useInfiniteQuery` results. The `query` prop type is `UseQueryResult<T>`, not `UseInfiniteQueryResult`. Use `InfiniteQueryState` (from `@godxjp/ui/query`) instead, which accepts `flatten` and renders a load-more footer.",
|
|
1492
|
+
"DO: supply `errorRenderer` only when the default `AlertQueryError` + retry button is not enough \u2014 e.g. a full-page error boundary with navigation. Otherwise rely on `showRetry` (default `true`) and the built-in `AlertQueryError`, and override `onRetry` only if `query.refetch()` is not the right action."
|
|
1493
|
+
],
|
|
1494
|
+
useCases: [
|
|
1495
|
+
"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.",
|
|
1496
|
+
"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.",
|
|
1497
|
+
"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.",
|
|
1498
|
+
"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.",
|
|
1499
|
+
"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."
|
|
1500
|
+
],
|
|
1501
|
+
related: [
|
|
1502
|
+
"InfiniteQueryState \u2014 use instead of DataState when the query is `useInfiniteQuery`; it accepts a `flatten` function to reduce pages and adds a load-more footer. DataState cannot accept `UseInfiniteQueryResult`.",
|
|
1503
|
+
"SkeletonTable / SkeletonCard \u2014 pass as the `skeleton` prop of DataState; they are not standalone replacements for DataState, only the loading slot inside it.",
|
|
1504
|
+
"EmptyState \u2014 pass as the `empty` prop of DataState alongside a matching `isEmpty` predicate; do not hand-roll an empty-check outside DataState by inspecting `query.data` yourself.",
|
|
1505
|
+
"MutationFeedback \u2014 sibling widget for mutation (not query) lifecycle; use it below a form submit button to surface `useMutation` errors, not DataState which only handles `useQuery`."
|
|
1506
|
+
],
|
|
1122
1507
|
example: `import { DataState } from "@godxjp/ui/query";
|
|
1123
1508
|
|
|
1124
1509
|
<DataState query={membersQuery} skeleton={<SkeletonTable />} isEmpty={(d) => d.items.length === 0} empty={<EmptyState title="\u4F1A\u54E1\u306A\u3057" />}>
|
|
@@ -1157,6 +1542,28 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1157
1542
|
description: "Render with flat data + { fetchNextPage, hasNextPage, isFetchingNextPage }."
|
|
1158
1543
|
}
|
|
1159
1544
|
],
|
|
1545
|
+
usage: [
|
|
1546
|
+
"DO: Import from `@godxjp/ui/query` (not `@godxjp/ui`). Use the bundled `flattenItemPages` helper for any API that returns `{ items: T[] }` pages \u2014 it handles `undefined` data safely. Custom page shapes require a custom `flatten` function.",
|
|
1547
|
+
"DO: Always pass `skeleton` (e.g. `<SkeletonTable />` or `<SkeletonCard />`). It shows on initial `isPending`, on refetch-after-error, and whenever `data` is absent. Never show a blank area while loading.",
|
|
1548
|
+
"DO: Pass `empty` (an `<EmptyState>` node) to handle the zero-results case \u2014 without it the children render-prop is called with an empty array and you get a silent blank screen. Provide a custom `isEmpty` only when `TFlat` is not an array.",
|
|
1549
|
+
"DON'T: Hand-roll a load-more button. The component renders a default centered outline Button when `hasNextPage` is true. Override only via `loadMore` (custom node) or `showLoadMore={false}` (hide entirely). Never call `query.fetchNextPage()` outside the component for pagination.",
|
|
1550
|
+
"DON'T: Use `InfiniteQueryState` for a `useQuery` result \u2014 it expects `UseInfiniteQueryResult` shape (`pages`, `hasNextPage`, `fetchNextPage`, `isFetchingNextPage`). For regular `useQuery` use `DataState` instead.",
|
|
1551
|
+
"DON'T: Confuse the two generics: `TPage` is the raw page shape from the API, `TFlat` is what `flatten` returns (usually `TItem[]`). The `children` render-prop receives `TFlat`, not `TPage`. Pass `isEmpty` if `TFlat` is not a plain array so empty detection works correctly."
|
|
1552
|
+
],
|
|
1553
|
+
useCases: [
|
|
1554
|
+
"Activity / audit-log feed that accumulates pages as the user scrolls down or clicks 'Load more' \u2014 the default footer button handles `fetchNextPage` automatically.",
|
|
1555
|
+
"Invoice or transaction list with cursor-based pagination where total count is unknown and pages are appended rather than replaced (replacing pages is DataTable's job).",
|
|
1556
|
+
"Notification inbox, comment thread, or journal entry list where new items are appended at the bottom and the user never pages backwards.",
|
|
1557
|
+
"Search results with a 'Show more' button rather than numbered pages \u2014 pass `showLoadMore={true}` (default) and hide the button once `hasNextPage` is false without any extra state.",
|
|
1558
|
+
"Admin dashboard 'recent events' widget backed by `useInfiniteQuery` \u2014 use `SkeletonTable` as `skeleton` and `<EmptyState title='No events yet' />` as `empty` so every state is handled.",
|
|
1559
|
+
"Infinite-scroll implementation: receive the `helpers` argument in `children` (`{ fetchNextPage, hasNextPage, isFetchingNextPage }`) to wire a scroll sentinel (Intersection Observer) instead of the built-in button, while still benefiting from error/skeleton/empty lifecycle handling."
|
|
1560
|
+
],
|
|
1561
|
+
related: [
|
|
1562
|
+
"DataState \u2014 use instead when the query is a plain `useQuery` (not infinite). Identical lifecycle surface (skeleton/empty/error/children) but expects a single page of data, not accumulated pages. Pick DataState for any paginated table where only one page is visible at a time.",
|
|
1563
|
+
"DataTable \u2014 use for tabular data with server-side pagination where pages are swapped, not appended. DataTable manages its own pagination UI (cursor buttons); InfiniteQueryState is for append-only / infinite-scroll patterns.",
|
|
1564
|
+
"SkeletonTable / SkeletonCard \u2014 pass as the `skeleton` prop to InfiniteQueryState; do not render them manually alongside InfiniteQueryState since the component controls when skeleton is visible.",
|
|
1565
|
+
"QueryRefetchButton \u2014 companion component for the page header refresh action wired to `query.refetch()`. Use alongside InfiniteQueryState when you want an explicit refresh control in addition to the built-in load-more footer."
|
|
1566
|
+
],
|
|
1160
1567
|
example: `import { InfiniteQueryState, flattenItemPages } from "@godxjp/ui/query";
|
|
1161
1568
|
|
|
1162
1569
|
<InfiniteQueryState query={q} skeleton={<SkeletonRows />} flatten={flattenItemPages} isEmpty={(it) => it.length === 0}>
|
|
@@ -1178,6 +1585,28 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1178
1585
|
},
|
|
1179
1586
|
{ name: "onRetry", type: "() => void", description: "Retry handler." }
|
|
1180
1587
|
],
|
|
1588
|
+
usage: [
|
|
1589
|
+
"DO: Import exclusively from `@godxjp/ui/query` \u2014 it is NOT exported from the main `@godxjp/ui` barrel.",
|
|
1590
|
+
"DO: Pass the full `useMutation` result object as `mutation`; the component reads `.isError`, `.error`, and `.isPending` \u2014 only those three fields are consumed, so a plain object mock works in tests.",
|
|
1591
|
+
"DO: Supply `onRetry` when the user can meaningfully re-trigger the mutation (e.g. re-submit a form, re-run a simulation). The Retry button is rendered by `AlertQueryError` only when `onRetry` is provided AND `showRetry` is not `false`. Set `showRetry={false}` to hide the button when retry is semantically wrong (e.g. a destructive delete that must not auto-repeat).",
|
|
1592
|
+
"DO: Use the `pending` prop to render an inline loading slot while `mutation.isPending` is true \u2014 it replaces the error area with arbitrary JSX (spinner, skeleton row) so the layout does not shift when the mutation transitions idle \u2192 pending \u2192 error.",
|
|
1593
|
+
"DON'T: Render `MutationFeedback` for query (fetch) errors \u2014 use `DataState` for those. `MutationFeedback` is scoped to write operations (`useMutation`), not read operations (`useQuery`).",
|
|
1594
|
+
"DON'T: Hand-roll an inline error alert for mutation failures \u2014 this component already wraps `Alert variant='destructive'` with i18n title, `humanError()` message formatting, and an accessible Retry button. Duplicating that logic breaks consistency and bypasses localisation."
|
|
1595
|
+
],
|
|
1596
|
+
useCases: [
|
|
1597
|
+
"Form save failures \u2014 place `<MutationFeedback mutation={saveMutation} onRetry={saveMutation.mutate} />` directly below a submit button so the error appears inline next to the control that triggered it, keeping the user in context without a page-level toast.",
|
|
1598
|
+
"Blocking simulator / calculation runs \u2014 when a long-running mutation (e.g. tax computation, invoice generation) fails and the page must stay on the form until the user corrects and retries, use `MutationFeedback` with `pending={<SkeletonTable />}` to show a skeleton while running and flip to an error banner on failure.",
|
|
1599
|
+
"Multi-step wizard step submissions \u2014 show per-step mutation errors inline inside each `<Steps>` panel so the user sees exactly which step failed without scrolling to a page-level alert.",
|
|
1600
|
+
"Admin destructive actions (delete, void, archive) \u2014 pass `showRetry={false}` so no Retry button appears after a failed irreversible operation, preventing accidental double-execution.",
|
|
1601
|
+
"Accounting record mutations (journal entry save, invoice approval) \u2014 inline error keeps audit-sensitive feedback close to the triggering form rather than relying on a transient toast that the user might miss.",
|
|
1602
|
+
"Background job kicks that surface an immediate API error \u2014 when a mutation POSTs to a job-dispatch endpoint and the server rejects synchronously, `MutationFeedback` surfaces the error inline with a retry CTA without requiring a separate alert component."
|
|
1603
|
+
],
|
|
1604
|
+
related: [
|
|
1605
|
+
"Toaster \u2014 use `toast.error()` (sonner) via `Toaster` for transient, non-blocking save confirmations where the user does not need to retry in-place; prefer `MutationFeedback` when the error is blocking and must stay visible until the user acts.",
|
|
1606
|
+
"DataState \u2014 use `DataState` for the full TanStack Query read lifecycle (pending skeleton / error / empty / data); `MutationFeedback` is the write-side counterpart for `useMutation` errors only.",
|
|
1607
|
+
"Alert \u2014 the raw destructive alert primitive; use `Alert variant='destructive'` directly only when the error is not from a `useMutation` result and you need full compositional control; `MutationFeedback` is the correct abstraction when you have a mutation object.",
|
|
1608
|
+
"QueryRefetchButton \u2014 for surfacing a manual refetch action on a `useQuery` result in a page header; `MutationFeedback` handles the equivalent retry on the mutation side and must not be conflated with query refetch patterns."
|
|
1609
|
+
],
|
|
1181
1610
|
example: `import { MutationFeedback } from "@godxjp/ui/query";
|
|
1182
1611
|
|
|
1183
1612
|
<MutationFeedback mutation={saveMutation} />`,
|
|
@@ -1221,6 +1650,28 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1221
1650
|
description: "The single control to render."
|
|
1222
1651
|
}
|
|
1223
1652
|
],
|
|
1653
|
+
usage: [
|
|
1654
|
+
"DO pass the same string to both `id` on `<FormField>` and `id` on the child control \u2014 the component wires `<Label htmlFor={id}>`, and builds `{id}-helper` / `{id}-error` ids for `aria-describedby`. If the ids diverge the label click and screen-reader announcements break.",
|
|
1655
|
+
"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.",
|
|
1656
|
+
"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.",
|
|
1657
|
+
"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.",
|
|
1658
|
+
"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.",
|
|
1659
|
+
"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."
|
|
1660
|
+
],
|
|
1661
|
+
useCases: [
|
|
1662
|
+
"Labelling a text `Input` or `Textarea` in an invoice-entry form, showing a red asterisk for required fields and surfacing server validation errors returned from a Laravel FormRequest.",
|
|
1663
|
+
"Wrapping a `Select` or `DatePicker` inside a multi-field filter panel where each control needs a visible label, helper hint (e.g. 'YYYY/MM/DD'), and inline error state.",
|
|
1664
|
+
"Adding a `labelAddon` tooltip button next to a 'Tax rate' label in an accounting form to explain when different rates apply, without breaking the label\u2013control association.",
|
|
1665
|
+
"Enclosing a `DateRangePicker` or `TimePicker` in an admin settings page where the field needs a label, a muted hint ('Inclusive of start and end date'), and conditional error display.",
|
|
1666
|
+
"Wrapping a `SearchSelect` or `Autocomplete` control for vendor/account lookup in a journal-entry form where the `id` must be kept consistent for programmatic focus management.",
|
|
1667
|
+
"Providing structured error feedback for a `Cascader` or `TreeSelect` in a multi-level category assignment screen, replacing ad-hoc error rendering with the standardised `role='alert'` pattern."
|
|
1668
|
+
],
|
|
1669
|
+
related: [
|
|
1670
|
+
"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.",
|
|
1671
|
+
"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.",
|
|
1672
|
+
"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.",
|
|
1673
|
+
"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."
|
|
1674
|
+
],
|
|
1224
1675
|
example: `import { FormField, Input } from "@godxjp/ui/data-entry";
|
|
1225
1676
|
|
|
1226
1677
|
<FormField id="coupon-name" label="\u30AF\u30FC\u30DD\u30F3\u540D" required error={errors.name} helper="\u6700\u592750\u6587\u5B57">
|
|
@@ -1244,6 +1695,27 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1244
1695
|
description: "Native change handler."
|
|
1245
1696
|
}
|
|
1246
1697
|
],
|
|
1698
|
+
usage: [
|
|
1699
|
+
"DO always wrap Input in FormField when the field needs a label, helper text, or validation error \u2014 FormField injects aria-describedby and aria-invalid onto Input automatically; never wire these attributes by hand.",
|
|
1700
|
+
"DO match the `id` prop on Input to the `id` prop on its parent FormField so that `htmlFor` linkage and the generated helper/error ids are consistent.",
|
|
1701
|
+
"DO use Input in controlled mode (`value` + `onChange`) for forms driven by Inertia's `useForm` or React state; uncontrolled usage (no `value`) is only acceptable for fire-and-forget inline edits where form state is not needed.",
|
|
1702
|
+
"DON'T use a raw `<input>` element \u2014 Input adds the full token-based styling (border-input, focus ring, disabled/invalid states, file-slot styling) and the `data-slot='input'` marker that FormField relies on to inject aria attributes.",
|
|
1703
|
+
"DON'T hand-roll an error border or red ring with className \u2014 Input reads `aria-invalid` (set by FormField) and applies `border-destructive` + `ring-destructive/20` automatically; adding manual destructive classes will conflict.",
|
|
1704
|
+
"DON'T use Input for multi-line text \u2014 use Textarea; DON'T use it for filtered/debounced search \u2014 use SearchInput which fires `onSearch` after a debounce and includes a clear button."
|
|
1705
|
+
],
|
|
1706
|
+
useCases: [
|
|
1707
|
+
"Single-line text fields in create/edit forms \u2014 invoice reference numbers, company names, contact emails, coupon codes, amounts typed as text (pair with `type='number'` for numeric entry).",
|
|
1708
|
+
"Inline editable cells or quick-edit dialogs where a single short value needs to be changed (e.g. editing a journal entry memo or an account code) and full Select/DatePicker overhead is unnecessary.",
|
|
1709
|
+
"File upload trigger when wrapped with `type='file'` \u2014 the file-slot classes style the native file button consistently without any extra wrapper.",
|
|
1710
|
+
"Password entry fields (`type='password'`) in auth or settings screens, where the styled focus ring and disabled-state opacity are needed without building a custom control.",
|
|
1711
|
+
"Numeric/currency input in accounting forms (`type='number'`, `inputMode='decimal'`) for quantities, exchange rates, or tax amounts where a free-form numeric entry is required rather than a slider or stepper."
|
|
1712
|
+
],
|
|
1713
|
+
related: [
|
|
1714
|
+
"SearchInput \u2014 use instead of Input when the value drives a live filter or search query; SearchInput debounces internally, fires `onSearch` (not `onChange`), and provides a built-in clear button. Never put debounce logic on top of a plain Input.",
|
|
1715
|
+
"Textarea \u2014 use instead of Input for multi-line text (notes, descriptions, memo fields). Input is strictly single-line.",
|
|
1716
|
+
"FormField \u2014 always compose Input inside FormField when the field needs a visible label, helper hint, or validation error message; FormField handles all a11y wiring so Input stays a pure unstyled-but-styled primitive.",
|
|
1717
|
+
"Select \u2014 use instead of Input when the value must come from a fixed or async option list; never render a plain Input and parse free text when the set of valid values is enumerable."
|
|
1718
|
+
],
|
|
1247
1719
|
example: `import { Input } from "@godxjp/ui/data-entry";
|
|
1248
1720
|
|
|
1249
1721
|
<Input id="qty" type="number" placeholder="\u4F8B: 500" value={value} onChange={(e) => setValue(e.target.value)} />`,
|
|
@@ -1276,6 +1748,27 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1276
1748
|
description: "Debounce delay (ms)."
|
|
1277
1749
|
}
|
|
1278
1750
|
],
|
|
1751
|
+
usage: [
|
|
1752
|
+
"DO: listen to `onSearch`, not `onChange`. The component debounces internally (default 250 ms) and fires `onSearch(q)` after the delay \u2014 never wire your filter logic to `onChange` on SearchInput because it does not expose one.",
|
|
1753
|
+
"DO: choose controlled vs uncontrolled deliberately. Pass `value` + `onSearch` together for controlled mode (e.g. when search state lives in a URL param or shared parent). For local-only ephemeral search pass only `defaultValue` + `onSearch` \u2014 omitting `value` puts the component in uncontrolled mode.",
|
|
1754
|
+
"DO: supply an `ariaLabel` (or visible `label`) when no adjacent label exists. Without either prop, SearchInput falls back to the i18n key `common.search` rendered as a visually-hidden `<Label>` \u2014 still accessible, but providing a context-specific string (e.g. `ariaLabel='\u8ACB\u6C42\u66F8\u3092\u691C\u7D22'`) is more descriptive for screen readers.",
|
|
1755
|
+
"DON'T: use SearchInput inside a `<form>` expecting native form submission. The component has no `name` prop and does not emit a form field value \u2014 it is a filter-trigger widget. For a form search field, use a plain `Input` inside `FormField`.",
|
|
1756
|
+
"DON'T: hand-roll a debounced input when you need a search box. SearchInput ships the debounce, clear button (\xD7), search icon, and accessible label \u2014 recreating these with a raw `<Input>` adds code and misses the UX contract.",
|
|
1757
|
+
"DON'T: place SearchInput inside a `FilterGroup` wrapper \u2014 `FilterGroup` is for Select/DatePicker controls with a label chip. SearchInput goes directly as a child of `FilterBar` (or standalone above a table), not wrapped in `FilterGroup`."
|
|
1758
|
+
],
|
|
1759
|
+
useCases: [
|
|
1760
|
+
"List-page filter bar: placed as the first child of `FilterBar` (before any `FilterGroup` children) to drive text-based filtering of a `DataTable`. The `onSearch` callback updates a query param or state variable that the table's data fetch reads.",
|
|
1761
|
+
"Inline client-side search over a small in-memory list (e.g. a sidebar nav list, a transfer panel, a settings category list) where results narrow immediately as the user types without a server round-trip \u2014 use uncontrolled mode (`defaultValue`) so no state is needed in the parent.",
|
|
1762
|
+
"URL-synced search: controlled mode where `value` comes from `useSearchParams()` and `onSearch` pushes to the URL, enabling deep-linkable, bookmarkable filtered views on invoice/transaction/customer index pages.",
|
|
1763
|
+
"Panel or dialog search: filtering a long dropdown list, a tree, or a multi-item selection panel that does not use the built-in `Command` palette \u2014 SearchInput provides the search box while the parent renders the filtered result set.",
|
|
1764
|
+
"Toolbar search on a data-heavy accounting page (e.g. journal-entry search, partner lookup in a subledger view) where the 250 ms debounce prevents a flood of API calls on every keystroke without requiring the developer to implement debounce logic."
|
|
1765
|
+
],
|
|
1766
|
+
related: [
|
|
1767
|
+
"Input \u2014 use `Input` (inside `FormField`) when the search field is part of a submitted form and needs a `name` attribute, or when you need full `onChange` control without any debounce or clear button. SearchInput is the right pick when the field only triggers filtering, not form submission.",
|
|
1768
|
+
"FilterBar \u2014 SearchInput is almost always placed as a direct child of `FilterBar`, which provides the surrounding strip, clear-all button, and active-filter state. Do not use SearchInput as a standalone header widget when a full filter strip (with selects etc.) already exists \u2014 compose them together.",
|
|
1769
|
+
"Command \u2014 use `Command` + `CommandInput` when you need a keyboard-navigable command palette or combobox list with grouped items and keyboard selection. `Command` is only meaningful when paired with `CommandList`; SearchInput is the right pick for a plain filter box with no item-selection behavior.",
|
|
1770
|
+
"Select (with showSearch) \u2014 when users must pick a value from a list AND search to narrow it, use `<Select options={...} showSearch>` (which has its own built-in search input). SearchInput is for filtering an external data set, not for value selection from an option list."
|
|
1771
|
+
],
|
|
1279
1772
|
example: `import { SearchInput } from "@godxjp/ui/data-entry";
|
|
1280
1773
|
|
|
1281
1774
|
<SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u30FBID\u3067\u691C\u7D22" value={search} onSearch={setSearch} />`,
|
|
@@ -1508,7 +2001,7 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1508
2001
|
{
|
|
1509
2002
|
name: "Switch",
|
|
1510
2003
|
group: "data-entry",
|
|
1511
|
-
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use
|
|
2004
|
+
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use ChoiceField.",
|
|
1512
2005
|
props: [
|
|
1513
2006
|
{ name: "checked", type: "boolean", description: "Controlled checked state." },
|
|
1514
2007
|
{
|
|
@@ -1524,6 +2017,27 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1524
2017
|
description: "Disable the toggle."
|
|
1525
2018
|
}
|
|
1526
2019
|
],
|
|
2020
|
+
usage: [
|
|
2021
|
+
"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.",
|
|
2022
|
+
"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>.",
|
|
2023
|
+
"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.",
|
|
2024
|
+
"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).",
|
|
2025
|
+
"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.",
|
|
2026
|
+
"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."
|
|
2027
|
+
],
|
|
2028
|
+
useCases: [
|
|
2029
|
+
"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.",
|
|
2030
|
+
"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.",
|
|
2031
|
+
"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).",
|
|
2032
|
+
"Filter toolbar toggle (e.g., 'Show archived') rendered inline next to other filter controls, using size='sm' for density parity with adjacent inputs.",
|
|
2033
|
+
"Preview/demo UI where the switch controls a local display state (dark-mode preview, feature flag preview) with no server persistence."
|
|
2034
|
+
],
|
|
2035
|
+
related: [
|
|
2036
|
+
"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.",
|
|
2037
|
+
"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.",
|
|
2038
|
+
"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.",
|
|
2039
|
+
"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."
|
|
2040
|
+
],
|
|
1527
2041
|
example: `import { Switch, Label } from "@godxjp/ui/data-entry";
|
|
1528
2042
|
|
|
1529
2043
|
<div className="flex items-center gap-2">
|
|
@@ -1547,6 +2061,27 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1547
2061
|
description: "Change handler."
|
|
1548
2062
|
}
|
|
1549
2063
|
],
|
|
2064
|
+
usage: [
|
|
2065
|
+
"DO always wrap Textarea in FormField when it appears in a form \u2014 FormField clones aria-describedby, aria-required, and aria-invalid onto the child, giving error/helper announcements and screen-reader labelling for free. Pass matching id props to both.",
|
|
2066
|
+
"DO use the godx-ui Textarea (`import { Textarea } from '@godxjp/ui/data-entry'`) \u2014 never a raw `<textarea>`. The component applies the `ui-control-multiline` token class that picks up density, focus-ring, and border tokens from the design system.",
|
|
2067
|
+
"DO control the value with `value` + `onChange` in React-managed forms (e.g. Inertia `useForm`). Textarea is a plain `forwardRef` over the native element so it accepts all standard `HTMLTextAreaElement` attributes \u2014 `rows`, `maxLength`, `disabled`, `name`, `placeholder`, `readOnly` all pass through directly.",
|
|
2068
|
+
"DO pass `name` when the textarea sits inside an HTML `<form>` for native form submission or when Inertia's `useForm` destructures field values by key \u2014 the `name` attribute maps the value into the form data bag.",
|
|
2069
|
+
"DON'T apply manual height or padding classes directly on Textarea to simulate a taller field \u2014 use the `rows` prop instead. The component does not auto-resize; if you need auto-grow behaviour you must wire a custom `onInput` handler that adjusts `style.height` explicitly.",
|
|
2070
|
+
"DON'T hand-roll label + error markup next to a bare Textarea. Always use FormField: it injects aria-invalid (red ring on the control), renders a `role='alert'` error paragraph, and links them via aria-describedby automatically."
|
|
2071
|
+
],
|
|
2072
|
+
useCases: [
|
|
2073
|
+
"Free-text memo or note fields on an invoice or transaction detail form \u2014 e.g. '\u5099\u8003 / Notes' that can hold multi-line internal comments alongside structured Invoice fields.",
|
|
2074
|
+
"Rejection reason or approval comment in an admin workflow dialog \u2014 a short-to-medium text block a reviewer types before confirming an action in a Dialog or Sheet.",
|
|
2075
|
+
"Address or multi-line description input on a vendor / partner entity form where a single-line Input would be too restrictive.",
|
|
2076
|
+
"Email body composer or message template editor in a lightweight CRM or notification settings screen where rich text is not required.",
|
|
2077
|
+
"Audit log annotation \u2014 allowing an accountant to attach a plain-text explanation to a manual journal entry or adjustment record."
|
|
2078
|
+
],
|
|
2079
|
+
related: [
|
|
2080
|
+
"Input \u2014 use Input for single-line values (names, amounts, codes). Use Textarea only when the expected value spans multiple lines or could be longer than ~80 characters.",
|
|
2081
|
+
"FormField \u2014 always the parent wrapper for Textarea in forms; provides label, helper text, error message, and injects all required aria attributes onto the Textarea child automatically.",
|
|
2082
|
+
"Select \u2014 when the user must pick from a finite set of multi-line-looking options (e.g. template choices) use Select, not a Textarea presenting options as free text.",
|
|
2083
|
+
"Autocomplete or SearchSelect \u2014 if the multi-line field is actually a tag/token input or a constrained lookup, prefer Autocomplete or SearchSelect over a Textarea that the user types into freely."
|
|
2084
|
+
],
|
|
1550
2085
|
example: `import { Textarea } from "@godxjp/ui/data-entry";
|
|
1551
2086
|
|
|
1552
2087
|
<Textarea id="notes" rows={4} placeholder="\u81EA\u7531\u8A18\u8FF0" value={notes} onChange={(e) => setNotes(e.target.value)} />`,
|
|
@@ -1561,6 +2096,28 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1561
2096
|
{ name: "htmlFor", type: "string", description: "Id of the associated control." },
|
|
1562
2097
|
{ name: "children", type: "ReactNode", description: "Label content." }
|
|
1563
2098
|
],
|
|
2099
|
+
usage: [
|
|
2100
|
+
"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.",
|
|
2101
|
+
'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.',
|
|
2102
|
+
"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.",
|
|
2103
|
+
"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.",
|
|
2104
|
+
"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.",
|
|
2105
|
+
"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."
|
|
2106
|
+
],
|
|
2107
|
+
useCases: [
|
|
2108
|
+
"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.",
|
|
2109
|
+
"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.",
|
|
2110
|
+
"Adding a visible label to a custom or third-party control that accepts an `id` prop but isn't wrapped by FormField or ChoiceField.",
|
|
2111
|
+
"Labelling a Textarea in a free-text form field when FormField's helper/error slots aren't needed, keeping the markup minimal.",
|
|
2112
|
+
"Rendering an accessible label inside a table row where a FormField's block layout would break the inline/grid structure.",
|
|
2113
|
+
"Adding a label to a DatePicker, TimePicker, or ColorPicker inside a simple layout that doesn't need the full FormField wrapper."
|
|
2114
|
+
],
|
|
2115
|
+
related: [
|
|
2116
|
+
"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.",
|
|
2117
|
+
"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.",
|
|
2118
|
+
"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.",
|
|
2119
|
+
"Checkbox \u2014 the most common bare-Label partner; pair with Label via shared useId() id/htmlFor when ChoiceField's layout is too heavy."
|
|
2120
|
+
],
|
|
1564
2121
|
example: `import { Label } from "@godxjp/ui/data-entry";
|
|
1565
2122
|
|
|
1566
2123
|
<Label htmlFor="stackable">\u4F75\u7528\u3092\u8A31\u53EF</Label>`,
|
|
@@ -1584,6 +2141,28 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1584
2141
|
},
|
|
1585
2142
|
{ name: "id", type: "string", description: "Links to a <Label htmlFor>." }
|
|
1586
2143
|
],
|
|
2144
|
+
usage: [
|
|
2145
|
+
"DO pair every standalone Checkbox with a `<Label htmlFor={id}>` \u2014 the id prop on Checkbox must match the htmlFor on Label so screen readers announce the label on focus. Without this pairing the control is inaccessible.",
|
|
2146
|
+
"DO use the controlled pattern (`checked` + `onCheckedChange`) for any form-bound checkbox. `onCheckedChange` receives `boolean | 'indeterminate'` \u2014 always coerce with `!!v` or an explicit guard before storing in state.",
|
|
2147
|
+
"DO use `Checkbox.Group` (alias for CheckboxGroup) with the `options` prop when you have \u22652 choices from an array \u2014 it renders each item inside a `ChoiceField` (label + optional description), generates stable ids automatically, and manages the `string[]` value array. NEVER hand-roll a loop of bare `<Checkbox>` elements for a multi-select list.",
|
|
2148
|
+
"DO pass `name` on `Checkbox.Group` (not on individual checkboxes) when the group must submit as form fields \u2014 the group propagates the name to each internal checkbox so the browser serialises all checked values under that key.",
|
|
2149
|
+
"DON'T use `checked='indeterminate'` on `Checkbox.Group` children \u2014 indeterminate is only meaningful on a parent 'select-all' control you wire manually; the group itself does not auto-compute it.",
|
|
2150
|
+
"DON'T wrap a standalone Checkbox in `ChoiceField` manually \u2014 `ChoiceField` is the internal composition primitive that `Checkbox.Group` uses. For a single boolean with a label, use `<div className='flex items-center gap-2'><Checkbox id='x' .../><Label htmlFor='x'>...</Label></div>` as shown in the catalog example; for a full labelled-checkbox with description, use `ChoiceField` directly only if you need a one-off item outside a group."
|
|
2151
|
+
],
|
|
2152
|
+
useCases: [
|
|
2153
|
+
"A 'Select all' / bulk-action row above a DataTable \u2014 standalone Checkbox with `checked='indeterminate'` when some (not all) rows are selected, toggling between all-selected and none-selected.",
|
|
2154
|
+
"A multi-step filter panel (e.g. filter invoices by payment status: Paid, Unpaid, Overdue) \u2014 `Checkbox.Group` with `options` prop and `orientation='vertical'`, controlled value wired to FilterBar state.",
|
|
2155
|
+
"Confirmation or consent acknowledgement before a destructive action in a Dialog \u2014 standalone Checkbox with controlled state used to enable/disable the confirm Button.",
|
|
2156
|
+
"Settings panel where each feature flag is a boolean toggle with a description line \u2014 `Checkbox.Group` with options carrying a `description` field so each row renders label + subtext via ChoiceField.",
|
|
2157
|
+
"Bulk-edit form row in an accounting ledger (e.g. 'Apply to all selected entries') \u2014 standalone Checkbox with name + value inside a `<form>` for native HTML form submission.",
|
|
2158
|
+
"Onboarding checklist (e.g. 'I have read the terms', 'I consent to data processing') with multiple distinct items whose values are independent \u2014 two separate standalone Checkboxes, each with their own id/state, not a Checkbox.Group (since each item maps to a different boolean field)."
|
|
2159
|
+
],
|
|
2160
|
+
related: [
|
|
2161
|
+
"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.",
|
|
2162
|
+
"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.",
|
|
2163
|
+
"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.",
|
|
2164
|
+
"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."
|
|
2165
|
+
],
|
|
1587
2166
|
example: `import { Checkbox, Label } from "@godxjp/ui/data-entry";
|
|
1588
2167
|
|
|
1589
2168
|
<div className="flex items-center gap-2">
|
|
@@ -1616,6 +2195,28 @@ export function PrioritySelect({ value, onValueChange }) {
|
|
|
1616
2195
|
description: "Layout direction."
|
|
1617
2196
|
}
|
|
1618
2197
|
],
|
|
2198
|
+
usage: [
|
|
2199
|
+
"DO use the `options` prop for the data-driven path: pass `{ label, value, disabled?, description? }[]` and RadioGroup renders every option as a correctly labelled ChoiceField automatically \u2014 never hand-roll Radio.Item + Label pairs in a loop yourself.",
|
|
2200
|
+
"DO provide `name` whenever the group lives inside an HTML form: Radix renders a hidden `<input name={name}>` carrying the selected string value, making the field natively form-submittable without a separate hidden input.",
|
|
2201
|
+
"DO use controlled mode (`value` + `onValueChange`) for any form managed by useForm or a state manager. Use `defaultValue` only for truly uncontrolled UI where you never need to read the value in code.",
|
|
2202
|
+
"DO NOT reach for children / manual composition unless the options list is dynamic-JSX (e.g. each item needs a custom rendered label with an icon). When you do compose children manually, wrap each Radio.Item in a ChoiceField \u2014 rendering a bare Radio.Item without ChoiceField skips the label and breaks a11y.",
|
|
2203
|
+
"DO NOT use RadioGroup when the user may select zero or multiple items \u2014 that is CheckboxGroup. RadioGroup enforces exactly one selection at all times (or none before first interaction when uncontrolled).",
|
|
2204
|
+
"A11y: the Radix root emits `role=radiogroup`; each item gets `role=radio` and is keyboard-navigable with arrow keys. Never suppress `name` on the Root when inside a form \u2014 without it the hidden input is unnamed and won't submit."
|
|
2205
|
+
],
|
|
2206
|
+
useCases: [
|
|
2207
|
+
"Selecting a single billing cycle (monthly / quarterly / annual) in an invoice or subscription settings form where all 2-4 options must be visible at once.",
|
|
2208
|
+
"Choosing a report output format (PDF / CSV / Excel) before triggering an async export job \u2014 keeps all options scannable without opening a dropdown.",
|
|
2209
|
+
"Picking a transaction type (income / expense / transfer) on an accounting entry form where the choice changes which subsequent fields are shown.",
|
|
2210
|
+
"Selecting a sync trigger mode (first purchase / birthday / manual) in a campaign or automation settings panel \u2014 matches the catalog example exactly.",
|
|
2211
|
+
"Filtering a compact inline control (horizontal orientation) such as date granularity (day / week / month) inside a dashboard filter bar where a full Select dropdown would be over-engineered.",
|
|
2212
|
+
"Choosing an approval status (pending / approved / rejected) on an admin detail sheet where all states must be visible so reviewers can compare them without interaction."
|
|
2213
|
+
],
|
|
2214
|
+
related: [
|
|
2215
|
+
"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.",
|
|
2216
|
+
"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.",
|
|
2217
|
+
"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.",
|
|
2218
|
+
"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."
|
|
2219
|
+
],
|
|
1619
2220
|
example: `import { RadioGroup } from "@godxjp/ui/data-entry";
|
|
1620
2221
|
|
|
1621
2222
|
<RadioGroup value={trigger} onValueChange={setTrigger} orientation="horizontal" options={[
|
|
@@ -1748,6 +2349,27 @@ export function InvoiceDueDateField() {
|
|
|
1748
2349
|
description: "form = Radix Dialog (\xD7 close); confirm = AlertDialog (no \xD7)."
|
|
1749
2350
|
}
|
|
1750
2351
|
],
|
|
2352
|
+
usage: [
|
|
2353
|
+
'DO use `mode="confirm"` (alertdialog role, no \xD7 button) for all destructive or irreversible actions \u2014 deletes, voids, bulk-overwrite \u2014 and `mode="form"` (default, dialog role, \xD7 button shown) for all data-entry or wizard steps. Never toggle these manually or add a custom close icon in confirm mode.',
|
|
2354
|
+
"DO use `Dialog.Confirm` (or `DialogConfirm`) as the pre-built preset for confirm flows: pass `open`, `onOpenChange`, `title`, `description`, `onConfirm`, `pending`, and optionally `confirmPhrase` for type-to-confirm friction (GitHub/Stripe style). This eliminates boilerplate for the most common confirm pattern.",
|
|
2355
|
+
"DO always control open state via `open` + `onOpenChange`. Dialog has no uncontrolled shortcut \u2014 omitting `open` means the trigger alone drives state, which is fine for simple trigger-only cases, but any async submission flow must use controlled state so you can hold the dialog open while `pending=true` and close it only on success.",
|
|
2356
|
+
"DO include `DialogHeader` with `DialogTitle` (and optionally `DialogDescription`) inside every `DialogContent`. Radix requires an accessible title for screen readers; omitting it triggers a console warning and breaks a11y. In confirm mode `DialogTitle` maps to `AlertDialogPrimitive.Title` automatically.",
|
|
2357
|
+
"DON'T put `DialogContent` outside a `Dialog` (or `DialogRoot`) \u2014 the `mode` context won't be set and the sub-parts will render as the wrong Radix primitive (dialog vs alertdialog), breaking keyboard focus trap and role semantics.",
|
|
2358
|
+
'DON\'T add a manual \xD7 close button in confirm mode \u2014 `DialogContent` only renders the `X` button when `mode="form"` (`showCloseButton` defaults to `true`). For confirm flows, pass `showCloseButton={false}` or use `mode="confirm"`, which suppresses it automatically. Pressing Escape still closes both modes via Radix defaults.'
|
|
2359
|
+
],
|
|
2360
|
+
useCases: [
|
|
2361
|
+
'Inline form dialog \u2014 create or edit a record (invoice line, supplier, coupon) without navigating away. Use `mode="form"`, place `FormField`/`Input`/`Select` inside `DialogContent`, and wire the submit button to your mutation; hold `open` while `pending` to prevent double-submit.',
|
|
2362
|
+
"Destructive confirm \u2014 delete a journal entry, void an invoice, or remove a user. Use `Dialog.Confirm` with `variant=\"destructive\"` and optionally `confirmPhrase` (the record name or 'DELETE') to add type-to-confirm friction for high-stakes ops.",
|
|
2363
|
+
"Wizard / multi-step flow \u2014 step through entity setup (legal entity \u2192 fiscal year \u2192 opening balances) using a single Dialog whose `DialogContent` conditionally renders different step panels. Control which step is shown in local state; use `keepOpenOnConfirm` if the confirm action advances steps rather than closing.",
|
|
2364
|
+
'Read-only detail popup \u2014 show a full transaction audit trail, attachment preview, or approval history in a modal without leaving the list page. Use `mode="form"` with no `DialogFooter` action buttons, just a close trigger.',
|
|
2365
|
+
"Batch action confirmation \u2014 confirm bulk-approve or bulk-archive of selected rows. Use `Dialog.Confirm` wired to a selection count message; pass `pending` from the mutation to disable the confirm button while the batch runs."
|
|
2366
|
+
],
|
|
2367
|
+
related: [
|
|
2368
|
+
"Sheet \u2014 use Sheet instead of Dialog when the content is a slide-in panel (filters, detail sidebar, settings drawer). Sheet uses `side` prop and is better suited for wide filter forms or contextual detail panels that don't demand full focus interruption.",
|
|
2369
|
+
"Alert \u2014 use Alert for inline, non-modal status messages (validation errors, success banners on the page). Dialog is modal and focus-trapping; Alert is inline and never blocks interaction.",
|
|
2370
|
+
"Popover \u2014 use Popover for lightweight non-modal overlays anchored to a trigger (quick-edit a single field, tooltip-style confirmation for low-stakes actions). Dialog is full-modal; Popover stays near its trigger and doesn't dim the page.",
|
|
2371
|
+
"MutationFeedback \u2014 use MutationFeedback for toast/inline feedback after the Dialog closes, not inside it. Putting a success toast inside a Dialog that is about to unmount causes it to disappear immediately; emit the feedback after `onOpenChange(false)` resolves."
|
|
2372
|
+
],
|
|
1751
2373
|
example: `import { useState } from "react";
|
|
1752
2374
|
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
|
|
1753
2375
|
import { Button } from "@godxjp/ui/general";
|
|
@@ -1786,6 +2408,27 @@ function CreateDialog() {
|
|
|
1786
2408
|
description: "Open-state change handler."
|
|
1787
2409
|
}
|
|
1788
2410
|
],
|
|
2411
|
+
usage: [
|
|
2412
|
+
"DO use all named sub-parts in order: Sheet (root) > SheetTrigger (opener) > SheetContent (panel) > SheetHeader > SheetTitle (required for a11y \u2014 maps to Radix DialogPrimitive.Title, announced as the accessible name) > optional SheetDescription > body content > SheetFooter. Never skip SheetTitle inside an open SheetContent.",
|
|
2413
|
+
"DO control state explicitly with open + onOpenChange on Sheet root when you need to close programmatically (e.g. after form submit). Uncontrolled (no props) works for simple trigger-only cases but gives you no hook to reset form state on close.",
|
|
2414
|
+
"DO use SheetTrigger asChild to wrap a Button or other interactive element \u2014 this avoids a nested <button> in the DOM. Never render a raw <button> as a direct child of SheetTrigger.",
|
|
2415
|
+
"DO use SheetFooter (renders at the bottom via mt-auto) for primary/cancel action Buttons. Never float action Buttons inside the body \u2014 they will not stick to the panel bottom.",
|
|
2416
|
+
"DON'T set showCloseButton={false} on SheetContent unless you provide your own SheetClose element; omitting both leaves users with no keyboard-accessible close path and breaks a11y.",
|
|
2417
|
+
"DON'T put a Sheet inside a Dialog (nested Radix portals conflict). If you need a slide-over triggered from within a modal, close the Dialog first, then open the Sheet."
|
|
2418
|
+
],
|
|
2419
|
+
useCases: [
|
|
2420
|
+
"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.",
|
|
2421
|
+
"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.",
|
|
2422
|
+
"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}.",
|
|
2423
|
+
"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.",
|
|
2424
|
+
"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."
|
|
2425
|
+
],
|
|
2426
|
+
related: [
|
|
2427
|
+
"Dialog \u2014 use Dialog (centered modal) when the action is destructive, requires full user focus, or needs a confirm/alertdialog (mode='confirm'). Use Sheet when the user benefits from seeing the page content behind the slide-over (filters, detail peek, quick-edit).",
|
|
2428
|
+
"FilterBar/FilterGroup \u2014 use FilterBar for inline persistent filter controls above a DataTable (no overlay). Use Sheet when the filter set is large (>4 controls) or on mobile where inline controls collapse poorly.",
|
|
2429
|
+
"Popover \u2014 use Popover for lightweight, anchor-positioned context menus or single-control overlays (date picker, color picker). Use Sheet when the panel has a header, multiple fields, or footer actions that need a dedicated panel.",
|
|
2430
|
+
"SplitPane \u2014 use SplitPane for a persistent side-by-side layout where both panes are always visible. Use Sheet when the secondary panel is transient and should overlay the primary content."
|
|
2431
|
+
],
|
|
1789
2432
|
example: `import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from "@godxjp/ui/feedback";
|
|
1790
2433
|
import { Button } from "@godxjp/ui/general";
|
|
1791
2434
|
|
|
@@ -1821,6 +2464,28 @@ import { Button } from "@godxjp/ui/general";
|
|
|
1821
2464
|
description: "Override or hide (false) the icon."
|
|
1822
2465
|
}
|
|
1823
2466
|
],
|
|
2467
|
+
usage: [
|
|
2468
|
+
'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 variant="destructive"><Alert.Content><Alert.Title>Error</Alert.Title><Alert.Description>{msg}</Alert.Description></Alert.Content><Alert.Actions><Button \u2026/></Alert.Actions></Alert>`.',
|
|
2469
|
+
"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.",
|
|
2470
|
+
'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.',
|
|
2471
|
+
'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.',
|
|
2472
|
+
'DON\'T suppress the icon with `icon={false}` unless there is a deliberate design reason; the icon is the primary a11y cue for sighted users since the root already carries `role="alert"` for screen readers.',
|
|
2473
|
+
"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."
|
|
2474
|
+
],
|
|
2475
|
+
useCases: [
|
|
2476
|
+
'Page-level error banner after a form submission fails server-side validation \u2014 `variant="destructive"` with `Alert.Title` summarising the error and `Alert.Description` listing field issues, paired with `onDismiss` so the user can clear it.',
|
|
2477
|
+
"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.",
|
|
2478
|
+
'Success confirmation banner rendered after a bulk-import job completes and the user returns to the list page \u2014 `variant="success"` with `Alert.Description` showing the record count imported.',
|
|
2479
|
+
"TanStack Query data-fetch failure inside a Card body \u2014 use `<Alert.QueryError error={error} onRetry={refetch} />` instead of writing a custom error state.",
|
|
2480
|
+
"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.",
|
|
2481
|
+
'Dismissible billing-overdue notice at the top of the dashboard \u2014 `variant="destructive"` with `onDismiss` that sets a session flag so it does not reappear until the next login.'
|
|
2482
|
+
],
|
|
2483
|
+
related: [
|
|
2484
|
+
"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.",
|
|
2485
|
+
"MutationFeedback \u2014 use when you want inline success/error feedback tightly coupled to a form mutation's state (renders inline below the submit button). Alert requires you to manage show/hide state yourself.",
|
|
2486
|
+
"DataState \u2014 use for full query lifecycle (loading skeleton + empty state + error) inside a data-fetching section. Alert.QueryError is the error sub-component DataState uses internally; prefer DataState when you also need the loading/empty states.",
|
|
2487
|
+
"EmptyState \u2014 use for the zero-data case inside a list or table section, not for errors or warnings. Alert is for status messages; EmptyState is for the absence of data."
|
|
2488
|
+
],
|
|
1824
2489
|
example: `import { Alert, AlertTitle, AlertDescription } from "@godxjp/ui/feedback";
|
|
1825
2490
|
|
|
1826
2491
|
<Alert variant="warning">
|
|
@@ -1843,6 +2508,28 @@ import { Button } from "@godxjp/ui/general";
|
|
|
1843
2508
|
description: "Columns in header + body."
|
|
1844
2509
|
}
|
|
1845
2510
|
],
|
|
2511
|
+
usage: [
|
|
2512
|
+
"DO use SkeletonTable as the pre-mount placeholder \u2014 either as a ternary fallback (`{!data ? <SkeletonTable rows={10} columns={6} /> : <DataTable \u2026 />}`) for Inertia deferred props, or as the `skeleton` prop of `DataState` (`<DataState query={q} skeleton={<SkeletonTable />} \u2026>`). It is NOT for in-table loading; once DataTable has mounted use its own `loading` prop instead.",
|
|
2513
|
+
"DO match rows/columns to the final DataTable layout: pass `rows` equal to your expected page size and `columns` equal to your column count so the skeleton doesn't visually jump on hydration. Defaults are rows=8, columns=5.",
|
|
2514
|
+
"DO NOT use SkeletonTable when data is already present but refetching \u2014 use `DataTable loading={isFetching}` for in-table refetch states. SkeletonTable is only for the initial pre-mount gap before DataTable is rendered.",
|
|
2515
|
+
"DO NOT wrap SkeletonTable in a Card \u2014 it renders its own header + body structure matching DataTable's DOM. Placing it inside CardContent adds unwanted padding around the skeleton rail.",
|
|
2516
|
+
'The root element carries `aria-busy="true"` automatically \u2014 do not add a second aria-busy on a wrapper. Screen readers announce the loading state correctly without extra markup.',
|
|
2517
|
+
"Import from `@godxjp/ui/feedback` (not `@godxjp/ui/admin`). Both paths resolve but the canonical export is `feedback`."
|
|
2518
|
+
],
|
|
2519
|
+
useCases: [
|
|
2520
|
+
"Inertia deferred props: the server streams the page shell immediately and defers the table data; render SkeletonTable until the prop arrives (`{!invoices ? <SkeletonTable rows={20} columns={7} /> : <DataTable data={invoices} columns={columns} />}`).",
|
|
2521
|
+
"TanStack Query initial load via DataState: pass SkeletonTable as the `skeleton` prop so DataState shows the correct table shape during the query's loading state before switching to the populated DataTable.",
|
|
2522
|
+
"Filter / search reset that unmounts and remounts DataTable: briefly show SkeletonTable while the new dataset fetches, preventing a flash of the empty state before results arrive.",
|
|
2523
|
+
"Admin list pages (invoices, journal entries, partners) where the table has a known column count \u2014 tune `columns` to match so column widths feel stable and don't reflow on hydration.",
|
|
2524
|
+
"Page-level Suspense boundaries: use SkeletonTable as the `fallback` of a React Suspense wrapping a lazy-loaded data table component.",
|
|
2525
|
+
"Route prefetch / navigation transitions: render SkeletonTable in the destination slot while Inertia visits are in-flight, keeping perceived layout stable."
|
|
2526
|
+
],
|
|
2527
|
+
related: [
|
|
2528
|
+
"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.",
|
|
2529
|
+
"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.",
|
|
2530
|
+
"SkeletonCard \u2014 sibling skeleton shaped like a StatCard tile; use inside a ResponsiveGrid to placeholder KPI dashboard cards, not tabular data.",
|
|
2531
|
+
"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."
|
|
2532
|
+
],
|
|
1846
2533
|
example: `import { SkeletonTable } from "@godxjp/ui/feedback";
|
|
1847
2534
|
|
|
1848
2535
|
{!coupons ? <SkeletonTable rows={10} columns={6} /> : <DataTable data={coupons} columns={columns} />}`,
|
|
@@ -1852,8 +2539,29 @@ import { Button } from "@godxjp/ui/general";
|
|
|
1852
2539
|
{
|
|
1853
2540
|
name: "SkeletonCard",
|
|
1854
2541
|
group: "feedback",
|
|
1855
|
-
tagline: "Loading placeholder shaped like a
|
|
2542
|
+
tagline: "Loading placeholder shaped like a StatCard tile. Use inside a ResponsiveGrid while KPIs load.",
|
|
1856
2543
|
props: [],
|
|
2544
|
+
usage: [
|
|
2545
|
+
"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).",
|
|
2546
|
+
"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.",
|
|
2547
|
+
"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.",
|
|
2548
|
+
"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.",
|
|
2549
|
+
`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.`,
|
|
2550
|
+
"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."
|
|
2551
|
+
],
|
|
2552
|
+
useCases: [
|
|
2553
|
+
"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.",
|
|
2554
|
+
"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.",
|
|
2555
|
+
"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.",
|
|
2556
|
+
"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.",
|
|
2557
|
+
"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."
|
|
2558
|
+
],
|
|
2559
|
+
related: [
|
|
2560
|
+
"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.",
|
|
2561
|
+
"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.",
|
|
2562
|
+
"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.",
|
|
2563
|
+
"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."
|
|
2564
|
+
],
|
|
1857
2565
|
example: `import { SkeletonCard } from "@godxjp/ui/feedback";
|
|
1858
2566
|
import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
1859
2567
|
|
|
@@ -1874,6 +2582,26 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
|
|
|
1874
2582
|
},
|
|
1875
2583
|
{ name: "richColors", type: "boolean", description: "Enable Sonner rich variant colours." }
|
|
1876
2584
|
],
|
|
2585
|
+
usage: [
|
|
2586
|
+
"DO: Mount exactly ONE `<Toaster richColors />` at the app root (e.g. inside your layout or AppShell children). Multiple mounts create duplicate toast stacks \u2014 there is no provider context, only DOM portals.",
|
|
2587
|
+
'DO: Import `toast` from `"sonner"` directly (not from `@godxjp/ui`) to fire toasts anywhere: `toast.success(\u2026)`, `toast.error(\u2026)`, `toast.warning(\u2026)`, `toast.info(\u2026)`, `toast.loading(\u2026)`, `toast.promise(\u2026)`.',
|
|
2588
|
+
"DON'T: Try to import a `toast` helper from `@godxjp/ui/feedback` \u2014 it does not exist. The component re-exports only the `Toaster` mount; the imperative API lives in the `sonner` package.",
|
|
2589
|
+
"DO: Let the wrapper handle theming \u2014 it uses `useDocumentTheme()` to sync with the document `dark` class and `prefers-color-scheme` automatically. Never pass a hardcoded `theme` prop unless you are deliberately overriding.",
|
|
2590
|
+
"DON'T: Use `Toaster` for persistent errors or blocking confirmations. Toasts auto-dismiss; they are not a substitute for `Alert` (inline persistent warnings) or `Dialog` (decisions requiring user input).",
|
|
2591
|
+
"DO: Pass `position` to relocate the stack if a persistent sidebar/footer would obscure the default `bottom-right`. The wrapper already sets a safe `mobileOffset`; don't add redundant mobile offsets unless your layout differs."
|
|
2592
|
+
],
|
|
2593
|
+
useCases: [
|
|
2594
|
+
'After a successful form save (invoice, journal entry, vendor record) \u2014 show `toast.success("\u4FDD\u5B58\u3057\u307E\u3057\u305F")` to confirm without blocking navigation.',
|
|
2595
|
+
'After a background job is enqueued (e.g. bulk sync or export) \u2014 show `toast.info("\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3092\u958B\u59CB\u3057\u307E\u3057\u305F")` then later update with `toast.promise()` to track completion.',
|
|
2596
|
+
"Mutation error fallback when the error is transient and retrying is the right UX \u2014 show `toast.error(message)` instead of replacing page content; reserve `MutationFeedback` for inline, persistent error display inside a form.",
|
|
2597
|
+
'Soft destructive action confirmation outcome \u2014 e.g. "\u524A\u9664\u3057\u307E\u3057\u305F" after an item is removed, paired with an undo action via `toast("\u2026", { action: { label: \'\u5143\u306B\u623B\u3059\', onClick: undo } })`.',
|
|
2598
|
+
'OAuth / session expiry warnings \u2014 surface a brief `toast.warning("\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u8FD1\u3065\u3044\u3066\u3044\u307E\u3059")` without interrupting the user\'s current form state.'
|
|
2599
|
+
],
|
|
2600
|
+
related: [
|
|
2601
|
+
"Alert \u2014 use for persistent, inline feedback that must stay visible (validation summaries, page-level warnings, destructive notices). Unlike Toaster, Alert does not auto-dismiss and lives in the document flow.",
|
|
2602
|
+
"MutationFeedback \u2014 use when you have a TanStack `useMutation` result and want an inline error + retry UI inside a form or card. Renders nothing on success/idle; pairs naturally with a `toast.success` in `onSuccess`.",
|
|
2603
|
+
"Dialog \u2014 use when the user must make a conscious decision (confirm delete, resolve conflict) before proceeding. Toaster toasts are fire-and-forget; Dialog blocks until the user responds."
|
|
2604
|
+
],
|
|
1877
2605
|
example: `// app root \u2014 mount once
|
|
1878
2606
|
import { Toaster } from "@godxjp/ui/feedback";
|
|
1879
2607
|
<>{children}<Toaster richColors /></>
|
|
@@ -1889,8 +2617,13 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
1889
2617
|
{
|
|
1890
2618
|
name: "Tabs",
|
|
1891
2619
|
group: "navigation",
|
|
1892
|
-
tagline: "Radix tab container.
|
|
2620
|
+
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.",
|
|
1893
2621
|
props: [
|
|
2622
|
+
{
|
|
2623
|
+
name: "items",
|
|
2624
|
+
type: "{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean }[]",
|
|
2625
|
+
description: "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels."
|
|
2626
|
+
},
|
|
1894
2627
|
{ name: "value", type: "string", description: "Controlled active tab key." },
|
|
1895
2628
|
{ name: "defaultValue", type: "string", description: "Uncontrolled initial tab key." },
|
|
1896
2629
|
{
|
|
@@ -1899,16 +2632,35 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
1899
2632
|
description: "Active-tab change handler."
|
|
1900
2633
|
}
|
|
1901
2634
|
],
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2635
|
+
usage: [
|
|
2636
|
+
"DO pass `items` when all tab content is known up front \u2014 each item needs a unique `value`, trigger `label`, and panel `content`.",
|
|
2637
|
+
'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.',
|
|
2638
|
+
"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.",
|
|
2639
|
+
"DO use `variant` on Tabs when using `items`; when composing manually, set `variant` on `TabsList`.",
|
|
2640
|
+
'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.',
|
|
2641
|
+
"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."
|
|
2642
|
+
],
|
|
2643
|
+
useCases: [
|
|
2644
|
+
"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.",
|
|
2645
|
+
"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`.",
|
|
2646
|
+
'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.',
|
|
2647
|
+
"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.",
|
|
2648
|
+
"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."
|
|
2649
|
+
],
|
|
2650
|
+
related: [
|
|
2651
|
+
"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.",
|
|
2652
|
+
"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.",
|
|
2653
|
+
"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."
|
|
2654
|
+
],
|
|
2655
|
+
example: `import { Tabs } from "@godxjp/ui/navigation";
|
|
2656
|
+
|
|
2657
|
+
<Tabs
|
|
2658
|
+
defaultValue="overview"
|
|
2659
|
+
items={[
|
|
2660
|
+
{ value: "overview", label: "\u6982\u8981", content: "\u6982\u8981\u30B3\u30F3\u30C6\u30F3\u30C4" },
|
|
2661
|
+
{ value: "history", label: "\u5C65\u6B74", content: "\u5C65\u6B74\u30B3\u30F3\u30C6\u30F3\u30C4" },
|
|
2662
|
+
]}
|
|
2663
|
+
/>`,
|
|
1912
2664
|
storyPath: "navigation/Tabs.stories.tsx",
|
|
1913
2665
|
rules: []
|
|
1914
2666
|
},
|
|
@@ -1930,6 +2682,28 @@ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
|
|
|
1930
2682
|
},
|
|
1931
2683
|
{ name: "onClear", type: "() => void", description: "Clear-all handler." }
|
|
1932
2684
|
],
|
|
2685
|
+
usage: [
|
|
2686
|
+
"DO place FilterBar ABOVE the table Card in the page layout \u2014 never inside CardContent or CardContent flush. It carries its own padding-block via CSS tokens; nesting it inside a flush card strips that spacing and breaks the visual rhythm. The correct pattern is: <PageInset><FilterBar \u2026/></PageInset> then a sibling <Card><CardContent flush><DataTable /></CardContent></Card>.",
|
|
2687
|
+
"DO wrap every labelled filter control (Select, DatePicker, DateRangePicker, etc.) in a FilterGroup with a descriptive label prop. A bare Select dropped directly in FilterBar has no label anchor and breaks the stacked-column layout on mobile. SearchInput is the one exception \u2014 it does NOT need a FilterGroup wrapper because it carries its own visible placeholder.",
|
|
2688
|
+
"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.",
|
|
2689
|
+
"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'}.",
|
|
2690
|
+
"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.",
|
|
2691
|
+
"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."
|
|
2692
|
+
],
|
|
2693
|
+
useCases: [
|
|
2694
|
+
"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.",
|
|
2695
|
+
"Partner / customer ledger list: FilterGroup for account type (AR/AP), FilterGroup for legal entity, SearchInput for partner name \u2014 clear-all resets the entity switcher back to 'all' as well as local filter state.",
|
|
2696
|
+
"Transaction history with multi-dimension filtering: date range + account Select + currency Select \u2014 FilterBar's responsive flex-wrap layout automatically stacks filters vertically on mobile and flows them into a row on \u2265640px without any custom CSS.",
|
|
2697
|
+
"Admin user / permission list: SearchInput for email search + FilterGroup wrapping a Select for role \u2014 the clear button becomes visible only when either control departs from its default, so the toolbar stays clean on first load.",
|
|
2698
|
+
"Report parameter bar above a read-only DataTable: two DatePickers inside FilterGroups set the report start/end dates, a Select picks the reporting entity \u2014 hasActiveFilters is always true once the user first applies the report, giving a one-click reset.",
|
|
2699
|
+
"Any list page migrating away from hand-rolled Tailwind flex rows with inline labels \u2014 drop existing label+control pairs into FilterGroup to get consistent label colour (muted-foreground), gap, and the responsive stacking behaviour for free."
|
|
2700
|
+
],
|
|
2701
|
+
related: [
|
|
2702
|
+
"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.",
|
|
2703
|
+
"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.",
|
|
2704
|
+
"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.",
|
|
2705
|
+
"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."
|
|
2706
|
+
],
|
|
1933
2707
|
example: `import { FilterBar, FilterGroup } from "@godxjp/ui/navigation";
|
|
1934
2708
|
import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@godxjp/ui/data-entry";
|
|
1935
2709
|
|
|
@@ -1961,6 +2735,27 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
1961
2735
|
},
|
|
1962
2736
|
{ name: "children", type: "ReactNode", required: true, description: "The filter control." }
|
|
1963
2737
|
],
|
|
2738
|
+
usage: [
|
|
2739
|
+
"DO: Always nest FilterGroup directly inside FilterBar \u2014 it renders a vertical label+control stack (ui-stack-xs) that aligns with FilterBar's flex-wrap row layout. Never use FilterGroup as a standalone wrapper outside FilterBar.",
|
|
2740
|
+
"DO: Pass exactly ONE filter control as children (Select, DatePicker, DateRangePicker, SearchSelect, etc.). FilterGroup is a single-control labeled slot, not a multi-control panel \u2014 it renders the label div above the control and sizes the slot to full-width mobile / auto desktop.",
|
|
2741
|
+
"DON'T: Pass a raw label string that contains complex JSX beyond text \u2014 label is ReactNode but is styled as muted small-size text (ui-filter-label). Do not add Tooltip or interactive elements inside the label as it is purely presentational.",
|
|
2742
|
+
"DON'T: Place FilterGroup inside CardContent (including flush variant) \u2014 this breaks horizontal padding. FilterBar (and thus all its FilterGroups) must sit above the table Card as a standalone block. Page order: KPIs \u2192 FilterBar (containing FilterGroups) \u2192 Card wrapping DataTable.",
|
|
2743
|
+
"DO: Use FilterGroup for every labelled filter slot; do NOT hand-roll a label+control pair with a div. The label placement, color (muted-foreground), and font-size are token-driven through ui-filter-label \u2014 duplicating this with raw Tailwind will drift from the design.",
|
|
2744
|
+
"A11y: label is rendered as a visible div, not a form <label> element \u2014 it is not wired to the child control's id. If the child control (e.g. Select) has its own accessible label, that is sufficient. Do not add htmlFor on the FilterGroup label; instead ensure the inner control has aria-label or its own Label via the control's API."
|
|
2745
|
+
],
|
|
2746
|
+
useCases: [
|
|
2747
|
+
"A list page for invoices/journal entries where each column value can be filtered: wrap each Select (status, period, entity) in its own FilterGroup inside a FilterBar placed above the DataTable card.",
|
|
2748
|
+
"An admin transaction log page with a date range picker and a partner Select \u2014 each gets its own FilterGroup label so users can scan 'Period' / 'Partner' labels horizontally before interacting.",
|
|
2749
|
+
"A report filter panel that shows 'Fiscal Year', 'Department', and 'Account Type' dropdowns \u2014 FilterGroups communicate the semantic name of each filter without requiring the controls themselves to have visible labels (where Select placeholder alone is ambiguous).",
|
|
2750
|
+
"Mobile-first responsive filter strip: FilterBar + FilterGroups stack vertically on narrow screens (full-width per FilterGroup) and wrap into a horizontal row at sm:, making them safe for 320 px viewports without extra breakpoint overrides.",
|
|
2751
|
+
"When a filter control (e.g. DateRangePicker) has no intrinsic visible label of its own \u2014 wrapping it in FilterGroup provides the visible label without hand-rolling a div/label pattern that would differ from design tokens."
|
|
2752
|
+
],
|
|
2753
|
+
related: [
|
|
2754
|
+
"FilterBar \u2014 the required parent container. FilterGroup has no useful meaning outside FilterBar; FilterBar also owns the clear-all button (onClear + hasActiveFilters). Always compose FilterGroup inside FilterBar, never standalone.",
|
|
2755
|
+
"SearchInput \u2014 sits directly inside FilterBar as a sibling to FilterGroup (not wrapped in FilterGroup) because it is self-labelling via its placeholder and search icon. Use FilterGroup only for controls that need a separate visible label.",
|
|
2756
|
+
"Select \u2014 the canonical child for FilterGroup when filtering by a categorical value (status, type, entity). Pair with FilterGroup for the label; do not hand-roll a label+Select div.",
|
|
2757
|
+
"Tabs \u2014 an alternative filtering/scoping pattern when the filter has exactly 2-4 mutually exclusive states and you want them visible as persistent tabs rather than a dropdown. Use Tabs+TabsList above the table instead of FilterBar+FilterGroup+Select when the values are few and well-known."
|
|
2758
|
+
],
|
|
1964
2759
|
example: `import { FilterGroup } from "@godxjp/ui/navigation";
|
|
1965
2760
|
|
|
1966
2761
|
<FilterGroup label="\u30B9\u30B3\u30FC\u30D7"><Select>{/* ... */}</Select></FilterGroup>`,
|
|
@@ -1991,6 +2786,27 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
1991
2786
|
description: "Page / page-size change handler."
|
|
1992
2787
|
}
|
|
1993
2788
|
],
|
|
2789
|
+
usage: [
|
|
2790
|
+
"DO always control Pagination externally: store `current` and `pageSize` in React state (or URL params), and update both in the `onChange(page, pageSize)` callback. Pagination is fully controlled \u2014 it has no internal state and will not move unless `current` changes.",
|
|
2791
|
+
"DO pass `total` as the raw item count (not page count). The component computes `Math.ceil(total / pageSize)` internally; passing a pre-computed page count as `total` will over-paginate.",
|
|
2792
|
+
"DO use `showSizeChanger` together with `pageSizeOptions` when the user needs density control (default options are [10, 20, 50, 100]). When `showSizeChanger` is omitted the page-size Select is not rendered at all \u2014 do NOT hand-roll your own Select beside Pagination.",
|
|
2793
|
+
"DO use `simple` mode for compact contexts (mobile, sidebars, sheet footers) \u2014 it renders Prev / `n / total` / Next with no page-number buttons. Use the full form for primary admin list pages.",
|
|
2794
|
+
"DO use `showTotal` to surface item counts: pass `true` for the built-in i18n label, or a function `(total, [from, to]) => ReactNode` for a custom range label like '1\u201310 of 342 invoices'. Never hard-code a total string beside the component.",
|
|
2795
|
+
"DON'T use Pagination for cursor- or infinite-scroll-based lists. Pagination is strictly offset/page-based (`current` is a page number). For cursor pagination inside a DataTable use `DataTable.Pagination`; for infinite scroll use `InfiniteQueryState`."
|
|
2796
|
+
],
|
|
2797
|
+
useCases: [
|
|
2798
|
+
"Standalone offset-paginated admin list pages (e.g. invoice list, customer list, transaction history) rendered outside DataTable \u2014 place Pagination below the table card, outside the card border, with `showTotal` and optionally `showSizeChanger`.",
|
|
2799
|
+
"Search results pages where the backend accepts `page` + `per_page` query parameters and returns a total count \u2014 wire `current` and `pageSize` to URL search params so the URL is shareable and browser-back works.",
|
|
2800
|
+
"Reports and filtered data grids where the user needs to export 'all selected pages': `showTotal` with a custom function lets you show '1\u201350 of 1 200 rows' so the user understands the scope before exporting.",
|
|
2801
|
+
"Compact modal or sheet footers with a long list (e.g. selecting from a product catalog inside a dialog) \u2014 use `simple` mode to save horizontal space while keeping navigation accessible.",
|
|
2802
|
+
"DataTable instances where the server returns an offset-based total and `DataTable.Pagination` is not being used: attach a standalone Pagination below the card and pass the same `page` / `pageSize` state to both the DataTable `data` prop and the API fetch."
|
|
2803
|
+
],
|
|
2804
|
+
related: [
|
|
2805
|
+
"DataTable.Pagination \u2014 use instead of standalone Pagination when the list is rendered inside a DataTable compound and uses cursor-based navigation (cursor + hasMore + onChange). DataTable.Pagination handles First/Next without page arithmetic; standalone Pagination requires a known total.",
|
|
2806
|
+
"InfiniteQueryState \u2014 use for infinite-scroll / load-more lists driven by useInfiniteQuery. It auto-manages skeleton, empty, and error states; Pagination is inappropriate here because there is no discrete page number.",
|
|
2807
|
+
"DataTable \u2014 when offset pagination is needed inside DataTable, prefer composing DataTable with a standalone Pagination below the card rather than DataTable.Pagination if the API is offset-based and returns a total count. DataTable itself does not paginate; you supply `data` for the current page.",
|
|
2808
|
+
"SearchInput \u2014 often placed in the same toolbar as Pagination. Resetting `current` to 1 inside the search `onChange` handler is mandatory; forgetting this is the most common bug when combining search and Pagination."
|
|
2809
|
+
],
|
|
1994
2810
|
example: `import { Pagination } from "@godxjp/ui/navigation";
|
|
1995
2811
|
|
|
1996
2812
|
<Pagination current={page} total={filtered.length} pageSize={10} showTotal onChange={(p) => setPage(p)} />`,
|
|
@@ -2009,6 +2825,28 @@ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectI
|
|
|
2009
2825
|
description: "Open-state change handler."
|
|
2010
2826
|
}
|
|
2011
2827
|
],
|
|
2828
|
+
usage: [
|
|
2829
|
+
"DO compose the full sub-part tree: DropdownMenu (root) \u2192 DropdownMenuTrigger (with asChild to delegate to your Button/icon) \u2192 DropdownMenuContent \u2192 DropdownMenuItem / DropdownMenuSeparator / DropdownMenuLabel / DropdownMenuGroup. Omitting any level (e.g. rendering DropdownMenuContent without DropdownMenu as ancestor) breaks Radix context and the menu will not open.",
|
|
2830
|
+
"DO use DropdownMenuTrigger with asChild and pass a godx-ui Button or icon Button as the child \u2014 never render a raw <button> or <div> as the trigger, and never omit asChild when the child is already a button-like element (double-button nesting breaks a11y).",
|
|
2831
|
+
"DO use variant='destructive' on DropdownMenuItem for irreversible actions (delete, revoke, void) \u2014 this applies the semantic destructive colour token automatically without any className override.",
|
|
2832
|
+
"DO use DropdownMenuSub + DropdownMenuSubTrigger + DropdownMenuSubContent for nested sub-menus (e.g. 'Export' \u2192 'CSV', 'PDF'). The ChevronRight icon is rendered automatically by DropdownMenuSubTrigger \u2014 do not add your own.",
|
|
2833
|
+
"DO use DropdownMenuCheckboxItem (with checked + onCheckedChange) or DropdownMenuRadioGroup + DropdownMenuRadioItem for toggle/selection menus such as column visibility or active view. These items manage their own checked indicator \u2014 do not layer a Checkbox or RadioGroup inside a plain DropdownMenuItem.",
|
|
2834
|
+
"DON'T use DropdownMenu for form submission \u2014 items fire onSelect callbacks, not form field values. There is no name prop for native form submission. If a menu selection must feed a form field, lift state into a controlled value and wire a hidden Input or use Select instead."
|
|
2835
|
+
],
|
|
2836
|
+
useCases: [
|
|
2837
|
+
"Row action menu in a DataTable: a '...' icon Button opens a DropdownMenu with Edit, Duplicate, DropdownMenuSeparator, then Delete (variant='destructive') \u2014 keeps the row compact and avoids inline button clutter.",
|
|
2838
|
+
"Topbar / avatar chip: a user-avatar Button triggers a DropdownMenu with Profile, Settings, DropdownMenuSeparator, Sign out \u2014 standard app-shell pattern for account actions.",
|
|
2839
|
+
"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.",
|
|
2840
|
+
"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.",
|
|
2841
|
+
"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.",
|
|
2842
|
+
"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."
|
|
2843
|
+
],
|
|
2844
|
+
related: [
|
|
2845
|
+
"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.",
|
|
2846
|
+
"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).",
|
|
2847
|
+
"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.",
|
|
2848
|
+
"Sidebar \u2014 use Sidebar for persistent left-rail navigation. DropdownMenu is transient (opens on click, dismisses on select); Sidebar is always-visible structural navigation."
|
|
2849
|
+
],
|
|
2012
2850
|
example: `import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@godxjp/ui/navigation";
|
|
2013
2851
|
import { Button } from "@godxjp/ui/general";
|
|
2014
2852
|
|
|
@@ -2046,6 +2884,27 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2046
2884
|
description: "Layout direction."
|
|
2047
2885
|
}
|
|
2048
2886
|
],
|
|
2887
|
+
usage: [
|
|
2888
|
+
"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.",
|
|
2889
|
+
"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`.",
|
|
2890
|
+
"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`.",
|
|
2891
|
+
"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.",
|
|
2892
|
+
"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.",
|
|
2893
|
+
"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."
|
|
2894
|
+
],
|
|
2895
|
+
useCases: [
|
|
2896
|
+
"Multi-step form wizard (entity onboarding, invoice creation): render Steps above a form, drive `current` from local state, advance on validated submit \u2014 use `status='error'` on the current step when server validation fails.",
|
|
2897
|
+
"Async background job tracker: display steps for a long-running import/export pipeline; poll job status and map job phases to `StepStatusProp` values (`'process'` with spinner for in-flight, `'finish'` for done, `'error'` for failed).",
|
|
2898
|
+
"Document approval workflow (accounting, contracts): map approval stages (Draft \u2192 Review \u2192 Approved \u2192 Archived) to `items` with per-item `status` reflecting the real state from the server \u2014 use `orientation='vertical'` for a sidebar timeline feel.",
|
|
2899
|
+
"Onboarding checklist sidebar: `orientation='vertical'` + `type='dot'` + `size='small'` for a compact sidebar progress guide alongside a multi-section settings page.",
|
|
2900
|
+
"Non-linear step navigation (e.g. revisit a previous step to correct data): provide `onChange` and leave only future steps `disabled`; past and current steps become clickable buttons."
|
|
2901
|
+
],
|
|
2902
|
+
related: [
|
|
2903
|
+
"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.",
|
|
2904
|
+
"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.",
|
|
2905
|
+
"Progress \u2014 use Progress for a single continuous percentage (file upload, quota fill); use Steps for discrete named stages with individual pass/fail status.",
|
|
2906
|
+
"Breadcrumb \u2014 use Breadcrumb for hierarchical location within a page tree; use Steps for sequential workflow progress where order and completion matter."
|
|
2907
|
+
],
|
|
2049
2908
|
example: `import { Steps } from "@godxjp/ui/navigation";
|
|
2050
2909
|
|
|
2051
2910
|
<Steps current={1} items={[{ title: "\u7533\u8ACB" }, { title: "\u5BE9\u67FB\u4E2D" }, { title: "\u5B8C\u4E86" }]} />`,
|
|
@@ -2083,6 +2942,28 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2083
2942
|
description: "Initial clock format."
|
|
2084
2943
|
}
|
|
2085
2944
|
],
|
|
2945
|
+
usage: [
|
|
2946
|
+
"DO mount AppProvider ONCE at the application root (e.g. in app.tsx or the Inertia layout), wrapping ALL children \u2014 every godx-ui picker (LocalePicker, TimezonePicker, DateFormatPicker, TimeFormatPicker), every formatDate call, and the Toaster all rely on the single context it provides. Nesting two AppProviders creates split contexts; inner pickers silently read the wrong one.",
|
|
2947
|
+
"DO NOT omit AppProvider and then try to use LocalePicker, TimezonePicker, or formatDate standalone \u2014 useAppContext() throws 'useAppContext must be used within <AppProvider>' at runtime. The only exception is using those pickers in fully controlled mode (value + onChange) which reads useOptionalAppContext() and returns null safely.",
|
|
2948
|
+
"DO use the `persist={false}` prop on AppProvider when writing isolated tests or standalone settings forms where localStorage should not be read or written. With the default `persist={true}` the provider reads from localStorage key `godxjp.app` on mount (after first render), so initial state may differ between SSR and client.",
|
|
2949
|
+
"DO set `defaultTimezone='system'` together with `systemTimezone={serverTimezone}` when your backend knows the legal entity's canonical timezone (e.g. 'Asia/Ho_Chi_Minh'). Use `defaultTimezone='browser'` (the default) only when you want the user's browser clock. Do NOT pass a raw IANA string to `defaultTimezone` if the user may be in a different zone \u2014 use the named aliases.",
|
|
2950
|
+
"DO wire `onLocaleChange`, `onTimezoneChange`, `onTimeFormatChange`, `onDateFormatChange` to persist changes server-side (e.g. patch user profile via Inertia router) in addition to the automatic localStorage write. These callbacks fire after state is set, so the new value is already reflected in context.",
|
|
2951
|
+
"DO restrict the timezone dropdown by passing `timezoneOptions={APP_TIMEZONE_PRESET}` (an exported constant) to AppProvider \u2014 all TimezonePicker instances that omit their own `options` prop will inherit this restricted list automatically from context. Without it, TimezonePicker renders the full IANA list (~600 entries)."
|
|
2952
|
+
],
|
|
2953
|
+
useCases: [
|
|
2954
|
+
"App bootstrap in a multi-locale SaaS admin (ja/en/vi) \u2014 mount AppProvider at the root with the tenant's preferred locale and IANA timezone so every DataTable date column, every formatDate call, and every picker renders consistently in the user's locale without any per-component configuration.",
|
|
2955
|
+
"User settings page \u2014 render LocalePicker, TimezonePicker, DateFormatPicker, and TimeFormatPicker as zero-prop children inside the existing AppProvider; each picker reads and writes context automatically. Wire `onLocaleChange` to an Inertia form submit to persist the change to the server profile.",
|
|
2956
|
+
"Server-rendered Inertia app with SSR hydration \u2014 pass `defaultTimezone='system'` and `systemTimezone={sharedProps.timezone}` (injected via HandleInertiaRequests) so the initial render is timezone-deterministic and avoids hydration mismatches caused by browser-timezone detection.",
|
|
2957
|
+
"Multi-entity accounting dashboard \u2014 use `timezoneOptions` to restrict the picker to the legal entity's permissible zones (e.g. Southeast Asian IANA ids only), preventing users from accidentally switching to an out-of-scope timezone that would misrepresent transaction timestamps.",
|
|
2958
|
+
"Isolated preview / Storybook story \u2014 wrap a single component in `<AppProvider persist={false} defaultLocale='en'>` to give it a stable context without polluting localStorage between stories.",
|
|
2959
|
+
"Test harness \u2014 wrap the component under test in `<AppProvider persist={false} defaultLocale='ja' defaultDateFormat='iso'>` to assert locale-sensitive formatting output deterministically, independent of whatever the browser or stored preferences report."
|
|
2960
|
+
],
|
|
2961
|
+
related: [
|
|
2962
|
+
"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.",
|
|
2963
|
+
"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.",
|
|
2964
|
+
"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.",
|
|
2965
|
+
"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."
|
|
2966
|
+
],
|
|
2086
2967
|
example: `import { AppProvider } from "@godxjp/ui/app";
|
|
2087
2968
|
|
|
2088
2969
|
<AppProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo" defaultDateFormat="iso" defaultTimeFormat="24h">
|
|
@@ -2109,6 +2990,27 @@ import { Button } from "@godxjp/ui/general";
|
|
|
2109
2990
|
description: "Output preset; auto infers from the value."
|
|
2110
2991
|
}
|
|
2111
2992
|
],
|
|
2993
|
+
usage: [
|
|
2994
|
+
"DO import from `@godxjp/ui/datetime` \u2014 NOT from `date-fns` or any other datetime utility. `formatDate` is the single mandatory display entry point; calling `date-fns/format` directly bypasses AppProvider locale/timezone/dateFormat/timeFormat context and produces inconsistent output across the app.",
|
|
2995
|
+
"DO ensure `AppProvider` is mounted at the app root before the first `formatDate` call. The function reads a module-level context synced by `AppProvider` via `syncDatetimeContext`. Without it the fallback locale is `'vi'` / timezone `'Asia/Ho_Chi_Minh'` / `'24h'`, which will silently produce wrong output in Japanese or English apps.",
|
|
2996
|
+
"DO pass `null` or `undefined` safely \u2014 `formatDate` returns an em-dash `'\u2014'` for null/undefined/empty string values. Never guard with a ternary before calling it.",
|
|
2997
|
+
"DO use `options.kind` when auto-detection is wrong: pass `kind: 'date'` for an ISO datetime string you want displayed as date-only, `kind: 'relative'` for age display (e.g. `'3\u65E5\u524D'`), `kind: 'long'` for full PPP format in modals/detail panels. Auto-detection maps plain `yyyy-MM-dd` \u2192 `'date'`, `HH:mm` \u2192 `'time'`, everything else \u2192 `'datetime'`.",
|
|
2998
|
+
"DO pass `{ calendar: true }` when the `Date` object came from a react-day-picker calendar pick \u2014 this prevents timezone shift that would occur if the Date were treated as a UTC instant.",
|
|
2999
|
+
"DON'T hand-roll per-call locale/timezone resolution with `Intl.DateTimeFormat` or raw `date-fns/format`. The `options.locale` / `options.timezone` overrides exist for one-off per-cell display differences (e.g. showing a partner's local time), not as a substitute for AppProvider context."
|
|
3000
|
+
],
|
|
3001
|
+
useCases: [
|
|
3002
|
+
"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.",
|
|
3003
|
+
"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.",
|
|
3004
|
+
"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.",
|
|
3005
|
+
"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'`.",
|
|
3006
|
+
"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."
|
|
3007
|
+
],
|
|
3008
|
+
related: [
|
|
3009
|
+
"AppProvider \u2014 required peer that seeds locale, timezone, dateFormat, and timeFormat into the module-level context that `formatDate` reads. Must be mounted once at app root; omitting it means `formatDate` silently falls back to Vietnamese/Ho Chi Minh City defaults.",
|
|
3010
|
+
"DatePicker \u2014 the corresponding input control for calendar dates. Use `DatePicker` to capture a date from the user; use `formatDate(value, { calendar: true })` to display the picked `Date` object back as a string.",
|
|
3011
|
+
"DateFormatPicker / TimeFormatPicker / TimezonePicker \u2014 preference pickers that update AppProvider context; their selections are automatically picked up by subsequent `formatDate` calls with no extra wiring needed.",
|
|
3012
|
+
"TimePicker \u2014 the corresponding input control for HH:mm time values. Use `TimePicker` to capture time; use `formatDate(hhmm)` (auto-detects `'time'` kind) to display the stored `HH:mm` string respecting the user's 12h/24h preference."
|
|
3013
|
+
],
|
|
2112
3014
|
example: `import { formatDate } from "@godxjp/ui/datetime";
|
|
2113
3015
|
|
|
2114
3016
|
formatDate(coupon.validFrom); // "2026-05-01"
|
|
@@ -2339,124 +3241,6 @@ export function InvoicePeriodFilter() {
|
|
|
2339
3241
|
storyPath: "data-entry/DateRangePicker.stories.tsx",
|
|
2340
3242
|
rules: [3, 6, 23, 31]
|
|
2341
3243
|
},
|
|
2342
|
-
{
|
|
2343
|
-
name: "SwitchField",
|
|
2344
|
-
group: "data-entry",
|
|
2345
|
-
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.",
|
|
2346
|
-
props: [
|
|
2347
|
-
{
|
|
2348
|
-
name: "label",
|
|
2349
|
-
type: "string",
|
|
2350
|
-
required: true,
|
|
2351
|
-
description: "Visible text label rendered to the right of the switch toggle."
|
|
2352
|
-
},
|
|
2353
|
-
{
|
|
2354
|
-
name: "name",
|
|
2355
|
-
type: "string",
|
|
2356
|
-
required: true,
|
|
2357
|
-
description: "HTML name attribute used by the hidden input \u2014 the value submitted is '1' (on) or '0' (off)."
|
|
2358
|
-
},
|
|
2359
|
-
{
|
|
2360
|
-
name: "id",
|
|
2361
|
-
type: "string",
|
|
2362
|
-
description: "ID wired to the Switch and Label htmlFor. Auto-generated via useId() when omitted."
|
|
2363
|
-
},
|
|
2364
|
-
{
|
|
2365
|
-
name: "checked",
|
|
2366
|
-
type: "boolean",
|
|
2367
|
-
description: "Controlled checked state. When provided the component is fully controlled; onCheckedChange must also be supplied."
|
|
2368
|
-
},
|
|
2369
|
-
{
|
|
2370
|
-
name: "defaultChecked",
|
|
2371
|
-
type: "boolean",
|
|
2372
|
-
defaultValue: "false",
|
|
2373
|
-
description: "Uncontrolled initial checked state. Ignored when checked is provided."
|
|
2374
|
-
},
|
|
2375
|
-
{
|
|
2376
|
-
name: "onCheckedChange",
|
|
2377
|
-
type: "(checked: boolean) => void",
|
|
2378
|
-
description: "Fires with the new boolean value whenever the switch is toggled."
|
|
2379
|
-
},
|
|
2380
|
-
{
|
|
2381
|
-
name: "required",
|
|
2382
|
-
type: "boolean",
|
|
2383
|
-
description: "Marks the field as required \u2014 renders a red asterisk next to the label and sets aria-required on the switch."
|
|
2384
|
-
},
|
|
2385
|
-
{
|
|
2386
|
-
name: "helper",
|
|
2387
|
-
type: "string",
|
|
2388
|
-
description: "Secondary hint text shown below the label. Hidden when error is set."
|
|
2389
|
-
},
|
|
2390
|
-
{
|
|
2391
|
-
name: "error",
|
|
2392
|
-
type: "string",
|
|
2393
|
-
description: "Validation error message. Renders below the row as a role=alert paragraph and sets aria-invalid on the switch."
|
|
2394
|
-
},
|
|
2395
|
-
{
|
|
2396
|
-
name: "labelAddon",
|
|
2397
|
-
type: "React.ReactNode",
|
|
2398
|
-
description: "Optional node rendered inline after the label text (e.g. a Badge or tooltip trigger)."
|
|
2399
|
-
},
|
|
2400
|
-
{ name: "disabled", type: "boolean", description: "Disables the switch toggle." },
|
|
2401
|
-
{
|
|
2402
|
-
name: "size",
|
|
2403
|
-
type: '"sm" | "default"',
|
|
2404
|
-
description: "Switch toggle size. Forwarded to the inner Switch primitive."
|
|
2405
|
-
},
|
|
2406
|
-
{
|
|
2407
|
-
name: "className",
|
|
2408
|
-
type: "string",
|
|
2409
|
-
description: "Extra class names applied to the outer wrapper div."
|
|
2410
|
-
}
|
|
2411
|
-
],
|
|
2412
|
-
usage: [
|
|
2413
|
-
"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.",
|
|
2414
|
-
"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.",
|
|
2415
|
-
"Controlled mode: supply both checked and onCheckedChange. Uncontrolled mode: use defaultChecked only. Never mix \u2014 passing checked without onCheckedChange leaves the switch frozen.",
|
|
2416
|
-
"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.",
|
|
2417
|
-
"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.",
|
|
2418
|
-
"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."
|
|
2419
|
-
],
|
|
2420
|
-
useCases: [
|
|
2421
|
-
"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.",
|
|
2422
|
-
"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.",
|
|
2423
|
-
"Inline required toggles in a multi-step wizard step where validation errors need to surface below the row via the error prop.",
|
|
2424
|
-
"Accounting app: 'Mark as reconciled', 'Exclude from report', 'Apply tax-exempt status' \u2014 boolean flags that must be submitted with the record form.",
|
|
2425
|
-
"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.",
|
|
2426
|
-
"Any boolean setting that needs a visible label + accessible association (htmlFor / aria) without writing the Switch + Label wiring manually."
|
|
2427
|
-
],
|
|
2428
|
-
related: [
|
|
2429
|
-
"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.",
|
|
2430
|
-
"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.",
|
|
2431
|
-
"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."
|
|
2432
|
-
],
|
|
2433
|
-
example: `import { SwitchField } from "@godxjp/ui/data-entry";
|
|
2434
|
-
import { useState } from "react";
|
|
2435
|
-
|
|
2436
|
-
// Uncontrolled \u2014 defaultChecked, value submitted as hidden 0/1
|
|
2437
|
-
<SwitchField
|
|
2438
|
-
name="auto_invoice"
|
|
2439
|
-
label="\u81EA\u52D5\u8ACB\u6C42\u3092\u6709\u52B9\u306B\u3059\u308B"
|
|
2440
|
-
helper="\u6709\u52B9\u306B\u3059\u308B\u3068\u6708\u672B\u306B\u81EA\u52D5\u3067\u8ACB\u6C42\u66F8\u304C\u767A\u884C\u3055\u308C\u307E\u3059"
|
|
2441
|
-
defaultChecked={false}
|
|
2442
|
-
/>
|
|
2443
|
-
|
|
2444
|
-
// Controlled with validation error
|
|
2445
|
-
function ReconcileToggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) {
|
|
2446
|
-
return (
|
|
2447
|
-
<SwitchField
|
|
2448
|
-
name="reconciled"
|
|
2449
|
-
label="\u7167\u5408\u6E08\u307F"
|
|
2450
|
-
checked={value}
|
|
2451
|
-
onCheckedChange={onChange}
|
|
2452
|
-
required
|
|
2453
|
-
error={!value ? "\u7167\u5408\u3092\u5B8C\u4E86\u3057\u3066\u304F\u3060\u3055\u3044" : undefined}
|
|
2454
|
-
/>
|
|
2455
|
-
);
|
|
2456
|
-
}`,
|
|
2457
|
-
storyPath: "data-entry/SwitchField.stories.tsx",
|
|
2458
|
-
rules: [3, 6, 13, 23]
|
|
2459
|
-
},
|
|
2460
3244
|
{
|
|
2461
3245
|
name: "Cascader",
|
|
2462
3246
|
group: "data-entry",
|
|
@@ -2567,11 +3351,11 @@ const REGIONS = [
|
|
|
2567
3351
|
{
|
|
2568
3352
|
value: "jp",
|
|
2569
3353
|
label: "\u65E5\u672C",
|
|
2570
|
-
|
|
3354
|
+
content: [
|
|
2571
3355
|
{
|
|
2572
3356
|
value: "tokyo",
|
|
2573
3357
|
label: "\u6771\u4EAC\u90FD",
|
|
2574
|
-
|
|
3358
|
+
content: [
|
|
2575
3359
|
{ value: "shinjuku", label: "\u65B0\u5BBF\u533A" },
|
|
2576
3360
|
{ value: "shibuya", label: "\u6E0B\u8C37\u533A" },
|
|
2577
3361
|
],
|
|
@@ -2581,11 +3365,11 @@ const REGIONS = [
|
|
|
2581
3365
|
{
|
|
2582
3366
|
value: "vn",
|
|
2583
3367
|
label: "Vi\u1EC7t Nam",
|
|
2584
|
-
|
|
3368
|
+
content: [
|
|
2585
3369
|
{
|
|
2586
3370
|
value: "hcm",
|
|
2587
3371
|
label: "TP. H\u1ED3 Ch\xED Minh",
|
|
2588
|
-
|
|
3372
|
+
content: [
|
|
2589
3373
|
{ value: "q1", label: "Qu\u1EADn 1" },
|
|
2590
3374
|
{ value: "q3", label: "Qu\u1EADn 3" },
|
|
2591
3375
|
],
|
|
@@ -2627,7 +3411,7 @@ function MultiRegionPicker() {
|
|
|
2627
3411
|
// With custom field names (data uses 'name'/'id'/'nodes')
|
|
2628
3412
|
<Cascader
|
|
2629
3413
|
options={rawApiData}
|
|
2630
|
-
fieldNames={{ label: "name", value: "id",
|
|
3414
|
+
fieldNames={{ label: "name", value: "id", content: "nodes" }}
|
|
2631
3415
|
defaultValue={["dept-1", "team-3"]}
|
|
2632
3416
|
/>
|
|
2633
3417
|
|
|
@@ -2733,7 +3517,7 @@ function MultiRegionPicker() {
|
|
|
2733
3517
|
{
|
|
2734
3518
|
name: "fieldNames",
|
|
2735
3519
|
type: "{ label?: string; value?: string; children?: string }",
|
|
2736
|
-
description: "Remap data object keys. Example: `{ label: 'name', value: 'id',
|
|
3520
|
+
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`."
|
|
2737
3521
|
}
|
|
2738
3522
|
],
|
|
2739
3523
|
usage: [
|
|
@@ -2765,13 +3549,13 @@ const accountTree = [
|
|
|
2765
3549
|
{
|
|
2766
3550
|
value: "assets",
|
|
2767
3551
|
label: "Assets",
|
|
2768
|
-
|
|
2769
|
-
{ value: "current-assets", label: "Current Assets",
|
|
3552
|
+
content: [
|
|
3553
|
+
{ value: "current-assets", label: "Current Assets", content: [
|
|
2770
3554
|
{ value: "cash", label: "Cash" },
|
|
2771
3555
|
{ value: "ar", label: "Accounts Receivable" },
|
|
2772
3556
|
],
|
|
2773
3557
|
},
|
|
2774
|
-
{ value: "fixed-assets", label: "Fixed Assets",
|
|
3558
|
+
{ value: "fixed-assets", label: "Fixed Assets", content: [
|
|
2775
3559
|
{ value: "equipment", label: "Equipment" },
|
|
2776
3560
|
],
|
|
2777
3561
|
},
|
|
@@ -2780,7 +3564,7 @@ const accountTree = [
|
|
|
2780
3564
|
{
|
|
2781
3565
|
value: "liabilities",
|
|
2782
3566
|
label: "Liabilities",
|
|
2783
|
-
|
|
3567
|
+
content: [
|
|
2784
3568
|
{ value: "ap", label: "Accounts Payable" },
|
|
2785
3569
|
],
|
|
2786
3570
|
},
|
|
@@ -2914,11 +3698,11 @@ export function DepartmentFilter() {
|
|
|
2914
3698
|
import { Transfer } from "@godxjp/ui/data-entry";
|
|
2915
3699
|
|
|
2916
3700
|
const ALL_ACCOUNTS = [
|
|
2917
|
-
{
|
|
2918
|
-
{
|
|
2919
|
-
{
|
|
2920
|
-
{
|
|
2921
|
-
{
|
|
3701
|
+
{ value: "1010", title: "Cash", description: "Asset" },
|
|
3702
|
+
{ value: "1020", title: "Accounts Receivable", description: "Asset" },
|
|
3703
|
+
{ value: "2010", title: "Accounts Payable", description: "Liability" },
|
|
3704
|
+
{ value: "3010", title: "Revenue", description: "Income" },
|
|
3705
|
+
{ value: "4010", title: "Cost of Goods Sold", description: "Expense", disabled: true },
|
|
2922
3706
|
];
|
|
2923
3707
|
|
|
2924
3708
|
export function AccountMapping() {
|
|
@@ -3374,7 +4158,7 @@ export function DisabledColor() {
|
|
|
3374
4158
|
"Read-only visual indicator \u2014 pass `disabled` with a controlled `value` to show a progress-style bar that cannot be interacted with."
|
|
3375
4159
|
],
|
|
3376
4160
|
related: [
|
|
3377
|
-
"
|
|
4161
|
+
"Progress \u2014 use Progress (not a disabled Slider) to show read-only progress; Slider with disabled is semantically a control, not a status indicator.",
|
|
3378
4162
|
"Input (type number) \u2014 use Input for free-form numeric entry; use Slider when the range is bounded and dragging is the expected UX.",
|
|
3379
4163
|
"Switch \u2014 for boolean on/off; Slider is for continuous or stepped numeric ranges.",
|
|
3380
4164
|
"RangeField (if present) \u2014 check the MCP first; if a composed range-input field exists, prefer it over wiring two Slider thumbs manually."
|
|
@@ -3770,7 +4554,7 @@ export function ReportRangeFilter() {
|
|
|
3770
4554
|
"Invoice creation forms in an accounting app that require a supplier or customer country, with `allowEmpty={false}` to guarantee a code is always present.",
|
|
3771
4555
|
"Optional 'country of origin' filter fields \u2014 use `allowEmpty={true}` so users can clear the selection back to 'no filter'.",
|
|
3772
4556
|
"Multi-step onboarding flows that must pre-select a country inferred from the user's locale, then let them correct it.",
|
|
3773
|
-
"Read-only display of a country name + flag in a
|
|
4557
|
+
"Read-only display of a country name + flag in a Descriptions or DataTable cell \u2014 use `CountryOptionLabel` directly (not CountrySelect) for non-interactive display."
|
|
3774
4558
|
],
|
|
3775
4559
|
related: [
|
|
3776
4560
|
"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.",
|
|
@@ -4158,7 +4942,7 @@ function AccountQuickPick({ onSelect }: { onSelect: (id: string) => void }) {
|
|
|
4158
4942
|
"RadioGroup \u2014 use when only ONE selection is allowed at a time (mutually exclusive). CheckboxGroup = multiple, RadioGroup = single.",
|
|
4159
4943
|
"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.",
|
|
4160
4944
|
"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.",
|
|
4161
|
-
"Switch /
|
|
4945
|
+
"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.",
|
|
4162
4946
|
"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."
|
|
4163
4947
|
],
|
|
4164
4948
|
example: `import { CheckboxGroup } from "@godxjp/ui/data-entry";
|
|
@@ -4269,7 +5053,7 @@ export function ControlledExample() {
|
|
|
4269
5053
|
],
|
|
4270
5054
|
related: [
|
|
4271
5055
|
"Checkbox.Group \u2014 use when users may select multiple options simultaneously; Radio.Group enforces single-selection only.",
|
|
4272
|
-
"Switch /
|
|
5056
|
+
"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.",
|
|
4273
5057
|
"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."
|
|
4274
5058
|
],
|
|
4275
5059
|
example: `{\`import { Radio } from "@godxjp/ui/data-entry";
|
|
@@ -4921,7 +5705,7 @@ export function FilterSection() {
|
|
|
4921
5705
|
],
|
|
4922
5706
|
related: [
|
|
4923
5707
|
"Timeline \u2014 use Timeline for chronological event sequences with timestamps; use TreeList for hierarchical parent-child structures.",
|
|
4924
|
-
"
|
|
5708
|
+
"Descriptions \u2014 use Descriptions for label/value pairs; use TreeList when items have a parent-child depth relationship.",
|
|
4925
5709
|
"DataTable \u2014 use DataTable for tabular data with columns, sorting, and selection; use TreeList for a single-column hierarchical list without those features.",
|
|
4926
5710
|
"EmptyState \u2014 pair with EmptyState when the items array may be empty; TreeList renders nothing (no empty row) when given an empty array."
|
|
4927
5711
|
],
|
|
@@ -4941,115 +5725,6 @@ export function ChartOfAccounts() {
|
|
|
4941
5725
|
storyPath: "data-display/TreeList.stories.tsx",
|
|
4942
5726
|
rules: [3, 6, 23, 31]
|
|
4943
5727
|
},
|
|
4944
|
-
{
|
|
4945
|
-
name: "ScanPanel",
|
|
4946
|
-
group: "data-display",
|
|
4947
|
-
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.",
|
|
4948
|
-
props: [
|
|
4949
|
-
{
|
|
4950
|
-
name: "title",
|
|
4951
|
-
type: "string",
|
|
4952
|
-
required: true,
|
|
4953
|
-
description: "Primary label rendered below the scan-line icon in semibold large text."
|
|
4954
|
-
},
|
|
4955
|
-
{
|
|
4956
|
-
name: "description",
|
|
4957
|
-
type: "string",
|
|
4958
|
-
description: "Optional secondary line rendered below the title in muted small text. Omit if no additional context is needed."
|
|
4959
|
-
}
|
|
4960
|
-
],
|
|
4961
|
-
usage: [
|
|
4962
|
-
"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.",
|
|
4963
|
-
"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.",
|
|
4964
|
-
"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.",
|
|
4965
|
-
"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.",
|
|
4966
|
-
"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.",
|
|
4967
|
-
"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."
|
|
4968
|
-
],
|
|
4969
|
-
useCases: [
|
|
4970
|
-
"An invoice-scanning workflow step where the user must point a camera or upload an image \u2014 show ScanPanel while no file is selected.",
|
|
4971
|
-
"A QR-code / barcode reader pane rendered before the camera stream is active, indicating the scan target area.",
|
|
4972
|
-
"A document upload dropzone placeholder in an accounting app (expense receipts, purchase orders) before any file is chosen.",
|
|
4973
|
-
"An OCR processing panel shown while the system awaits a document scan input from a connected scanner device.",
|
|
4974
|
-
"A 'no attachment yet' state in an accounting record detail view that invites the user to attach a scanned document."
|
|
4975
|
-
],
|
|
4976
|
-
related: [
|
|
4977
|
-
"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.",
|
|
4978
|
-
"DataState / InfiniteQueryState \u2014 use for TanStack Query lifecycle (loading/empty/error) in list views; not for scan prompts.",
|
|
4979
|
-
"SkeletonTable \u2014 use while data is loading into a table; not for scan/upload placeholder states."
|
|
4980
|
-
],
|
|
4981
|
-
example: `import { ScanPanel } from "@godxjp/ui/data-display";
|
|
4982
|
-
|
|
4983
|
-
export function InvoiceScanStep() {
|
|
4984
|
-
return (
|
|
4985
|
-
<div className="max-w-xs mx-auto">
|
|
4986
|
-
<ScanPanel
|
|
4987
|
-
title="Scan invoice"
|
|
4988
|
-
description="Point your camera at the invoice barcode or upload a file."
|
|
4989
|
-
/>
|
|
4990
|
-
</div>
|
|
4991
|
-
);
|
|
4992
|
-
}`,
|
|
4993
|
-
storyPath: "data-display/ScanPanel.stories.tsx",
|
|
4994
|
-
rules: [31]
|
|
4995
|
-
},
|
|
4996
|
-
{
|
|
4997
|
-
name: "CodeBadge",
|
|
4998
|
-
group: "data-display",
|
|
4999
|
-
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.",
|
|
5000
|
-
props: [
|
|
5001
|
-
{
|
|
5002
|
-
name: "kind",
|
|
5003
|
-
type: '"internal" | "seller" | "yamato"',
|
|
5004
|
-
required: true,
|
|
5005
|
-
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.'
|
|
5006
|
-
},
|
|
5007
|
-
{
|
|
5008
|
-
name: "value",
|
|
5009
|
-
type: "string",
|
|
5010
|
-
required: true,
|
|
5011
|
-
description: "The actual reference code string to display after the icon (e.g. an order number, shipment tracking ID, or internal SKU)."
|
|
5012
|
-
}
|
|
5013
|
-
],
|
|
5014
|
-
usage: [
|
|
5015
|
-
"DO: always supply both `kind` and `value` \u2014 both are required; omitting either leaves the chip meaningless or broken.",
|
|
5016
|
-
'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.',
|
|
5017
|
-
"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.",
|
|
5018
|
-
"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>`.",
|
|
5019
|
-
"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`).",
|
|
5020
|
-
'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).'
|
|
5021
|
-
],
|
|
5022
|
-
useCases: [
|
|
5023
|
-
"Displaying an order's internal system reference code alongside seller and carrier codes in an order detail panel or DataTable column.",
|
|
5024
|
-
"Rendering a Yamato shipment tracking number in a logistics/fulfillment table so operators can visually distinguish it from internal IDs at a glance.",
|
|
5025
|
-
"Showing a seller's own reference code (e.g. merchant PO number) in an invoice or accounting line-item row.",
|
|
5026
|
-
"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.",
|
|
5027
|
-
"In a DataTable cell renderer, wrapping a code field so the column's kind is immediately obvious without a separate column header."
|
|
5028
|
-
],
|
|
5029
|
-
related: [
|
|
5030
|
-
"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.",
|
|
5031
|
-
'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.'
|
|
5032
|
-
],
|
|
5033
|
-
example: `{\`import { CodeBadge } from "@godxjp/ui/data-display";
|
|
5034
|
-
|
|
5035
|
-
// Internal system reference code
|
|
5036
|
-
<CodeBadge kind="internal" value="ORD-00123" />
|
|
5037
|
-
|
|
5038
|
-
// Seller reference code
|
|
5039
|
-
<CodeBadge kind="seller" value="SLR-98765" />
|
|
5040
|
-
|
|
5041
|
-
// Yamato carrier tracking code
|
|
5042
|
-
<CodeBadge kind="yamato" value="YMT-4444-5555-6666" />
|
|
5043
|
-
|
|
5044
|
-
// Multiple codes for one order
|
|
5045
|
-
<div className="flex flex-wrap gap-2">
|
|
5046
|
-
<CodeBadge kind="internal" value="ORD-00123" />
|
|
5047
|
-
<CodeBadge kind="seller" value="SLR-98765" />
|
|
5048
|
-
<CodeBadge kind="yamato" value="YMT-4444-5555-6666" />
|
|
5049
|
-
</div>\`}`,
|
|
5050
|
-
storyPath: "data-display/CodeBadge.stories.tsx",
|
|
5051
|
-
rules: [3, 6, 13, 35]
|
|
5052
|
-
},
|
|
5053
5728
|
{
|
|
5054
5729
|
name: "PageHeader",
|
|
5055
5730
|
group: "navigation",
|
|
@@ -5280,7 +5955,7 @@ export function TimezoneField() {
|
|
|
5280
5955
|
import { AppProvider } from "@godxjp/ui/app";
|
|
5281
5956
|
import { APP_TIMEZONE_PRESET } from "@godxjp/ui/navigation";
|
|
5282
5957
|
|
|
5283
|
-
export function Shell({ children }: {
|
|
5958
|
+
export function Shell({ children }: { content: React.ReactNode }) {
|
|
5284
5959
|
return (
|
|
5285
5960
|
<AppProvider
|
|
5286
5961
|
defaultLocale="ja"
|
|
@@ -5469,366 +6144,6 @@ export function SettingsForm() {
|
|
|
5469
6144
|
storyPath: "navigation/TimeFormatPicker.stories.tsx",
|
|
5470
6145
|
rules: [3, 5, 6, 13]
|
|
5471
6146
|
},
|
|
5472
|
-
{
|
|
5473
|
-
name: "TabsItems",
|
|
5474
|
-
group: "navigation",
|
|
5475
|
-
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.",
|
|
5476
|
-
props: [
|
|
5477
|
-
{
|
|
5478
|
-
name: "items",
|
|
5479
|
-
type: "TabItemProp[]",
|
|
5480
|
-
required: true,
|
|
5481
|
-
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)."
|
|
5482
|
-
},
|
|
5483
|
-
{
|
|
5484
|
-
name: "value",
|
|
5485
|
-
type: "string",
|
|
5486
|
-
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."
|
|
5487
|
-
},
|
|
5488
|
-
{
|
|
5489
|
-
name: "defaultValue",
|
|
5490
|
-
type: "string",
|
|
5491
|
-
description: "Uncontrolled initial active tab key. Defaults to `items[0].key` when omitted. Ignored when `value` is provided."
|
|
5492
|
-
},
|
|
5493
|
-
{
|
|
5494
|
-
name: "onValueChange",
|
|
5495
|
-
type: "(key: string) => void",
|
|
5496
|
-
description: "Callback fired when the user switches tabs. Receives the selected item's `key`. Required in controlled mode."
|
|
5497
|
-
},
|
|
5498
|
-
{
|
|
5499
|
-
name: "variant",
|
|
5500
|
-
type: '"default" | "line" | "card"',
|
|
5501
|
-
defaultValue: '"default"',
|
|
5502
|
-
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."
|
|
5503
|
-
},
|
|
5504
|
-
{
|
|
5505
|
-
name: "className",
|
|
5506
|
-
type: "string",
|
|
5507
|
-
description: "Extra Tailwind classes applied to the outer Radix `Tabs` root element."
|
|
5508
|
-
},
|
|
5509
|
-
{
|
|
5510
|
-
name: "listClassName",
|
|
5511
|
-
type: "string",
|
|
5512
|
-
description: "Extra classes applied to the `TabsList` wrapper (the trigger bar). Useful to control width, alignment, or border overrides."
|
|
5513
|
-
},
|
|
5514
|
-
{
|
|
5515
|
-
name: "contentClassName",
|
|
5516
|
-
type: "string",
|
|
5517
|
-
description: "Extra classes applied to every `TabsContent` panel. Use for padding, min-height, or background overrides shared across all panels."
|
|
5518
|
-
}
|
|
5519
|
-
],
|
|
5520
|
-
usage: [
|
|
5521
|
-
"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.",
|
|
5522
|
-
"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.",
|
|
5523
|
-
"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.",
|
|
5524
|
-
"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.",
|
|
5525
|
-
"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.",
|
|
5526
|
-
"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."
|
|
5527
|
-
],
|
|
5528
|
-
useCases: [
|
|
5529
|
-
"Admin detail pages with multiple sections (Overview / Transactions / Attachments / Audit Log) where all content is known up-front and switching is purely client-side.",
|
|
5530
|
-
"Accounting invoice or journal-entry detail drawers that split metadata, line items, and comments into named tabs without needing URL routing.",
|
|
5531
|
-
"Dashboard widgets presenting the same data in different views (e.g. Chart / Table / Raw) using controlled `value` driven by a toolbar toggle.",
|
|
5532
|
-
"Settings pages with a vertical or horizontal tab strip (line variant) separating General / Notifications / Billing / Security sections.",
|
|
5533
|
-
"Entity profile pages (company, partner, employee) where each tab loads deferred or lazy content \u2014 combine with Inertia deferred props per panel.",
|
|
5534
|
-
"Any place where Ant Design-style `<Tabs items={[...]} />` would have been used \u2014 TabsItems is the direct godx-ui equivalent."
|
|
5535
|
-
],
|
|
5536
|
-
related: [
|
|
5537
|
-
"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.",
|
|
5538
|
-
"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.",
|
|
5539
|
-
"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."
|
|
5540
|
-
],
|
|
5541
|
-
example: `{\`import { TabsItems } from "@godxjp/ui/navigation";
|
|
5542
|
-
import { FileText, List, BarChart2 } from "lucide-react";
|
|
5543
|
-
|
|
5544
|
-
// Uncontrolled \u2014 first item active by default
|
|
5545
|
-
export function InvoiceDetailTabs() {
|
|
5546
|
-
return (
|
|
5547
|
-
<TabsItems
|
|
5548
|
-
variant="line"
|
|
5549
|
-
items={[
|
|
5550
|
-
{
|
|
5551
|
-
key: "overview",
|
|
5552
|
-
label: "Overview",
|
|
5553
|
-
icon: <FileText size={14} />,
|
|
5554
|
-
children: <OverviewPanel />,
|
|
5555
|
-
},
|
|
5556
|
-
{
|
|
5557
|
-
key: "line-items",
|
|
5558
|
-
label: "Line Items",
|
|
5559
|
-
icon: <List size={14} />,
|
|
5560
|
-
children: <LineItemsTable />,
|
|
5561
|
-
},
|
|
5562
|
-
{
|
|
5563
|
-
key: "analytics",
|
|
5564
|
-
label: "Analytics",
|
|
5565
|
-
icon: <BarChart2 size={14} />,
|
|
5566
|
-
disabled: false,
|
|
5567
|
-
children: <AnalyticsChart />,
|
|
5568
|
-
},
|
|
5569
|
-
]}
|
|
5570
|
-
/>
|
|
5571
|
-
);
|
|
5572
|
-
}
|
|
5573
|
-
|
|
5574
|
-
// Controlled \u2014 active tab driven by URL search param
|
|
5575
|
-
export function ControlledTabs() {
|
|
5576
|
-
const [tab, setTab] = React.useState("overview");
|
|
5577
|
-
|
|
5578
|
-
return (
|
|
5579
|
-
<TabsItems
|
|
5580
|
-
variant="default"
|
|
5581
|
-
value={tab}
|
|
5582
|
-
onValueChange={setTab}
|
|
5583
|
-
items={[
|
|
5584
|
-
{ key: "overview", label: "Overview", children: <OverviewPanel /> },
|
|
5585
|
-
{ key: "history", label: "History", children: <HistoryPanel /> },
|
|
5586
|
-
]}
|
|
5587
|
-
/>
|
|
5588
|
-
);
|
|
5589
|
-
}\`}`,
|
|
5590
|
-
storyPath: "navigation/TabsItems.stories.tsx",
|
|
5591
|
-
rules: [3, 23, 31, 37]
|
|
5592
|
-
},
|
|
5593
|
-
{
|
|
5594
|
-
name: "Menu",
|
|
5595
|
-
group: "layout",
|
|
5596
|
-
tagline: "A simplified sidebar-backed navigation menu \u2014 pass sections with active flags and Menu resolves the activeId automatically; never set activeId manually.",
|
|
5597
|
-
props: [
|
|
5598
|
-
{
|
|
5599
|
-
name: "items",
|
|
5600
|
-
type: "MenuSection[]",
|
|
5601
|
-
required: true,
|
|
5602
|
-
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."
|
|
5603
|
-
}
|
|
5604
|
-
],
|
|
5605
|
-
usage: [
|
|
5606
|
-
"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).",
|
|
5607
|
-
"DO: supply every item with a unique `id` string \u2014 it is required by the underlying SidebarItemProp. Duplicate ids cause incorrect active/highlight state.",
|
|
5608
|
-
"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.",
|
|
5609
|
-
"DO: nest child pages under an item using `children: SidebarItemProp[]` \u2014 this renders a collapsible submenu that auto-opens when any child is active.",
|
|
5610
|
-
"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.",
|
|
5611
|
-
"MenuItem extends SidebarItem so you may also pass `badge` (ReactNode), `disabled` (boolean), and `children` (nested items) on each item."
|
|
5612
|
-
],
|
|
5613
|
-
useCases: [
|
|
5614
|
-
"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.",
|
|
5615
|
-
"Admin dashboards with a small, fixed set of top-level nav entries (Dashboard, Invoices, Settings) grouped into labelled sections (e.g. 'Accounting', 'Admin').",
|
|
5616
|
-
"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.",
|
|
5617
|
-
"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.",
|
|
5618
|
-
"Rapid prototyping: quickly render a working sidebar nav by passing a flat MenuSection array without needing to understand Sidebar's full API."
|
|
5619
|
-
],
|
|
5620
|
-
related: [
|
|
5621
|
-
"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.",
|
|
5622
|
-
"AppShell \u2014 the page-level shell that accepts a sidebar node. Pass a Menu (or Sidebar) as its sidebar prop.",
|
|
5623
|
-
"Topbar \u2014 the horizontal bar that pairs with AppShell; not a replacement for Menu/Sidebar nav."
|
|
5624
|
-
],
|
|
5625
|
-
example: `
|
|
5626
|
-
import { Menu } from "@godxjp/ui/layout";
|
|
5627
|
-
import { LayoutDashboard, FileText, Settings, Users } from "lucide-react";
|
|
5628
|
-
|
|
5629
|
-
const sections = [
|
|
5630
|
-
{
|
|
5631
|
-
label: "Accounting",
|
|
5632
|
-
items: [
|
|
5633
|
-
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard, active: true },
|
|
5634
|
-
{ id: "invoices", label: "Invoices", icon: FileText },
|
|
5635
|
-
],
|
|
5636
|
-
},
|
|
5637
|
-
{
|
|
5638
|
-
label: "Admin",
|
|
5639
|
-
items: [
|
|
5640
|
-
{
|
|
5641
|
-
id: "users",
|
|
5642
|
-
label: "Users",
|
|
5643
|
-
icon: Users,
|
|
5644
|
-
children: [
|
|
5645
|
-
{ id: "users-list", label: "All Users", icon: Users },
|
|
5646
|
-
{ id: "users-invite", label: "Invite", icon: Users },
|
|
5647
|
-
],
|
|
5648
|
-
},
|
|
5649
|
-
{ id: "settings", label: "Settings", icon: Settings },
|
|
5650
|
-
],
|
|
5651
|
-
},
|
|
5652
|
-
];
|
|
5653
|
-
|
|
5654
|
-
export default function AppSidebar() {
|
|
5655
|
-
return <Menu items={sections} />;
|
|
5656
|
-
}
|
|
5657
|
-
`,
|
|
5658
|
-
storyPath: "layout/Menu.stories.tsx",
|
|
5659
|
-
rules: [23]
|
|
5660
|
-
},
|
|
5661
|
-
{
|
|
5662
|
-
name: "ShellApp",
|
|
5663
|
-
group: "layout",
|
|
5664
|
-
tagline: "Opinionated full-page shell (AppShell + a hardcoded Topbar) \u2014 use AppShell directly if you need to configure the topbar.",
|
|
5665
|
-
props: [
|
|
5666
|
-
{
|
|
5667
|
-
name: "menu",
|
|
5668
|
-
type: "ReactNode",
|
|
5669
|
-
required: true,
|
|
5670
|
-
description: "Sidebar content \u2014 pass a configured <Sidebar> component here. Rendered inside the left rail."
|
|
5671
|
-
},
|
|
5672
|
-
{
|
|
5673
|
-
name: "breadcrumb",
|
|
5674
|
-
type: "ReactNode",
|
|
5675
|
-
description: "Optional breadcrumb strip rendered above the main content area, inside the app-breadcrumb div."
|
|
5676
|
-
},
|
|
5677
|
-
{
|
|
5678
|
-
name: "children",
|
|
5679
|
-
type: "ReactNode",
|
|
5680
|
-
required: true,
|
|
5681
|
-
description: "Main page content \u2014 rendered inside <main class='app-main'>. Typically a <PageContainer> or a list of page-level components."
|
|
5682
|
-
}
|
|
5683
|
-
],
|
|
5684
|
-
usage: [
|
|
5685
|
-
"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.",
|
|
5686
|
-
"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.",
|
|
5687
|
-
"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.",
|
|
5688
|
-
"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.",
|
|
5689
|
-
"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.",
|
|
5690
|
-
"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."
|
|
5691
|
-
],
|
|
5692
|
-
useCases: [
|
|
5693
|
-
"Rapid admin panel scaffolding where the default GodX branding topbar is acceptable and the only variable parts are the sidebar menu and page content.",
|
|
5694
|
-
"Internal tools and dashboards that need a consistent three-zone shell (sidebar / topbar / main) without customising the product chip or notification system.",
|
|
5695
|
-
"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.",
|
|
5696
|
-
"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."
|
|
5697
|
-
],
|
|
5698
|
-
related: [
|
|
5699
|
-
"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.",
|
|
5700
|
-
"Sidebar \u2014 pass as the `menu` prop; handles activeId, collapsing, section labels, nested groups with Collapsible, and collapsed popover flyouts.",
|
|
5701
|
-
"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.",
|
|
5702
|
-
"PageContainer \u2014 the right component to use as the direct child of ShellApp to get a titled, padded page with actions/breadcrumb/footer.",
|
|
5703
|
-
"PageInset \u2014 use inside a flush PageContainer for padded strips (filter bars, intros) that sit above a full-bleed DataTable."
|
|
5704
|
-
],
|
|
5705
|
-
example: `{\`import { ShellApp, Sidebar, PageContainer, Breadcrumb } from "@godxjp/ui/layout";
|
|
5706
|
-
import { LayoutDashboard, FileText, Settings } from "lucide-react";
|
|
5707
|
-
|
|
5708
|
-
const NAV_SECTIONS = [
|
|
5709
|
-
{
|
|
5710
|
-
items: [
|
|
5711
|
-
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
|
5712
|
-
{ id: "invoices", label: "Invoices", icon: FileText },
|
|
5713
|
-
{ id: "settings", label: "Settings", icon: Settings },
|
|
5714
|
-
],
|
|
5715
|
-
},
|
|
5716
|
-
];
|
|
5717
|
-
|
|
5718
|
-
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
5719
|
-
const [activeId, setActiveId] = React.useState("dashboard");
|
|
5720
|
-
|
|
5721
|
-
return (
|
|
5722
|
-
<ShellApp
|
|
5723
|
-
menu={
|
|
5724
|
-
<Sidebar
|
|
5725
|
-
activeId={activeId}
|
|
5726
|
-
onSelect={setActiveId}
|
|
5727
|
-
sections={NAV_SECTIONS}
|
|
5728
|
-
product={{ name: "CoreBooks", color: "hsl(var(--attention))" }}
|
|
5729
|
-
/>
|
|
5730
|
-
}
|
|
5731
|
-
breadcrumb={
|
|
5732
|
-
<Breadcrumb items={[{ label: "Home", href: "/" }, { label: "Dashboard" }]} />
|
|
5733
|
-
}
|
|
5734
|
-
>
|
|
5735
|
-
{children}
|
|
5736
|
-
</ShellApp>
|
|
5737
|
-
);
|
|
5738
|
-
}\`}`,
|
|
5739
|
-
storyPath: "layout/ShellApp.stories.tsx",
|
|
5740
|
-
rules: [23, 24, 31, 35]
|
|
5741
|
-
},
|
|
5742
|
-
{
|
|
5743
|
-
name: "MobileFrame",
|
|
5744
|
-
group: "layout",
|
|
5745
|
-
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.",
|
|
5746
|
-
props: [
|
|
5747
|
-
{
|
|
5748
|
-
name: "title",
|
|
5749
|
-
type: "string",
|
|
5750
|
-
required: true,
|
|
5751
|
-
description: "Primary heading shown in the mobile header bar (e.g. page name or branch name)."
|
|
5752
|
-
},
|
|
5753
|
-
{
|
|
5754
|
-
name: "subtitle",
|
|
5755
|
-
type: "string",
|
|
5756
|
-
description: "Secondary line rendered beneath the title \u2014 typically a context descriptor such as location or mode."
|
|
5757
|
-
},
|
|
5758
|
-
{
|
|
5759
|
-
name: "status",
|
|
5760
|
-
type: "string",
|
|
5761
|
-
description: "Domain status string rendered as a secondary Badge in the header (e.g. 'Online', 'Offline'). Omit to suppress the badge."
|
|
5762
|
-
},
|
|
5763
|
-
{
|
|
5764
|
-
name: "children",
|
|
5765
|
-
type: "ReactNode",
|
|
5766
|
-
required: true,
|
|
5767
|
-
description: "Main body content rendered inside the scrollable <main> region of the frame."
|
|
5768
|
-
},
|
|
5769
|
-
{
|
|
5770
|
-
name: "navItems",
|
|
5771
|
-
type: "MobileFrameNavItem[]",
|
|
5772
|
-
defaultValue: "[]",
|
|
5773
|
-
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."
|
|
5774
|
-
}
|
|
5775
|
-
],
|
|
5776
|
-
usage: [
|
|
5777
|
-
"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.",
|
|
5778
|
-
"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.",
|
|
5779
|
-
"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.",
|
|
5780
|
-
"DO NOT: Nest MobileFrame inside PageContainer or AppShell \u2014 it is an independent shell; mixing shells breaks the layout semantics.",
|
|
5781
|
-
"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.).",
|
|
5782
|
-
"DO NOT: Use MobileFrame for desktop admin pages \u2014 it renders a narrow, phone-sized canvas. Use PageContainer + AppShell for desktop screens."
|
|
5783
|
-
],
|
|
5784
|
-
useCases: [
|
|
5785
|
-
"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.",
|
|
5786
|
-
"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.",
|
|
5787
|
-
"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.",
|
|
5788
|
-
"Prototype or demo of a consumer-facing mobile app embedded inside an admin dashboard as a live preview widget.",
|
|
5789
|
-
"Delivery dispatch or order-tracking app rendered on a handheld device where bottom-tab navigation is the primary navigation affordance."
|
|
5790
|
-
],
|
|
5791
|
-
related: [
|
|
5792
|
-
"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.",
|
|
5793
|
-
"PageContainer \u2014 the page-level content wrapper used inside AppShell for desktop pages; do not substitute for MobileFrame on mobile screens.",
|
|
5794
|
-
"ShellApp \u2014 another desktop shell variant; same rule as AppShell \u2014 mutually exclusive with MobileFrame."
|
|
5795
|
-
],
|
|
5796
|
-
example: `import { Archive, CalendarClock, Package, ScanLine } from "lucide-react";
|
|
5797
|
-
import { MobileFrame, type MobileFrameNavItem } from "@godxjp/ui/layout";
|
|
5798
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
5799
|
-
import { Stack } from "@godxjp/ui/layout";
|
|
5800
|
-
|
|
5801
|
-
const navItems: MobileFrameNavItem[] = [
|
|
5802
|
-
{ label: "Scan", icon: ScanLine, active: true },
|
|
5803
|
-
{ label: "Receive", icon: Archive },
|
|
5804
|
-
{ label: "Pack", icon: Package },
|
|
5805
|
-
{ label: "Schedule", icon: CalendarClock },
|
|
5806
|
-
];
|
|
5807
|
-
|
|
5808
|
-
export default function HandheldScanPage() {
|
|
5809
|
-
return (
|
|
5810
|
-
<MobileFrame
|
|
5811
|
-
title="Tokyo scan"
|
|
5812
|
-
subtitle="Branch handheld"
|
|
5813
|
-
status="Online"
|
|
5814
|
-
navItems={navItems}
|
|
5815
|
-
>
|
|
5816
|
-
<Stack gap="md">
|
|
5817
|
-
<Card>
|
|
5818
|
-
<CardHeader banded>
|
|
5819
|
-
<CardTitle>Last scan</CardTitle>
|
|
5820
|
-
</CardHeader>
|
|
5821
|
-
<CardContent>
|
|
5822
|
-
Ready to scan vendor or internal code.
|
|
5823
|
-
</CardContent>
|
|
5824
|
-
</Card>
|
|
5825
|
-
</Stack>
|
|
5826
|
-
</MobileFrame>
|
|
5827
|
-
);
|
|
5828
|
-
}`,
|
|
5829
|
-
storyPath: "layout/MobileFrame.stories.tsx",
|
|
5830
|
-
rules: [2, 3, 23, 24]
|
|
5831
|
-
},
|
|
5832
6147
|
{
|
|
5833
6148
|
name: "Tooltip",
|
|
5834
6149
|
group: "feedback",
|
|
@@ -6095,7 +6410,7 @@ import { fetchInvoice } from "@/api/invoices";
|
|
|
6095
6410
|
"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.",
|
|
6096
6411
|
"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.",
|
|
6097
6412
|
"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.",
|
|
6098
|
-
"For i18n, pass a translated string as label or
|
|
6413
|
+
"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'."
|
|
6099
6414
|
],
|
|
6100
6415
|
useCases: [
|
|
6101
6416
|
"Toolbar 'Refresh' button on an invoice list page that re-fetches from the server without a full navigation.",
|
|
@@ -6128,6 +6443,118 @@ export function InvoiceListHeader() {
|
|
|
6128
6443
|
}\`}`,
|
|
6129
6444
|
storyPath: "data-display/QueryRefetchButton.stories.tsx",
|
|
6130
6445
|
rules: [3, 5, 6, 13]
|
|
6446
|
+
},
|
|
6447
|
+
{
|
|
6448
|
+
name: "Avatar",
|
|
6449
|
+
group: "data-display",
|
|
6450
|
+
tagline: "Radix Avatar wrapper with image and fallback slots for users, teams, and entities.",
|
|
6451
|
+
props: [
|
|
6452
|
+
{ name: "children", type: "ReactNode", description: "Compose AvatarImage and AvatarFallback." },
|
|
6453
|
+
{ name: "className", type: "string", description: "Extra classes on the avatar root." }
|
|
6454
|
+
],
|
|
6455
|
+
usage: [
|
|
6456
|
+
"DO compose Avatar > AvatarImage + AvatarFallback so broken or missing images still show a readable fallback.",
|
|
6457
|
+
"DON'T use Avatar for decorative thumbnails; use CardCover or an img when the image is content rather than identity."
|
|
6458
|
+
],
|
|
6459
|
+
useCases: ["User profile chips", "Team member lists", "Account owner cells in a DataTable"],
|
|
6460
|
+
related: ["Badge \u2014 use beside Avatar for role/status metadata."],
|
|
6461
|
+
example: `import { Avatar, AvatarFallback, AvatarImage } from "@godxjp/ui/data-display";
|
|
6462
|
+
|
|
6463
|
+
<Avatar>
|
|
6464
|
+
<AvatarImage src="/user.png" alt="User" />
|
|
6465
|
+
<AvatarFallback>UI</AvatarFallback>
|
|
6466
|
+
</Avatar>`,
|
|
6467
|
+
storyPath: "data-display/Avatar.stories.tsx",
|
|
6468
|
+
rules: [3, 35]
|
|
6469
|
+
},
|
|
6470
|
+
{
|
|
6471
|
+
name: "Separator",
|
|
6472
|
+
group: "layout",
|
|
6473
|
+
tagline: "Radix Separator wrapper for tokenized horizontal or vertical dividers.",
|
|
6474
|
+
props: [
|
|
6475
|
+
{ name: "orientation", type: '"horizontal" | "vertical"', defaultValue: '"horizontal"', description: "Divider direction." },
|
|
6476
|
+
{ name: "decorative", type: "boolean", defaultValue: "true", description: "Whether the separator is decorative for assistive tech." }
|
|
6477
|
+
],
|
|
6478
|
+
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."],
|
|
6479
|
+
useCases: ["Separating toolbar groups", "Dividing stacked page sections", "Vertical split between metadata groups"],
|
|
6480
|
+
related: ["Stack \u2014 use for vertical spacing without a visible rule."],
|
|
6481
|
+
example: `import { Separator } from "@godxjp/ui/layout";
|
|
6482
|
+
|
|
6483
|
+
<Separator />`,
|
|
6484
|
+
storyPath: "layout/Separator.stories.tsx",
|
|
6485
|
+
rules: [2, 3]
|
|
6486
|
+
},
|
|
6487
|
+
{
|
|
6488
|
+
name: "Skeleton",
|
|
6489
|
+
group: "feedback",
|
|
6490
|
+
tagline: "Base pulsing skeleton block for custom loading placeholders.",
|
|
6491
|
+
props: [
|
|
6492
|
+
{ name: "className", type: "string", description: "Size and layout classes for the block." }
|
|
6493
|
+
],
|
|
6494
|
+
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."],
|
|
6495
|
+
useCases: ["Single loading line", "Custom card media placeholder", "Inline metadata placeholder"],
|
|
6496
|
+
related: ["SkeletonRows", "SkeletonTable", "SkeletonCard"],
|
|
6497
|
+
example: `import { Skeleton } from "@godxjp/ui/feedback";
|
|
6498
|
+
|
|
6499
|
+
<Skeleton className="h-6 w-48" />`,
|
|
6500
|
+
storyPath: "feedback/Skeleton.stories.tsx",
|
|
6501
|
+
rules: [3, 31]
|
|
6502
|
+
},
|
|
6503
|
+
{
|
|
6504
|
+
name: "Toggle",
|
|
6505
|
+
group: "data-entry",
|
|
6506
|
+
tagline: "Radix Toggle wrapper with default/outline variants and tokenized sizes.",
|
|
6507
|
+
props: [
|
|
6508
|
+
{ name: "pressed", type: "boolean", description: "Controlled pressed state." },
|
|
6509
|
+
{ name: "onPressedChange", type: "(pressed: boolean) => void", description: "Pressed-state callback." },
|
|
6510
|
+
{ name: "variant", type: '"default" | "outline"', defaultValue: '"default"', description: "Visual style." },
|
|
6511
|
+
{ name: "size", type: '"sm" | "default" | "lg"', defaultValue: '"default"', description: "Control size." }
|
|
6512
|
+
],
|
|
6513
|
+
usage: ["DO provide an accessible label when the toggle only contains an icon.", "DON'T use Toggle for multi-option selection; use ToggleGroup."],
|
|
6514
|
+
useCases: ["Bold/italic toolbar buttons", "Pinned filter toggles", "Compact view mode buttons"],
|
|
6515
|
+
related: ["ToggleGroup", "Button"],
|
|
6516
|
+
example: `import { Toggle } from "@godxjp/ui/data-entry";
|
|
6517
|
+
|
|
6518
|
+
<Toggle aria-label="Bold">B</Toggle>`,
|
|
6519
|
+
storyPath: "data-entry/Toggle.stories.tsx",
|
|
6520
|
+
rules: [3, 13]
|
|
6521
|
+
},
|
|
6522
|
+
{
|
|
6523
|
+
name: "ToggleGroup",
|
|
6524
|
+
group: "data-entry",
|
|
6525
|
+
tagline: "Radix ToggleGroup wrapper for single or multiple toggle selection.",
|
|
6526
|
+
props: [
|
|
6527
|
+
{ name: "type", type: '"single" | "multiple"', required: true, description: "Selection mode." },
|
|
6528
|
+
{ name: "value", type: "string | string[]", description: "Controlled selected value(s)." },
|
|
6529
|
+
{ name: "onValueChange", type: "(value: string | string[]) => void", description: "Selection callback." }
|
|
6530
|
+
],
|
|
6531
|
+
usage: ["DO choose type='single' for mutually exclusive toolbar modes.", "DO choose type='multiple' for independent formatting toggles."],
|
|
6532
|
+
useCases: ["Text alignment selector", "Formatting toolbar", "View density switcher"],
|
|
6533
|
+
related: ["Toggle", "RadioGroup"],
|
|
6534
|
+
example: `import { ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
|
|
6535
|
+
|
|
6536
|
+
<ToggleGroup type="single">
|
|
6537
|
+
<ToggleGroupItem value="left">Left</ToggleGroupItem>
|
|
6538
|
+
</ToggleGroup>`,
|
|
6539
|
+
storyPath: "data-entry/ToggleGroup.stories.tsx",
|
|
6540
|
+
rules: [3, 13]
|
|
6541
|
+
},
|
|
6542
|
+
{
|
|
6543
|
+
name: "AspectRatio",
|
|
6544
|
+
group: "layout",
|
|
6545
|
+
tagline: "Radix AspectRatio wrapper for stable media and preview frames.",
|
|
6546
|
+
props: [
|
|
6547
|
+
{ name: "ratio", type: "number", defaultValue: "16 / 9", description: "Width divided by height." },
|
|
6548
|
+
{ name: "children", type: "ReactNode", description: "Content constrained to the ratio." }
|
|
6549
|
+
],
|
|
6550
|
+
usage: ["DO use AspectRatio for media, maps, charts, or previews that must not jump during load.", "DON'T use it for unconstrained text content."],
|
|
6551
|
+
useCases: ["Video embed frame", "Image preview slot", "Dashboard chart placeholder"],
|
|
6552
|
+
related: ["CardCover", "Skeleton"],
|
|
6553
|
+
example: `import { AspectRatio } from "@godxjp/ui/layout";
|
|
6554
|
+
|
|
6555
|
+
<AspectRatio ratio={16 / 9}>...</AspectRatio>`,
|
|
6556
|
+
storyPath: "layout/AspectRatio.stories.tsx",
|
|
6557
|
+
rules: [2, 3]
|
|
6131
6558
|
}
|
|
6132
6559
|
];
|
|
6133
6560
|
function findComponent(name) {
|
|
@@ -6369,9 +6796,9 @@ var CARDINAL_RULES = [
|
|
|
6369
6796
|
{ 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." },
|
|
6370
6797
|
{ 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." },
|
|
6371
6798
|
{ 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." },
|
|
6372
|
-
{ 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 (`
|
|
6373
|
-
{ number: 35, title: "Status chips never wrap", body: "A `
|
|
6374
|
-
{ number: 36, title: "
|
|
6799
|
+
{ 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`.' },
|
|
6800
|
+
{ 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." },
|
|
6801
|
+
{ 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." },
|
|
6375
6802
|
{ 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.)" },
|
|
6376
6803
|
{ 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." },
|
|
6377
6804
|
{ 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." },
|
|
@@ -6385,7 +6812,7 @@ function findRule(num) {
|
|
|
6385
6812
|
var PATTERNS = [
|
|
6386
6813
|
{
|
|
6387
6814
|
name: "common-fixes",
|
|
6388
|
-
tagline: "Fix the most common @godxjp/ui consumer mistakes & visual bugs (
|
|
6815
|
+
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.",
|
|
6389
6816
|
tags: ["fixes", "migration", "bug", "cardstat", "statusbadge", "datatable", "sidebar", "gotcha", "review"],
|
|
6390
6817
|
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
|
|
6391
6818
|
// 0) \u2605 MOST COMMON: <Card> body has NO padding (content is flush against the edges)
|
|
@@ -6401,19 +6828,19 @@ var PATTERNS = [
|
|
|
6401
6828
|
// empty rows \u2192 DataTable's built-in empty / <EmptyState> (not a custom data.length===0 guard).
|
|
6402
6829
|
// If a primitive exists, USE it \u2014 don't reinvent it.
|
|
6403
6830
|
|
|
6404
|
-
// 1)
|
|
6405
|
-
// Cause:
|
|
6406
|
-
// \u274C <Card><CardContent><
|
|
6407
|
-
// \u2705 <ResponsiveGrid columns={4}><
|
|
6831
|
+
// 1) StatCard shows a DOUBLE border (too thick)
|
|
6832
|
+
// Cause: StatCard IS already a bordered Card. Don't wrap it.
|
|
6833
|
+
// \u274C <Card><CardContent><StatCard label="x" value="1" /></CardContent></Card>
|
|
6834
|
+
// \u2705 <ResponsiveGrid columns={4}><StatCard label="x" value="1" /></ResponsiveGrid>
|
|
6408
6835
|
// Need a section title? Use a heading, NOT a Card:
|
|
6409
6836
|
// \u2705 <Stack gap="sm"><div className="text-sm font-medium">KPI</div>
|
|
6410
|
-
// <ResponsiveGrid columns={4}><
|
|
6837
|
+
// <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Stack>
|
|
6411
6838
|
|
|
6412
|
-
// 2)
|
|
6839
|
+
// 2) Badge renders grey with a \u25CB (no colour) for localized/tier labels
|
|
6413
6840
|
// Cause: it auto-maps only English lifecycle keys. (@godxjp/ui >= 6.1)
|
|
6414
|
-
// \u274C <
|
|
6415
|
-
// \u2705 <
|
|
6416
|
-
// \u2705 <
|
|
6841
|
+
// \u274C <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" />
|
|
6842
|
+
// \u2705 <Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" variant="success" icon={null} /> // tier \u2192 pill, no icon
|
|
6843
|
+
// \u2705 <Badge status="active">\u516C\u958B\u4E2D</Badge> // lifecycle \u2192 keep icon
|
|
6417
6844
|
|
|
6418
6845
|
// 3) Table text collapses to one char per line, or a chip wraps
|
|
6419
6846
|
// Cause: pre-6.1.2. (@godxjp/ui >= 6.1.2 \u2192 cells + chips are nowrap)
|
|
@@ -6450,7 +6877,7 @@ var PATTERNS = [
|
|
|
6450
6877
|
|
|
6451
6878
|
// 10) Hide a column on mobile / sign-aware KPI delta (@godxjp/ui >= 6.2.0)
|
|
6452
6879
|
// \u2705 columns: [{ key: "email", header: "\u30E1\u30FC\u30EB", hiddenOnMobile: true }]
|
|
6453
|
-
// \u2705 <
|
|
6880
|
+
// \u2705 <StatCard label="\u58F2\u4E0A" value="\xA58.2M" delta="+12%" /> // + green / - red; inverse flips`
|
|
6454
6881
|
},
|
|
6455
6882
|
{
|
|
6456
6883
|
name: "signup-form",
|
|
@@ -6604,12 +7031,12 @@ export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
|
|
|
6604
7031
|
},
|
|
6605
7032
|
{
|
|
6606
7033
|
name: "inertia-list-page",
|
|
6607
|
-
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable +
|
|
7034
|
+
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable + Badge + Pagination (current primitive API).",
|
|
6608
7035
|
tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
|
|
6609
7036
|
code: `import { Head, router } from "@inertiajs/react"
|
|
6610
7037
|
import { useMemo, useState } from "react"
|
|
6611
7038
|
import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
|
|
6612
|
-
import { Card, CardContent,
|
|
7039
|
+
import { Card, CardContent, StatCard, DataTable, EmptyState, Badge, type ColumnDef } from "@godxjp/ui/data-display"
|
|
6613
7040
|
import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
|
|
6614
7041
|
import { FilterBar, FilterGroup, Pagination } from "@godxjp/ui/navigation"
|
|
6615
7042
|
import { formatDate } from "@godxjp/ui/datetime"
|
|
@@ -6633,8 +7060,8 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
6633
7060
|
// ColumnDef = { key, header, render?, align?: "left"|"center"|"right", sortable?, width? }
|
|
6634
7061
|
const columns: ColumnDef<Coupon>[] = [
|
|
6635
7062
|
{ key: "name", header: "\u30AF\u30FC\u30DD\u30F3\u540D", render: (c) => <span className="font-medium">{c.name}</span> },
|
|
6636
|
-
{ key: "scope", header: "\u30B9\u30B3\u30FC\u30D7", render: (c) => <
|
|
6637
|
-
{ key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (c) => <
|
|
7063
|
+
{ key: "scope", header: "\u30B9\u30B3\u30FC\u30D7", render: (c) => <Badge status={c.scope} variant="info" icon={null} /> },
|
|
7064
|
+
{ key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (c) => <Badge status={c.status} /> },
|
|
6638
7065
|
{ key: "valid", header: "\u6709\u52B9\u671F\u9593", render: (c) => \`\${formatDate(c.validFrom)} \u301C \${formatDate(c.validTo)}\` },
|
|
6639
7066
|
{ key: "usage", header: "\u5229\u7528\u6570", align: "right", render: (c) => c.usage.toLocaleString() },
|
|
6640
7067
|
]
|
|
@@ -6646,9 +7073,9 @@ function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
|
6646
7073
|
<PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
|
|
6647
7074
|
<Stack gap="lg">
|
|
6648
7075
|
<ResponsiveGrid columns={3}>
|
|
6649
|
-
<
|
|
6650
|
-
<
|
|
6651
|
-
<
|
|
7076
|
+
<StatCard label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
|
|
7077
|
+
<StatCard label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
|
|
7078
|
+
<StatCard label="\u4EF6\u6570" value={coupons.length} />
|
|
6652
7079
|
</ResponsiveGrid>
|
|
6653
7080
|
|
|
6654
7081
|
<FilterBar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
|
|
@@ -6688,11 +7115,11 @@ export default Coupons`
|
|
|
6688
7115
|
},
|
|
6689
7116
|
{
|
|
6690
7117
|
name: "inertia-detail-page",
|
|
6691
|
-
tagline: "Inertia detail page \u2014 receives {id} prop,
|
|
7118
|
+
tagline: "Inertia detail page \u2014 receives {id} prop, Descriptions (compound) + StatCard + EmptyState fallback.",
|
|
6692
7119
|
tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
|
|
6693
7120
|
code: `import { Head, router } from "@inertiajs/react"
|
|
6694
7121
|
import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
|
|
6695
|
-
import { Card, CardContent,
|
|
7122
|
+
import { Card, CardContent, StatCard, EmptyState, Descriptions, Badge } from "@godxjp/ui/data-display"
|
|
6696
7123
|
import { Button } from "@godxjp/ui/general"
|
|
6697
7124
|
import { formatDate } from "@godxjp/ui/datetime"
|
|
6698
7125
|
import { ArrowLeft } from "lucide-react"
|
|
@@ -6721,20 +7148,20 @@ function MemberShow({ id }: { id: string }) {
|
|
|
6721
7148
|
<PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
|
|
6722
7149
|
<Stack gap="lg">
|
|
6723
7150
|
<ResponsiveGrid columns={4}>
|
|
6724
|
-
<
|
|
6725
|
-
<
|
|
6726
|
-
<
|
|
6727
|
-
<
|
|
7151
|
+
<StatCard label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
|
|
7152
|
+
<StatCard label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
|
|
7153
|
+
<StatCard label="\u30DD\u30A4\u30F3\u30C8" value={member.points.toLocaleString()} />
|
|
7154
|
+
<StatCard label="LTV" value={\`\xA5\${member.ltv.toLocaleString()}\`} />
|
|
6728
7155
|
</ResponsiveGrid>
|
|
6729
7156
|
<Card>
|
|
6730
7157
|
<CardContent>
|
|
6731
|
-
{/*
|
|
6732
|
-
<
|
|
6733
|
-
<
|
|
6734
|
-
<
|
|
6735
|
-
<
|
|
6736
|
-
<
|
|
6737
|
-
</
|
|
7158
|
+
{/* Descriptions is COMPOUND \u2014 value goes in children, not a prop */}
|
|
7159
|
+
<Descriptions columns={2}>
|
|
7160
|
+
<Descriptions.Item label="\u6C0F\u540D">{member.name}</Descriptions.Item>
|
|
7161
|
+
<Descriptions.Item label="\u30E9\u30F3\u30AF"><Badge status={member.rank} variant="info" icon={null} /></Descriptions.Item>
|
|
7162
|
+
<Descriptions.Item label="\u30B9\u30C6\u30FC\u30BF\u30B9"><Badge status={member.status} /></Descriptions.Item>
|
|
7163
|
+
<Descriptions.Item label="\u767B\u9332\u65E5">{formatDate(member.registeredAt)}</Descriptions.Item>
|
|
7164
|
+
</Descriptions>
|
|
6738
7165
|
</CardContent>
|
|
6739
7166
|
</Card>
|
|
6740
7167
|
</Stack>
|
|
@@ -6780,31 +7207,31 @@ export const withCrmLayout = [CrmLayout] // \u2705 array \u2192 Inertia passes
|
|
|
6780
7207
|
const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; return x - Math.floor(x) }`
|
|
6781
7208
|
},
|
|
6782
7209
|
{
|
|
6783
|
-
name: "
|
|
6784
|
-
tagline: "Colour a
|
|
7210
|
+
name: "badge-coloring",
|
|
7211
|
+
tagline: "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props, @godxjp/ui \u2265 6.1).",
|
|
6785
7212
|
tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
|
|
6786
|
-
code: `import {
|
|
7213
|
+
code: `import { Badge } from "@godxjp/ui/data-display"
|
|
6787
7214
|
|
|
6788
|
-
//
|
|
7215
|
+
// Badge auto-colours a fixed set of English LIFECYCLE keys:
|
|
6789
7216
|
// active/completed (success \u2713) \xB7 draft (neutral \u25CB) \xB7 pending/temporary (warning \u23F1)
|
|
6790
7217
|
// scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
|
|
6791
7218
|
// Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
|
|
6792
7219
|
|
|
6793
7220
|
// 1) Lifecycle with localized text \u2014 map to the key, keep JP via \`label\` (icon stays):
|
|
6794
|
-
<
|
|
7221
|
+
<Badge status="active">\u516C\u958B\u4E2D</Badge> // green \u2713 \u516C\u958B\u4E2D
|
|
6795
7222
|
|
|
6796
7223
|
// 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
|
|
6797
|
-
<
|
|
7224
|
+
<Badge status="\u516C\u958B\u4E2D" variant="success" />
|
|
6798
7225
|
|
|
6799
7226
|
// 3) Tier / category \u2014 coloured pill, drop the misleading glyph with icon={null}:
|
|
6800
|
-
<
|
|
6801
|
-
<
|
|
6802
|
-
<
|
|
7227
|
+
<Badge status="\u30D7\u30EC\u30DF\u30A2\u30E0" variant="success" icon={null} />
|
|
7228
|
+
<Badge status="\u30B4\u30FC\u30EB\u30C9" variant="warning" icon={null} />
|
|
7229
|
+
<Badge status="\u6CD5\u4EBA\u5171\u901A" variant="info" icon={null} />
|
|
6803
7230
|
|
|
6804
|
-
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type
|
|
7231
|
+
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type BadgeTone)
|
|
6805
7232
|
// RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
|
|
6806
7233
|
// narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
|
|
6807
|
-
// import that instead of the raw
|
|
7234
|
+
// import that instead of the raw Badge across pages.`
|
|
6808
7235
|
}
|
|
6809
7236
|
];
|
|
6810
7237
|
function findPattern(name) {
|
|
@@ -7552,7 +7979,7 @@ content, system-bar safe area. The framework's \`useBreakpoint\`
|
|
|
7552
7979
|
body: `10+ tabs at the top of a screen, no priority. User has to read all
|
|
7553
7980
|
of them to find the right one. AI default: "more tabs = more
|
|
7554
7981
|
features = better".`,
|
|
7555
|
-
fix: `2-4 tabs max. If you have more categories, use a sidebar (
|
|
7982
|
+
fix: `2-4 tabs max. If you have more categories, use a sidebar (Sidebar),
|
|
7556
7983
|
or a Cascader / Tree picker. Tabs are for switching between PEERS
|
|
7557
7984
|
(2-4 mutually exclusive views of the same data).`
|
|
7558
7985
|
},
|