@godxjp/ui-mcp 0.3.0 → 0.4.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 CHANGED
@@ -12,643 +12,973 @@ import {
12
12
 
13
13
  // src/data/components.ts
14
14
  var COMPONENTS = [
15
- // ─── general ────────────────────────────────────────────────────
15
+ // ─── layout ─────────────────────────────────────────────────────────────
16
16
  {
17
- name: "Button",
18
- group: "general",
19
- tagline: "Canonical action primitive. 6 variants \xD7 4 sizes. Loading state, icon slots, Slot-based asChild for routing wrap.",
17
+ name: "PageContainer",
18
+ group: "layout",
19
+ tagline: "Mandatory page shell \u2014 EVERY page wraps its content in PageContainer (title/subtitle/extra/footer/breadcrumb).",
20
20
  props: [
21
- { name: "variant", type: '"primary" | "secondary" | "outline" | "ghost" | "destructive" | "link"', description: "Visual treatment.", defaultValue: '"primary"' },
22
- { name: "size", type: "SizeWithXSProp", description: '"x-small" | "small" | "default" | "large".', defaultValue: '"default"' },
23
- { name: "block", type: "boolean", description: "Stretch to fill parent width (mobile submit pattern)." },
24
- { name: "loading", type: "boolean", description: "Show spinner + disable interaction." },
25
- { name: "startContent", type: "ReactNode", description: "Left icon slot." },
26
- { name: "endContent", type: "ReactNode", description: "Right icon slot." },
27
- { name: "asChild", type: "boolean", description: "Radix Slot \u2014 wrap a Link / RouterLink without nesting." }
21
+ { name: "title", type: "string", required: true, description: "Page heading rendered as <h1>." },
22
+ { name: "subtitle", type: "string", description: "Secondary line beneath the title." },
23
+ { name: "extra", type: "ReactNode", description: "Action buttons / controls rendered right of the title row." },
24
+ { name: "footer", type: "ReactNode", description: "Content area pinned below the page body." },
25
+ { name: "breadcrumb", type: "BreadcrumbItemProp[]", description: "Ordered trail of { label, to? } segments above the title." },
26
+ { name: "variant", type: '"default" | "narrow" | "flush" | "ghost"', defaultValue: '"default"', description: "Page shell layout; flush removes padding for full-bleed content." },
27
+ { name: "density", type: '"compact" | "default" | "comfortable"', defaultValue: '"default"', description: "Spacing density across the page subtree." },
28
+ { name: "stickyFooter", type: "boolean", defaultValue: "false", description: 'Pin footer to viewport bottom on scroll \u2014 pairs with variant="narrow".' }
28
29
  ],
29
- example: `<Button variant="primary" size="default" startContent={<Save size={14} />}>
30
- \u4FDD\u5B58
31
- </Button>
32
-
33
- <Button variant="ghost" asChild>
34
- <Link to="/settings">\u8A2D\u5B9A</Link>
35
- </Button>`,
36
- docPath: "data-entry/Button.md",
37
- storyPath: "general/Button.stories.tsx",
38
- rules: [14, 23]
30
+ example: `import { PageContainer, Stack } from "@godxjp/ui/layout";
31
+ import { Button } from "@godxjp/ui/general";
32
+
33
+ export default function OrdersPage() {
34
+ return (
35
+ <PageContainer
36
+ title="\u6CE8\u6587\u4E00\u89A7"
37
+ subtitle="\u76F4\u8FD130\u65E5\u9593\u306E\u53D7\u6CE8\u30C7\u30FC\u30BF"
38
+ breadcrumb={[{ label: "\u30DB\u30FC\u30E0", to: "/" }, { label: "\u6CE8\u6587\u4E00\u89A7" }]}
39
+ extra={<Button>\u65B0\u898F\u6CE8\u6587</Button>}
40
+ >
41
+ <Stack gap="lg">{/* page content */}</Stack>
42
+ </PageContainer>
43
+ );
44
+ }`,
45
+ storyPath: "layout/PageContainer.stories.tsx",
46
+ rules: [23]
39
47
  },
40
48
  {
41
- name: "Typography",
42
- group: "general",
43
- tagline: "Headings / paragraph / text / link primitives. `Typography.Title size={1..5}`, `Typography.Text`, `Typography.Paragraph`, `Typography.Link`.",
49
+ name: "Stack",
50
+ group: "layout",
51
+ tagline: "Vertical flex column with token gap \u2014 the default block-stacking primitive (use instead of space-y-*).",
52
+ props: [
53
+ { name: "gap", type: '"xs" | "sm" | "md" | "lg" | "xl"', defaultValue: '"md"', description: "Vertical space between children (design tokens)." },
54
+ { name: "className", type: "string", description: "Extra classes merged via cn()." },
55
+ { name: "children", type: "ReactNode", description: "Block-level children to stack." }
56
+ ],
57
+ example: `import { Stack } from "@godxjp/ui/layout";
58
+
59
+ <Stack gap="lg">
60
+ <KpiRow />
61
+ <FilterBarBlock />
62
+ <TableCard />
63
+ </Stack>`,
64
+ storyPath: "layout/Stack.stories.tsx",
65
+ rules: [2, 40]
66
+ },
67
+ {
68
+ name: "Inline",
69
+ group: "layout",
70
+ tagline: "Horizontal flex row with token gap \u2014 the default inline/row arrangement (use instead of gap-* on a flex div).",
44
71
  props: [
45
- { name: "size (Title)", type: "1 | 2 | 3 | 4 | 5", description: "Heading level (h1\u2013h5)." },
46
- { name: "color", type: "TypographyColor", description: "default | secondary | success | warning | attention | info | destructive." },
47
- { name: "strong", type: "boolean", description: "Bold weight." },
48
- { name: "truncate", type: '"ellipsis" | { lines: number }', description: "Single-line or multi-line truncation." }
72
+ { name: "gap", type: '"xs" | "sm" | "md" | "lg"', defaultValue: '"sm"', description: "Horizontal space between children." },
73
+ { name: "className", type: "string", description: "Extra classes merged via cn()." },
74
+ { name: "children", type: "ReactNode", description: "Inline children in a row." }
49
75
  ],
50
- example: `<Typography.Title size={2}>\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u8A2D\u5B9A</Typography.Title>
51
- <Typography.Paragraph>\u5909\u66F4\u306F\u5373\u5EA7\u306B\u53CD\u6620\u3055\u308C\u307E\u3059\u3002</Typography.Paragraph>
52
- <Typography.Text color="secondary">\u66F4\u65B0\u65E5: 2026-05-19</Typography.Text>`,
53
- storyPath: "general/Typography.stories.tsx",
76
+ example: `import { Inline } from "@godxjp/ui/layout";
77
+ import { Button } from "@godxjp/ui/general";
78
+
79
+ <Inline gap="sm">
80
+ <Button>\u4FDD\u5B58</Button>
81
+ <Button variant="outline">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
82
+ </Inline>`,
83
+ storyPath: "layout/Inline.stories.tsx",
84
+ rules: [2]
85
+ },
86
+ {
87
+ name: "ResponsiveGrid",
88
+ group: "layout",
89
+ tagline: "Auto-responsive card grid \u2014 columns collapse to 1 on mobile, scale up on wider breakpoints.",
90
+ props: [
91
+ { name: "columns", type: "2 | 3 | 4", defaultValue: "3", description: "Target column count at desktop; collapses to 1 on mobile." },
92
+ { name: "children", type: "ReactNode", required: true, description: "Grid items \u2014 typically Card or CardStat." }
93
+ ],
94
+ example: `import { ResponsiveGrid } from "@godxjp/ui/layout";
95
+ import { CardStat } from "@godxjp/ui/data-display";
96
+
97
+ <ResponsiveGrid columns={4}>
98
+ <CardStat label="\u7DCF\u4F1A\u54E1\u6570" value="12,400" />
99
+ <CardStat label="\u516C\u958B\u4E2D\u30AF\u30FC\u30DD\u30F3" value="8" />
100
+ <CardStat label="\u6708\u9593\u5229\u7528\u6570" value="3,210" />
101
+ <CardStat label="\u5272\u5F15\u7DCF\u984D" value="\xA5480,000" />
102
+ </ResponsiveGrid>`,
103
+ storyPath: "layout/ResponsiveGrid.stories.tsx",
104
+ rules: [24, 40]
105
+ },
106
+ {
107
+ name: "AppShell",
108
+ group: "layout",
109
+ tagline: "Root application shell \u2014 composes sidebar, topbar rail, main content area, and optional footer.",
110
+ props: [
111
+ { name: "sidebar", type: "ReactNode", required: true, description: "Sidebar node \u2014 typically a <Sidebar>." },
112
+ { name: "children", type: "ReactNode", required: true, description: "Main page content rendered in <main>." },
113
+ { name: "topbar", type: "ReactNode", description: "Full topbar override; else a rail is built from topbarLeft/topbarRight/logo." },
114
+ { name: "topbarRight", type: "ReactNode", description: "Right slot of the auto-built topbar rail (user menu, switcher)." },
115
+ { name: "topbarLeft", type: "ReactNode", description: "Left slot of the auto-built topbar rail." },
116
+ { name: "logo", type: "ReactNode", description: "Logo at the far-left of the auto-built topbar rail." },
117
+ { name: "sidebarCollapsed", type: "boolean", defaultValue: "false", description: "Collapse the sidebar to icon-only mode." },
118
+ { name: "footer", type: "ReactNode", description: "App-level footer outside the main content area." }
119
+ ],
120
+ example: `import { AppShell, Sidebar } from "@godxjp/ui/layout";
121
+ import { LayoutDashboard, Users } from "lucide-react";
122
+ import { router } from "@inertiajs/react";
123
+
124
+ const sidebar = (
125
+ <Sidebar
126
+ activeId="/dashboard"
127
+ onSelect={(id) => router.visit(id)}
128
+ sections={[{ items: [
129
+ { id: "/dashboard", label: "\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9", icon: LayoutDashboard },
130
+ { id: "/users", label: "\u30E6\u30FC\u30B6\u30FC", icon: Users },
131
+ ] }]}
132
+ product={{ name: "JOVY CRM", role: "\u672C\u90E8", color: "var(--color-primary)" }}
133
+ />
134
+ );
135
+
136
+ export function CrmLayout({ children }: { children: React.ReactNode }) {
137
+ return <AppShell sidebar={sidebar}>{children}</AppShell>;
138
+ }`,
139
+ storyPath: "layout/AppShell.stories.tsx",
54
140
  rules: [23]
55
141
  },
56
- // ─── layout ─────────────────────────────────────────────────────
57
142
  {
58
- name: "Flex",
143
+ name: "Sidebar",
59
144
  group: "layout",
60
- tagline: "Flexbox container with prop-driven config (Ant-style).",
145
+ tagline: "Navigation sidebar with sections, items, product header, and collapsible icon-only mode.",
61
146
  props: [
62
- { name: "vertical", type: "boolean", description: "Column instead of row." },
63
- { name: "gap", type: "FlexGap", description: "number | small | default | large." },
64
- { name: "justify", type: "FlexJustify", description: "start | end | center | space-between | space-around | space-evenly." },
65
- { name: "align", type: "AlignProp", description: "start | end | center | stretch | baseline." },
66
- { name: "wrap", type: "boolean", description: "Allow wrapping." }
147
+ { name: "activeId", type: "string", required: true, description: "Id of the active nav item; drives highlight." },
148
+ { name: "sections", type: "SidebarSectionProp[]", required: true, description: "Array of { label?, items: SidebarItemProp[] } where item = { id, label, icon, badge? }." },
149
+ { name: "onSelect", type: "(id: string) => void", description: "Called on item click; typically router.visit(id)." },
150
+ { name: "product", type: "{ name: string; role?: string; color?: string }", description: "Product/workspace block at the top." },
151
+ { name: "brand", type: "ReactNode", description: "Custom brand node replacing the product block." },
152
+ { name: "collapsed", type: "boolean", defaultValue: "false", description: "Icon-only mode; labels/section headings hidden." },
153
+ { name: "footer", type: "ReactNode", description: "Bottom slot (user info, logout)." }
67
154
  ],
68
- example: `<Flex vertical gap="default" align="stretch">
69
- <Input placeholder="\u540D\u524D" />
70
- <Input placeholder="\u30E1\u30FC\u30EB" />
71
- <Button type="submit">\u9001\u4FE1</Button>
72
- </Flex>`,
73
- storyPath: "layout/Flex.stories.tsx",
74
- rules: [23, 24]
155
+ example: `import { Sidebar } from "@godxjp/ui/layout";
156
+ import { LayoutDashboard, Users } from "lucide-react";
157
+ import { router, usePage } from "@inertiajs/react";
158
+
159
+ export function AppSidebar() {
160
+ const { url } = usePage();
161
+ return (
162
+ <Sidebar
163
+ activeId={url}
164
+ onSelect={(id) => router.visit(id)}
165
+ sections={[{ label: "\u30E1\u30A4\u30F3", items: [
166
+ { id: "/dashboard", label: "\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9", icon: LayoutDashboard },
167
+ { id: "/members", label: "\u4F1A\u54E1\u7BA1\u7406", icon: Users },
168
+ ] }]}
169
+ product={{ name: "JOVY CRM", role: "\u672C\u90E8" }}
170
+ />
171
+ );
172
+ }`,
173
+ storyPath: "layout/Sidebar.stories.tsx",
174
+ rules: [23]
75
175
  },
76
176
  {
77
- name: "Grid",
177
+ name: "Topbar",
78
178
  group: "layout",
79
- tagline: "CSS Grid container. `cols` number or template string. Mobile-first gap.",
179
+ tagline: "Application topbar with product/project switcher and search/notification slots (or use AppShell's topbarRight).",
80
180
  props: [
81
- { name: "cols", type: "number | string", description: "Equal columns OR full grid-template-columns string." },
82
- { name: "gap", type: "GridGap", description: "number | small | default | large." }
181
+ { name: "product", type: "{ name: string; color?: string }", required: true, description: "Current product chip." },
182
+ { name: "project", type: "{ name: string } | null", description: "Current project chip; null shows placeholder." },
183
+ { name: "onSearchOpen", type: "() => void", description: "Called when the search bar is clicked." },
184
+ { name: "onProductOpen", type: "() => void", description: "Called when the product chip is clicked." }
83
185
  ],
84
- example: `<Grid cols={3} gap="default">
85
- <Card title="\u58F2\u4E0A">\xA5120,000</Card>
86
- <Card title="\u5229\u76CA">\xA545,000</Card>
87
- <Card title="\u9867\u5BA2">+12</Card>
88
- </Grid>`,
89
- storyPath: "layout/Grid.stories.tsx",
90
- rules: [23, 24]
186
+ example: `import { Topbar } from "@godxjp/ui/layout";
187
+
188
+ <Topbar product={{ name: "JOVY CRM" }} project={{ name: "\u672C\u756A\u74B0\u5883" }} />`,
189
+ storyPath: "layout/Topbar.stories.tsx",
190
+ rules: [23]
91
191
  },
92
192
  {
93
- name: "Row / Col",
193
+ name: "PageInset",
94
194
  group: "layout",
95
- tagline: "24-column responsive grid (Ant-style). `gutter` rebound per breakpoint.",
195
+ tagline: 'Padded horizontal strip aligned with the page header \u2014 use inside variant="flush" for filter bars / intros.',
96
196
  props: [
97
- { name: "gutter", type: "number | [h,v] | { xs, sm, md, lg, xl, xxl }", description: "Pixel gap, optionally per breakpoint." },
98
- { name: "xs / sm / md / lg / xl", type: "number", description: "Column span at that breakpoint (1-24)." }
197
+ { name: "children", type: "ReactNode", description: "Content rendered with standard page horizontal padding." },
198
+ { name: "className", type: "string", description: "Extra classes." }
99
199
  ],
100
- example: `<Row gutter={{ xs: 8, md: 16 }}>
101
- <Col xs={24} md={8}><Card>One</Card></Col>
102
- <Col xs={24} md={8}><Card>Two</Card></Col>
103
- <Col xs={24} md={8}><Card>Three</Card></Col>
104
- </Row>`,
105
- storyPath: "layout/Grid.stories.tsx",
106
- rules: [23, 24]
200
+ example: `import { PageContainer, PageInset } from "@godxjp/ui/layout";
201
+
202
+ <PageContainer title="\u5546\u54C1\u4E00\u89A7" variant="flush">
203
+ <PageInset><FilterBarBlock /></PageInset>
204
+ {/* full-bleed table below */}
205
+ </PageContainer>`,
206
+ storyPath: "layout/PageInset.stories.tsx",
207
+ rules: []
208
+ },
209
+ {
210
+ name: "SplitPane",
211
+ group: "layout",
212
+ tagline: "Two-column layout with a main content area and a fixed-width aside panel.",
213
+ props: [
214
+ { name: "children", type: "ReactNode", required: true, description: "Main (left) content." },
215
+ { name: "aside", type: "ReactNode", required: true, description: "Aside (right) panel content." },
216
+ { name: "asideWidth", type: '"sm" | "md"', defaultValue: '"md"', description: "Width preset for the aside column." }
217
+ ],
218
+ example: `import { SplitPane } from "@godxjp/ui/layout";
219
+
220
+ <SplitPane aside={<DetailPanel />} asideWidth="sm">
221
+ <MainContent />
222
+ </SplitPane>`,
223
+ storyPath: "layout/SplitPane.stories.tsx",
224
+ rules: [24]
225
+ },
226
+ {
227
+ name: "Breadcrumb",
228
+ group: "layout",
229
+ tagline: "Standalone breadcrumb nav rendering an ordered trail of page segments.",
230
+ props: [
231
+ { name: "items", type: "BreadcrumbItemProp[]", required: true, description: "Array of { label, to? } \u2014 omit `to` on the last (current) segment." }
232
+ ],
233
+ example: `import { Breadcrumb } from "@godxjp/ui/layout";
234
+
235
+ <Breadcrumb items={[
236
+ { label: "\u30DB\u30FC\u30E0", to: "/" },
237
+ { label: "\u4F1A\u54E1\u7BA1\u7406", to: "/members" },
238
+ { label: "\u7530\u4E2D \u592A\u90CE" },
239
+ ]} />`,
240
+ storyPath: "layout/Breadcrumb.stories.tsx",
241
+ rules: []
242
+ },
243
+ // ─── general ────────────────────────────────────────────────────────────
244
+ {
245
+ name: "Button",
246
+ group: "general",
247
+ tagline: "Core button with variant + size presets, built on cva and Radix Slot (asChild).",
248
+ props: [
249
+ { name: "variant", type: '"default" | "destructive" | "outline" | "secondary" | "ghost" | "link"', defaultValue: '"default"', description: "Visual style." },
250
+ { name: "size", type: '"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg"', defaultValue: '"default"', description: "Size preset (height, padding, icon dims)." },
251
+ { name: "asChild", type: "boolean", defaultValue: "false", description: "Render as Radix Slot \u2014 merge props onto the child (<a>/<Link>)." },
252
+ { name: "disabled", type: "boolean", description: "Disable the button." },
253
+ { name: "onClick", type: "React.MouseEventHandler<HTMLButtonElement>", description: "Click handler." }
254
+ ],
255
+ example: `import { Button } from "@godxjp/ui/general";
256
+ import { Trash2 } from "lucide-react";
257
+
258
+ <>
259
+ <Button>\u4FDD\u5B58</Button>
260
+ <Button variant="outline" size="sm">\u7DE8\u96C6</Button>
261
+ <Button variant="ghost" size="icon-sm"><Trash2 className="size-4" /></Button>
262
+ </>`,
263
+ storyPath: "general/Button.stories.tsx",
264
+ rules: [23]
265
+ },
266
+ // ─── data-display ───────────────────────────────────────────────────────
267
+ {
268
+ name: "DataTable",
269
+ group: "data-display",
270
+ tagline: "Full-width admin list. Lives in its OWN row (Card + CardContent flush) \u2014 never inside a narrow grid column. Cells default to white-space:nowrap (scroll, don't crush).",
271
+ props: [
272
+ { name: "data", type: "T[]", required: true, description: "Row data array." },
273
+ { name: "columns", type: "ColumnDef<T>[]", required: true, description: "Each: { key, header, render?(row), align?: 'left'|'center'|'right', sortable?, width? }. width is a Tailwind class string e.g. 'w-64'." },
274
+ { name: "getRowId", type: "(row: T) => string", description: "Row key extractor (falls back to row.id). Required when selectable." },
275
+ { name: "onRowClick", type: "(row: T) => void", description: "Navigate on row click; ignored when target is interactive." },
276
+ { name: "selectable", type: "boolean", defaultValue: "false", description: "Enable checkbox column + bulk selection." },
277
+ { name: "selected", type: "Set<string>", description: "Controlled selection set." },
278
+ { name: "onSelectChange", type: "(next: Set<string>) => void", description: "Selection change handler." },
279
+ { name: "onSortChange", type: "(sort | undefined) => void", description: "Fires when a sortable header is clicked; undefined clears sort." }
280
+ ],
281
+ example: `import { Card, CardContent, DataTable, StatusBadge } from "@godxjp/ui/data-display";
282
+ import type { ColumnDef } from "@godxjp/ui/data-display";
283
+ import { router } from "@inertiajs/react";
284
+
285
+ type Member = { id: string; name: string; status: string };
286
+ const columns: ColumnDef<Member>[] = [
287
+ { key: "name", header: "\u6C0F\u540D", width: "w-64", render: (m) => <span className="font-medium">{m.name}</span> },
288
+ { key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (m) => <StatusBadge status={m.status} /> },
289
+ ];
290
+
291
+ <Card>
292
+ <CardContent flush>
293
+ <DataTable data={members} columns={columns} getRowId={(m) => m.id}
294
+ onRowClick={(m) => router.visit("/members/" + m.id)} />
295
+ </CardContent>
296
+ </Card>`,
297
+ storyPath: "data-display/DataTable.stories.tsx",
298
+ rules: [37, 39, 35]
107
299
  },
108
- // ─── data-display ───────────────────────────────────────────────
109
300
  {
110
301
  name: "Card",
111
302
  group: "data-display",
112
- tagline: "Surface container. Title + subtitle + body + footer slots, accent / band decorations, padding scale, hoverable.",
303
+ tagline: "Surface container with optional accent stripe, variant fill, size, and density. Compose with CardHeader/CardTitle/CardContent/CardFooter.",
113
304
  props: [
114
- { name: "title", type: "ReactNode", description: "Header title." },
115
- { name: "subtitle", type: "ReactNode", description: "Sub-text under title." },
116
- { name: "kicker", type: "ReactNode", description: "Small uppercase kicker above title." },
117
- { name: "extra", type: "ReactNode", description: "Right-aligned action slot in header." },
118
- { name: "padding", type: "PaddingProp", description: "tight | default | cozy | none." },
119
- { name: "tone", type: "CardTone", description: "default | muted | outline-only." },
120
- { name: "accent", type: "CardAccent", description: "Edge accent + 'featured' ring." },
121
- { name: "band", type: "CardBand", description: "Top-edge color strip." },
122
- { name: "actions", type: "ReactNode", description: "Right-aligned footer action bar." },
123
- { name: "hoverable", type: "boolean", description: "Hover lift + border tint." }
305
+ { name: "accent", type: '"primary" | "success" | "warning" | "info" | "attention" | "destructive"', description: "3px left-edge semantic accent stripe." },
306
+ { name: "variant", type: '"default" | "muted" | "outline" | "featured"', defaultValue: '"default"', description: "Surface fill style." },
307
+ { name: "size", type: '"default" | "compact"', defaultValue: '"default"', description: "Card size preset." },
308
+ { name: "density", type: '"tight" | "cozy"', description: "Internal padding density (base 16 / tight 12 / cozy 20)." }
124
309
  ],
125
- example: `<Card
126
- title="\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB"
127
- subtitle="\u4ED6\u306E\u30E1\u30F3\u30D0\u30FC\u306B\u516C\u958B\u3055\u308C\u308B\u60C5\u5831\u3067\u3059\u3002"
128
- actions={
129
- <>
130
- <Button variant="ghost">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
131
- <Button variant="primary">\u4FDD\u5B58</Button>
132
- </>
133
- }
134
- >
135
- {/* form fields */}
310
+ example: `import { Card, CardHeader, CardTitle, CardContent } from "@godxjp/ui/data-display";
311
+
312
+ <Card accent="success">
313
+ <CardHeader><CardTitle>\u6CE8\u6587\u30B5\u30DE\u30EA\u30FC</CardTitle></CardHeader>
314
+ <CardContent>\u7DCF\u58F2\u4E0A: \xA51,234,567</CardContent>
136
315
  </Card>`,
137
- docPath: "data-display/Card.md",
138
316
  storyPath: "data-display/Card.stories.tsx",
139
- rules: [22, 23]
317
+ rules: []
140
318
  },
141
319
  {
142
- name: "Table",
320
+ name: "CardContent",
143
321
  group: "data-display",
144
- tagline: "Slim primitive. Columns \xD7 rows + interaction features per cell/row (sort, resize, expand, edit, tree, group, sticky). NO chrome \u2014 use DataTable composite for toolbar/views/batch/pagination.",
322
+ tagline: "Card body. flush = edge-to-edge (for DataTable/tabs); tight = no top gap; solo = no header above. NEVER put a FilterBar inside flush (it loses padding).",
145
323
  props: [
146
- { name: "columns", type: "TableColumn<T>[]", required: true, description: "TanStack ColumnDef array. `meta.sticky` for responsive sticky columns." },
147
- { name: "data", type: "T[]", required: true, description: "Row data." },
148
- { name: "rowKey", type: "string | (row) => string", required: true, description: "Stable row identity." },
149
- { name: "density", type: "DensityProp", description: "compact | default | comfortable." },
150
- { name: "bordered", type: "boolean", description: "Inner border lines." },
151
- { name: "selection", type: "TableSelectionConfig", description: "Checkbox column + selected row keys." },
152
- { name: "sortable", type: "TableSortState | boolean", description: "Controlled multi-sort." },
153
- { name: "expandable / tree / groupBy / editing / sticky", type: "configs", description: "Optional features." }
324
+ { name: "flush", type: "boolean", description: "Remove horizontal padding for edge-to-edge tables / tabs lists." },
325
+ { name: "tight", type: "boolean", description: "No top gap after header \u2014 pair with flush toolbars/tabs." },
326
+ { name: "solo", type: "boolean", description: "No header above: top padding matches the card shell." }
154
327
  ],
155
- example: `<Table
156
- columns={[
157
- { accessorKey: "name", header: "\u6C0F\u540D", meta: { sticky: { side: "left", from: "md" } } },
158
- { accessorKey: "role", header: "\u5F79\u8077" },
159
- { accessorKey: "status", header: "\u72B6\u614B",
160
- cell: ({ row }) => row.original.status === "active"
161
- ? <Badge variant="success" dot>\u7A3C\u50CD\u4E2D</Badge>
162
- : <Badge variant="neutral" dot>\u4F11\u8077</Badge>
163
- },
164
- ]}
165
- data={employees}
166
- rowKey="id"
167
- />`,
168
- docPath: "data-display/Table.md",
169
- storyPath: "data-display/Table.stories.tsx",
170
- rules: [22, 23, 34]
328
+ example: `import { Card, CardContent, DataTable } from "@godxjp/ui/data-display";
329
+
330
+ <Card>
331
+ <CardContent flush>
332
+ <DataTable data={rows} columns={columns} />
333
+ </CardContent>
334
+ </Card>`,
335
+ storyPath: "data-display/Card.stories.tsx",
336
+ rules: [37, 38]
337
+ },
338
+ {
339
+ name: "CardStat",
340
+ group: "data-display",
341
+ tagline: "KPI tile with label, value, optional hint and delta. NO accent prop (accent is a Card prop).",
342
+ props: [
343
+ { name: "label", type: "ReactNode", required: true, description: "Metric name." },
344
+ { name: "value", type: "ReactNode", required: true, description: "Metric value (string/number/ReactNode)." },
345
+ { name: "hint", type: "ReactNode", description: "Secondary context below the value." },
346
+ { name: "delta", type: "ReactNode", description: "Compact trend text beside the value." },
347
+ { name: "layout", type: '"stacked" | "inline"', defaultValue: '"stacked"', description: "stacked = label over value; inline = label left / value right." },
348
+ { name: "align", type: '"start" | "end"', description: "Align the metric group." }
349
+ ],
350
+ example: `import { CardStat } from "@godxjp/ui/data-display";
351
+ import { ResponsiveGrid } from "@godxjp/ui/layout";
352
+
353
+ <ResponsiveGrid columns={3}>
354
+ <CardStat label="\u7DCF\u4F1A\u54E1\u6570" value="12,450" hint="\u5148\u6708\u6BD4 +3%" />
355
+ <CardStat label="\u6708\u6B21\u58F2\u4E0A" value="\xA58,200,000" delta="+12%" />
356
+ <CardStat label="\u5229\u7528\u7387" value="68.4%" />
357
+ </ResponsiveGrid>`,
358
+ storyPath: "data-display/CardStat.stories.tsx",
359
+ rules: []
360
+ },
361
+ {
362
+ name: "StatusBadge",
363
+ group: "data-display",
364
+ tagline: "Lifecycle chip that auto-maps English keys (active/draft/pending/failed/\u2026) to tone + icon. For localized labels or tiers, pass tone explicitly; pass icon={null} for tiers. Chips never wrap.",
365
+ props: [
366
+ { name: "status", type: "string", required: true, description: "Lifecycle key or any domain string. Unknown strings fall back to neutral unless tone is set." },
367
+ { name: "tone", type: '"success" | "warning" | "destructive" | "info" | "neutral"', description: "Override the resolved tone (escape hatch for localized / tier values)." },
368
+ { name: "icon", type: "LucideIcon | null", description: "Override the icon; null hides it \u2014 preferred for tier / category badges." },
369
+ { name: "label", type: "ReactNode", description: "Override display text (default: i18n of key, or raw status)." }
370
+ ],
371
+ example: `import { StatusBadge } from "@godxjp/ui/data-display";
372
+
373
+ <>
374
+ <StatusBadge status="active" label="\u516C\u958B\u4E2D" /> {/* green check */}
375
+ <StatusBadge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} /> {/* tier pill, no icon */}
376
+ <StatusBadge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
377
+ </>`,
378
+ storyPath: "data-display/StatusBadge.stories.tsx",
379
+ rules: [35, 36]
171
380
  },
172
381
  {
173
382
  name: "Badge",
174
383
  group: "data-display",
175
- tagline: "Status pill (numeric or short word). Semantic color via `variant`, visual treatment via `appearance`.",
384
+ tagline: "Plain label chip with semantic variants. Use for static category tags; use StatusBadge for lifecycle status.",
176
385
  props: [
177
- { name: "variant", type: '"primary" | "success" | "warning" | "info" | "destructive" | "attention" | "neutral" | "outline"', description: "Semantic color slot. Use `destructive` (NOT `error` \u2014 Tag migration v5.0)." },
178
- { name: "appearance", type: '"soft" | "solid" | "outline"', description: "Visual treatment.", defaultValue: '"soft"' },
179
- { name: "dot", type: "boolean", description: "Show colored dot before label.", defaultValue: "true" }
386
+ { name: "variant", type: '"default" | "secondary" | "destructive" | "outline" | "success" | "warning"', defaultValue: '"default"', description: "Visual variant." },
387
+ { name: "children", type: "ReactNode", required: true, description: "Badge text/content." }
180
388
  ],
181
- example: `<Badge variant="success" dot>\u627F\u8A8D\u6E08</Badge>
182
- <Badge variant="destructive" appearance="solid">\u5374\u4E0B</Badge>
183
- <Badge variant="neutral" dot={false}>\u4E0B\u66F8\u304D</Badge>`,
184
- docPath: "data-display/Badge.md",
389
+ example: `import { Badge } from "@godxjp/ui/data-display";
390
+
391
+ <Badge variant="secondary">A/B</Badge>
392
+ <Badge variant="success">\u627F\u8A8D\u6E08</Badge>`,
185
393
  storyPath: "data-display/Badge.stories.tsx",
186
- rules: [23]
394
+ rules: [35]
187
395
  },
188
396
  {
189
- name: "Tag",
397
+ name: "KeyValueGrid",
190
398
  group: "data-display",
191
- tagline: "Label chip (closable, removable). Distinct from Badge \u2014 multiple in a row, often deletable.",
399
+ tagline: "Responsive definition grid for detail-page metadata. COMPOUND \u2014 value goes in KeyValueGrid.Item children.",
192
400
  props: [
193
- { name: "color", type: "TagPresetColor | string", description: "Preset hue OR any CSS color." },
194
- { name: "bordered", type: "boolean", description: "Show outline.", defaultValue: "true" },
195
- { name: "closable", type: "boolean", description: "Render \xD7 button." },
196
- { name: "onClose", type: "(e) => void", description: "Called when \xD7 clicked." },
197
- { name: "icon", type: "ReactNode", description: "Leading icon." }
401
+ { name: "columns", type: "1 | 2 | 3", defaultValue: "2", description: "Column count; collapses to 1 on mobile." },
402
+ { name: "children", type: "ReactNode", required: true, description: "KeyValueGrid.Item elements." }
198
403
  ],
199
- example: `<Tag color="primary" closable onClose={() => removeTag(id)}>
200
- React
201
- </Tag>`,
202
- docPath: "data-display/Tag.md",
203
- storyPath: "data-display/Tag.stories.tsx",
204
- rules: [23]
404
+ example: `import { KeyValueGrid } from "@godxjp/ui/data-display";
405
+
406
+ <KeyValueGrid columns={2}>
407
+ <KeyValueGrid.Item label="\u4F1A\u54E1ID" mono>{member.id}</KeyValueGrid.Item>
408
+ <KeyValueGrid.Item label="\u30D7\u30E9\u30F3">{member.plan}</KeyValueGrid.Item>
409
+ <KeyValueGrid.Item label="\u30E1\u30E2" span={2}>{member.note}</KeyValueGrid.Item>
410
+ </KeyValueGrid>`,
411
+ storyPath: "data-display/KeyValueGrid.stories.tsx",
412
+ rules: []
205
413
  },
206
414
  {
207
- name: "Avatar",
415
+ name: "EmptyState",
208
416
  group: "data-display",
209
- tagline: "Profile picture with initials fallback. Circle / square. Size = token or pixel number.",
417
+ tagline: "Centred empty placeholder with icon, title, description, and optional CTA.",
210
418
  props: [
211
- { name: "alt", type: "string", description: "Display name \u2014 first 2 initials are the fallback." },
212
- { name: "size", type: '"xs" | "sm" | "default" | "lg" | "xl" | number', description: "Token or pixel size." },
213
- { name: "shape", type: '"circle" | "square"', description: "Geometric form.", defaultValue: '"circle"' },
214
- { name: "src", type: "string", description: "Image URL. Falls back to initials on load error." }
419
+ { name: "title", type: "string", required: true, description: "Primary empty message." },
420
+ { name: "description", type: "string", description: "Secondary helper text." },
421
+ { name: "icon", type: "LucideIcon", description: "Icon above the title." },
422
+ { name: "action", type: "ReactNode", description: "CTA element (e.g. a Button)." }
215
423
  ],
216
- example: `<Avatar size="lg" alt="\u5C71\u7530 \u592A\u90CE" />
217
- <Avatar size={56} alt="Hanako" src="/avatar.jpg" />`,
218
- storyPath: "data-display/Avatar.stories.tsx",
219
- rules: [23]
424
+ example: `import { EmptyState } from "@godxjp/ui/data-display";
425
+
426
+ <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" />`,
427
+ storyPath: "data-display/EmptyState.stories.tsx",
428
+ rules: []
429
+ },
430
+ {
431
+ name: "ProgressMeter",
432
+ group: "data-display",
433
+ tagline: "Horizontal progress bar 0\u2013100 with optional label and semantic tone.",
434
+ props: [
435
+ { name: "value", type: "number", required: true, description: "Progress percentage 0\u2013100 (clamped)." },
436
+ { name: "label", type: "string", description: "Text label beside/below the bar." },
437
+ { name: "tone", type: '"success" | "warning"', defaultValue: '"success"', description: "Bar colour tone." }
438
+ ],
439
+ example: `import { ProgressMeter } from "@godxjp/ui/data-display";
440
+
441
+ <ProgressMeter value={pct} label={pct + "% \u4F7F\u7528\u4E2D"} tone={pct >= 80 ? "warning" : "success"} />`,
442
+ storyPath: "data-display/ProgressMeter.stories.tsx",
443
+ rules: []
220
444
  },
221
445
  {
222
- name: "Separator",
446
+ name: "Timeline",
223
447
  group: "data-display",
224
- tagline: "Horizontal or vertical divider. Radix-backed for ARIA semantics.",
448
+ tagline: "Vertical event list with an icon rail. Current item gets a highlighted glyph.",
225
449
  props: [
226
- { name: "orientation", type: "OrientationProp", description: "horizontal | vertical." },
227
- { name: "decorative", type: "boolean", description: "If true, role=none (not announced)." }
450
+ { name: "items", type: "TimelineItem[]", required: true, description: "Array of { title, location?, time?, note?, current? }." }
228
451
  ],
229
- example: `<Separator />
230
- <Separator orientation="vertical" />`,
231
- storyPath: "data-display/Separator.stories.tsx",
232
- rules: [3, 23]
452
+ example: `import { Timeline } from "@godxjp/ui/data-display";
453
+
454
+ <Timeline items={[
455
+ { title: "\u6CE8\u6587\u53D7\u4ED8", time: "2024-06-01 10:00" },
456
+ { title: "\u767A\u9001\u6E96\u5099\u4E2D", time: "2024-06-01 14:00" },
457
+ { title: "\u914D\u9001\u4E2D", current: true },
458
+ ]} />`,
459
+ storyPath: "data-display/Timeline.stories.tsx",
460
+ rules: []
461
+ },
462
+ {
463
+ name: "Table",
464
+ group: "data-display",
465
+ tagline: "Primitive table shell (Table/TableHeader/TableBody/TableRow/TableHead/TableCell). Prefer DataTable for admin lists; use these for custom one-off tables.",
466
+ props: [
467
+ { name: "children", type: "ReactNode", required: true, description: "TableHeader / TableBody composition." },
468
+ { name: "className", type: "string", description: "Extra classes on the table element." }
469
+ ],
470
+ example: `import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@godxjp/ui/data-display";
471
+
472
+ <Table>
473
+ <TableHeader><TableRow><TableHead>\u9805\u76EE</TableHead><TableHead className="text-right">\u91D1\u984D</TableHead></TableRow></TableHeader>
474
+ <TableBody><TableRow><TableCell>\u9001\u6599</TableCell><TableCell className="text-right">\xA5500</TableCell></TableRow></TableBody>
475
+ </Table>`,
476
+ storyPath: "data-display/Table.stories.tsx",
477
+ rules: []
478
+ },
479
+ {
480
+ name: "DataState",
481
+ group: "data-display",
482
+ tagline: "TanStack Query lifecycle widget \u2014 skeleton / error / empty / success for one useQuery block. Import from @godxjp/ui/query.",
483
+ props: [
484
+ { name: "query", type: "UseQueryResult<T>", required: true, description: "The useQuery result." },
485
+ { name: "skeleton", type: "ReactNode", required: true, description: "Shown while loading." },
486
+ { name: "children", type: "(data) => ReactNode", required: true, description: "Render function with resolved data." },
487
+ { name: "empty", type: "ReactNode", description: "Shown when isEmpty(data) is true." },
488
+ { name: "isEmpty", type: "(data) => boolean", description: "Custom empty check." }
489
+ ],
490
+ example: `import { DataState } from "@godxjp/ui/query";
491
+
492
+ <DataState query={membersQuery} skeleton={<SkeletonTable />} isEmpty={(d) => d.items.length === 0} empty={<EmptyState title="\u4F1A\u54E1\u306A\u3057" />}>
493
+ {(d) => <MemberTable items={d.items} />}
494
+ </DataState>`,
495
+ storyPath: "query/DataState.stories.tsx",
496
+ rules: []
497
+ },
498
+ {
499
+ name: "InfiniteQueryState",
500
+ group: "data-display",
501
+ tagline: "useInfiniteQuery widget \u2014 flatten pages, skeleton/empty/error, load-more footer. Import from @godxjp/ui/query.",
502
+ props: [
503
+ { name: "query", type: "UseInfiniteQueryResult", required: true, description: "The useInfiniteQuery result." },
504
+ { name: "skeleton", type: "ReactNode", required: true, description: "Shown while initial load pends." },
505
+ { name: "flatten", type: "(data) => TFlat", required: true, description: "Reduce pages to a flat list (use flattenItemPages helper)." },
506
+ { name: "children", type: "(flat, helpers) => ReactNode", required: true, description: "Render with flat data + { fetchNextPage, hasNextPage, isFetchingNextPage }." }
507
+ ],
508
+ example: `import { InfiniteQueryState, flattenItemPages } from "@godxjp/ui/query";
509
+
510
+ <InfiniteQueryState query={q} skeleton={<SkeletonRows />} flatten={flattenItemPages} isEmpty={(it) => it.length === 0}>
511
+ {(items) => items.map((a) => <ActivityRow key={a.id} activity={a} />)}
512
+ </InfiniteQueryState>`,
513
+ storyPath: "query/InfiniteQueryState.stories.tsx",
514
+ rules: []
515
+ },
516
+ {
517
+ name: "MutationFeedback",
518
+ group: "data-display",
519
+ tagline: "Inline mutation error \u2014 renders nothing when idle/successful. Import from @godxjp/ui/query.",
520
+ props: [
521
+ { name: "mutation", type: "{ isError, error, isPending }", required: true, description: "useMutation result." },
522
+ { name: "onRetry", type: "() => void", description: "Retry handler." }
523
+ ],
524
+ example: `import { MutationFeedback } from "@godxjp/ui/query";
525
+
526
+ <MutationFeedback mutation={saveMutation} />`,
527
+ storyPath: "query/MutationFeedback.stories.tsx",
528
+ rules: []
529
+ },
530
+ // ─── data-entry ─────────────────────────────────────────────────────────
531
+ {
532
+ name: "FormField",
533
+ group: "data-entry",
534
+ tagline: "Wraps a control with label, helper, and error; injects aria-describedby/aria-invalid onto the child.",
535
+ props: [
536
+ { name: "id", type: "string", required: true, description: "Forwarded to Label htmlFor + builds helper/error ids." },
537
+ { name: "label", type: "ReactNode", required: true, description: "Field label above the control." },
538
+ { name: "required", type: "boolean", defaultValue: "false", description: "Red asterisk + aria-required on the child." },
539
+ { name: "helper", type: "string", description: "Muted hint shown when there is no error." },
540
+ { name: "error", type: "string", description: "Destructive error message (role=alert); overrides helper." },
541
+ { name: "children", type: "ReactNode", required: true, description: "The single control to render." }
542
+ ],
543
+ example: `import { FormField, Input } from "@godxjp/ui/data-entry";
544
+
545
+ <FormField id="coupon-name" label="\u30AF\u30FC\u30DD\u30F3\u540D" required error={errors.name} helper="\u6700\u592750\u6587\u5B57">
546
+ <Input id="coupon-name" placeholder="\u6625\u306E\u82B1\u7C89\u75C7\u5BFE\u7B5615%OFF" value={name} onChange={(e) => setName(e.target.value)} />
547
+ </FormField>`,
548
+ storyPath: "data-entry/FormField.stories.tsx",
549
+ rules: [23]
233
550
  },
234
- // ─── data-entry ─────────────────────────────────────────────────
235
551
  {
236
552
  name: "Input",
237
553
  group: "data-entry",
238
- tagline: "Text input. `prefix` / `suffix` slots, `status` for validation, `addonBefore` / `addonAfter` for connected groups.",
554
+ tagline: "Styled wrapper around native <input>; accepts all HTML input attributes. Pair with FormField for labelled fields.",
239
555
  props: [
240
- { name: "value / onChange", type: "string / (e) => void", description: "Controlled value (or use `defaultValue`)." },
241
- { name: "size", type: "InputSize (= SizeProp)", description: "small | default | large." },
242
- { name: "status", type: "InputStatus (= StatusProp)", description: "default | success | warning | error. Use `error` for form validation." },
243
- { name: "prefix / suffix", type: "ReactNode", description: "Icon slot inside the input box." },
244
- { name: "addonBefore / addonAfter", type: "ReactNode", description: "Connected group (e.g. URL prefix, currency)." }
556
+ { name: "id", type: "string", description: "Associates with a <label htmlFor>." },
557
+ { name: "type", type: "string", defaultValue: '"text"', description: "Native input type." },
558
+ { name: "placeholder", type: "string", description: "Placeholder." },
559
+ { name: "value", type: "string | number", description: "Controlled value." },
560
+ { name: "onChange", type: "React.ChangeEventHandler<HTMLInputElement>", description: "Native change handler." }
245
561
  ],
246
- example: `<Input
247
- placeholder="https://"
248
- prefix={<Globe size={14} />}
249
- addonBefore="https://"
250
- addonAfter=".com"
251
- />`,
252
- docPath: "data-entry/Input.md",
562
+ example: `import { Input } from "@godxjp/ui/data-entry";
563
+
564
+ <Input id="qty" type="number" placeholder="\u4F8B: 500" value={value} onChange={(e) => setValue(e.target.value)} />`,
253
565
  storyPath: "data-entry/Input.stories.tsx",
254
- rules: [14, 23]
566
+ rules: []
255
567
  },
256
568
  {
257
- name: "InputNumber",
569
+ name: "SearchInput",
258
570
  group: "data-entry",
259
- tagline: "Numeric input with stepper. `min` / `max` / `step`, `precision`, `formatter` / `parser` for currency.",
571
+ tagline: "Debounced search box with a clear button. Fires onSearch (NOT onChange) after the debounce. Controlled (value) or uncontrolled (defaultValue).",
260
572
  props: [
261
- { name: "value / onValueChange", type: "number | null / (n) => void", description: "Radix-style controlled state." },
262
- { name: "min / max / step", type: "number", description: "Numeric bounds + increment." },
263
- { name: "precision", type: "number", description: "Decimal places." },
264
- { name: "formatter / parser", type: "(value) => string / string", description: "Custom display (currency, units)." }
573
+ { name: "onSearch", type: "(q: string) => void", required: true, description: "Called with the query after the debounce. Use this to drive filtering \u2014 NOT onChange." },
574
+ { name: "value", type: "string", description: "Controlled value." },
575
+ { name: "defaultValue", type: "string", defaultValue: '""', description: "Initial uncontrolled value." },
576
+ { name: "placeholder", type: "string", description: "Input placeholder." },
577
+ { name: "debounce", type: "number", defaultValue: "250", description: "Debounce delay (ms)." }
265
578
  ],
266
- example: `<InputNumber
267
- defaultValue={1000}
268
- min={0} max={10000} step={100}
269
- formatter={(v) => \`\xA5\${v.toLocaleString()}\`}
270
- parser={(v) => Number(v.replace(/[^0-9]/g, ""))}
271
- />`,
272
- storyPath: "data-entry/InputNumber.stories.tsx",
579
+ example: `import { SearchInput } from "@godxjp/ui/data-entry";
580
+
581
+ <SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u30FBID\u3067\u691C\u7D22" value={search} onSearch={setSearch} />`,
582
+ storyPath: "data-entry/SearchInput.stories.tsx",
273
583
  rules: [23]
274
584
  },
275
585
  {
276
586
  name: "Select",
277
587
  group: "data-entry",
278
- tagline: "Single-select dropdown (Radix). `options` array. `searchable` for cmdk filter.",
588
+ tagline: "Radix compound select. Controlled via value + onValueChange on <Select>; compose SelectTrigger>SelectValue and SelectContent>SelectItem. This is the filter/select pattern the app uses.",
279
589
  props: [
280
- { name: "options", type: "Array<{ value, label, disabled? }> | OptionGroup[]", required: true, description: "Option list, optionally grouped." },
281
- { name: "value / onValueChange", type: "string / (v) => void", description: "Controlled selection." },
282
- { name: "size", type: "InputSize", description: "small | default | large." },
283
- { name: "searchable", type: "boolean", description: "cmdk-powered keyboard search." },
284
- { name: "loading", type: "boolean", description: "Show loading row (searchable mode)." }
590
+ { name: "value", type: "string", description: "Controlled selected value." },
591
+ { name: "defaultValue", type: "string", description: "Uncontrolled initial value." },
592
+ { name: "onValueChange", type: "(value: string) => void", description: "Called when the user picks an item." },
593
+ { name: "disabled", type: "boolean", defaultValue: "false", description: "Disable the trigger." }
285
594
  ],
286
- example: `<Select
287
- options={[
288
- { value: "ja", label: "\u65E5\u672C\u8A9E" },
289
- { value: "en-US", label: "English" },
290
- ]}
291
- value={locale}
292
- onValueChange={setLocale}
293
- />`,
595
+ example: `import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@godxjp/ui/data-entry";
596
+
597
+ <Select value={status} onValueChange={setStatus}>
598
+ <SelectTrigger><SelectValue placeholder="\u5168\u30B9\u30C6\u30FC\u30BF\u30B9" /></SelectTrigger>
599
+ <SelectContent>
600
+ <SelectItem value="all">\u5168\u30B9\u30C6\u30FC\u30BF\u30B9</SelectItem>
601
+ <SelectItem value="active">\u516C\u958B\u4E2D</SelectItem>
602
+ <SelectItem value="draft">\u4E0B\u66F8\u304D</SelectItem>
603
+ </SelectContent>
604
+ </Select>`,
294
605
  storyPath: "data-entry/Select.stories.tsx",
295
- rules: [3, 23]
606
+ rules: [23]
296
607
  },
297
608
  {
298
- name: "Form",
609
+ name: "Switch",
299
610
  group: "data-entry",
300
- tagline: "react-hook-form + zod wrapper. `defaultValues` + `resolver` + `onSubmit` \u2014 children = inline `<FormField>` per row.",
611
+ tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use SwitchField.",
301
612
  props: [
302
- { name: "form", type: "UseFormReturn<T>", description: "Pass `useForm()` return for external control." },
303
- { name: "defaultValues / resolver / mode", type: "configs", description: "Shorthand for internal useForm." },
304
- { name: "onSubmit", type: "(values: T) => void | Promise<void>", description: "Submit handler (handleSubmit-wrapped)." },
305
- { name: "layout", type: "FormLayout", description: "vertical (default) | horizontal | inline. Mobile-first." },
306
- { name: "loading", type: "LoadingProp", description: "Cascade loading to every FormField \u2014 true=spinner, {kind:'skeleton'}=skeleton." }
613
+ { name: "checked", type: "boolean", description: "Controlled checked state." },
614
+ { name: "onCheckedChange", type: "(checked: boolean) => void", description: "Fires when toggled." },
615
+ { name: "id", type: "string", description: "Links to a <Label htmlFor>." },
616
+ { name: "disabled", type: "boolean", defaultValue: "false", description: "Disable the toggle." }
307
617
  ],
308
- example: `<Form<RegistrationValues>
309
- resolver={zodResolver(registrationSchema)}
310
- defaultValues={{ name: "", email: "", age: 18, agree: true }}
311
- onSubmit={(values) => api.register(values)}
312
- >
313
- <FormField name="name" label="\u6C0F\u540D" required>
314
- <Input placeholder="\u5C71\u7530 \u592A\u90CE" />
315
- </FormField>
316
- <FormField name="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required>
317
- <Input type="email" />
318
- </FormField>
319
- <Button type="submit" variant="primary">\u767B\u9332</Button>
320
- </Form>`,
321
- storyPath: "data-entry/Form.stories.tsx",
322
- rules: [3, 23, 34]
618
+ example: `import { Switch, Label } from "@godxjp/ui/data-entry";
619
+
620
+ <div className="flex items-center gap-2">
621
+ <Switch id="stackable" checked={stackable} onCheckedChange={setStackable} />
622
+ <Label htmlFor="stackable">\u4ED6\u30AF\u30FC\u30DD\u30F3\u3068\u306E\u4F75\u7528\u3092\u8A31\u53EF</Label>
623
+ </div>`,
624
+ storyPath: "data-entry/Switch.stories.tsx",
625
+ rules: []
323
626
  },
324
627
  {
325
- name: "FormField",
628
+ name: "Textarea",
326
629
  group: "data-entry",
327
- tagline: "Form-wired Field. Reads value/onChange/error from <Form> via useController, clones child with the right adapter (Input/Checkbox/Switch/Select/Slider/Cascader/TreeSelect/Transfer/etc.).",
630
+ tagline: "Styled wrapper around native <textarea>. Pair with FormField for labelled fields.",
328
631
  props: [
329
- { name: "name", type: "FieldPath<T>", required: true, description: "Schema field path." },
330
- { name: "label / description / required / optional", type: "Field-style props", description: "Label + help + status badges." },
331
- { name: "loading", type: "LoadingProp", description: "Per-field override of Form-level loading." },
332
- { name: "children", type: "ReactElement", required: true, description: "Single data-entry primitive (Input/Select/Checkbox/...)." }
632
+ { name: "id", type: "string", description: "Associates with a <Label htmlFor>." },
633
+ { name: "rows", type: "number", description: "Visible text rows." },
634
+ { name: "value", type: "string", description: "Controlled value." },
635
+ { name: "onChange", type: "React.ChangeEventHandler<HTMLTextAreaElement>", description: "Change handler." }
333
636
  ],
334
- example: `<FormField name="phone" label="\u96FB\u8A71\u756A\u53F7" required description="\u30CF\u30A4\u30D5\u30F3\u7121\u3057\u3067\u5165\u529B">
335
- <Input placeholder="08012345678" />
336
- </FormField>`,
337
- storyPath: "data-entry/Form.stories.tsx",
338
- rules: [23, 34]
637
+ example: `import { Textarea } from "@godxjp/ui/data-entry";
638
+
639
+ <Textarea id="notes" rows={4} placeholder="\u81EA\u7531\u8A18\u8FF0" value={notes} onChange={(e) => setNotes(e.target.value)} />`,
640
+ storyPath: "data-entry/Textarea.stories.tsx",
641
+ rules: []
339
642
  },
340
643
  {
341
- name: "Checkbox",
644
+ name: "Label",
342
645
  group: "data-entry",
343
- tagline: "Boolean toggle (Radix). Label as children. Use `CheckboxGroup` for multi-select.",
646
+ tagline: "Styled Radix Label; use htmlFor to associate with a control.",
344
647
  props: [
345
- { name: "checked / onCheckedChange", type: "boolean / (next) => void", description: "Radix-style." },
346
- { name: "children", type: "ReactNode", description: "Label text (clickable area extends to it)." }
648
+ { name: "htmlFor", type: "string", description: "Id of the associated control." },
649
+ { name: "children", type: "ReactNode", description: "Label content." }
347
650
  ],
348
- example: `<Checkbox checked={agree} onCheckedChange={setAgree}>
349
- \u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B
350
- </Checkbox>`,
351
- storyPath: "data-entry/Checkbox.stories.tsx",
352
- rules: [3, 23]
651
+ example: `import { Label } from "@godxjp/ui/data-entry";
652
+
653
+ <Label htmlFor="stackable">\u4F75\u7528\u3092\u8A31\u53EF</Label>`,
654
+ storyPath: "data-entry/Label.stories.tsx",
655
+ rules: []
353
656
  },
354
657
  {
355
- name: "Switch",
658
+ name: "Checkbox",
356
659
  group: "data-entry",
357
- tagline: "Boolean toggle, slider style (Radix). Use in settings / feature flags.",
660
+ tagline: "Radix checkbox; standalone or via CheckboxGroup with an options array.",
358
661
  props: [
359
- { name: "checked / onCheckedChange", type: "boolean / (next) => void", description: "Radix-style." },
360
- { name: "disabled", type: "boolean", description: "Lock state." }
662
+ { name: "checked", type: "boolean | 'indeterminate'", description: "Controlled checked state." },
663
+ { name: "onCheckedChange", type: "(checked) => void", description: "Fires when checked state changes." },
664
+ { name: "id", type: "string", description: "Links to a <Label htmlFor>." }
361
665
  ],
362
- example: `<Switch checked={notifEnabled} onCheckedChange={setNotifEnabled} />`,
363
- storyPath: "data-entry/Switch.stories.tsx",
364
- rules: [3, 23]
666
+ example: `import { Checkbox, Label } from "@godxjp/ui/data-entry";
667
+
668
+ <div className="flex items-center gap-2">
669
+ <Checkbox id="agree" checked={agreed} onCheckedChange={(v) => setAgreed(!!v)} />
670
+ <Label htmlFor="agree">\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Label>
671
+ </div>`,
672
+ storyPath: "data-entry/Checkbox.stories.tsx",
673
+ rules: []
365
674
  },
366
675
  {
367
- name: "DatePicker / DateField / DateRangePicker / TimeField",
676
+ name: "RadioGroup",
368
677
  group: "data-entry",
369
- tagline: "React Aria-backed date/time inputs. Locale-aware (via GodxConfigProvider), keyboard-complete, IME-friendly.",
678
+ tagline: "Radio group accepting an options array or RadioItem children.",
370
679
  props: [
371
- { name: "value / onChange", type: "DateValue | TimeValue", description: "@internationalized/date values." },
372
- { name: "label / description", type: "ReactNode", description: "Inline label." },
373
- { name: "minValue / maxValue", type: "DateValue", description: "Bounds." }
680
+ { name: "value", type: "string", description: "Controlled selected value." },
681
+ { name: "onValueChange", type: "(value: string) => void", description: "Fires on selection change." },
682
+ { name: "options", type: "ChoiceOptionProp[]", description: "Declarative list: { label, value, disabled?, description? }." },
683
+ { name: "orientation", type: '"horizontal" | "vertical"', defaultValue: '"vertical"', description: "Layout direction." }
374
684
  ],
375
- example: `<DateField label="\u751F\u5E74\u6708\u65E5" defaultValue={parseDate("2000-01-01")} />
376
- <TimeField label="\u51FA\u52E4\u6642\u523B" defaultValue={new Time(9, 0)} />`,
377
- storyPath: "data-entry/DatePicker.stories.tsx",
378
- rules: [3, 14, 23]
685
+ example: `import { RadioGroup } from "@godxjp/ui/data-entry";
686
+
687
+ <RadioGroup value={trigger} onValueChange={setTrigger} orientation="horizontal" options={[
688
+ { label: "\u521D\u56DE\u8CFC\u5165", value: "first_purchase" },
689
+ { label: "\u8A95\u751F\u65E5", value: "birthday" },
690
+ ]} />`,
691
+ storyPath: "data-entry/RadioGroup.stories.tsx",
692
+ rules: [23]
379
693
  },
380
- // ─── feedback ───────────────────────────────────────────────────
381
694
  {
382
- name: "Alert",
383
- group: "feedback",
384
- tagline: "Inline notice banner. 5 semantic colors \xD7 outlined/banner variants.",
695
+ name: "DatePicker",
696
+ group: "data-entry",
697
+ tagline: "Calendar popover for a single date; controlled via value (Date) + onChange.",
385
698
  props: [
386
- { name: "color", type: "FeedbackColorProp", description: "default | info | success | warning | destructive." },
387
- { name: "variant", type: '"outlined" | "banner"', description: "Card style or full-width banner." },
388
- { name: "title / description", type: "ReactNode", description: "Two-line stack." },
389
- { name: "actions", type: "ReactNode", description: "Right-aligned action slot." },
390
- { name: "closable / onClose", type: "boolean / fn", description: "\xD7 button." }
699
+ { name: "value", type: "Date", description: "Controlled selected date." },
700
+ { name: "onChange", type: "(date: Date | undefined) => void", description: "Fires when picked/cleared." },
701
+ { name: "placeholder", type: "string", description: "Trigger label when empty." }
391
702
  ],
392
- example: `<Alert color="warning" title="3 \u4EF6\u306E\u6253\u523B\u6F0F\u308C\u304C\u3042\u308A\u307E\u3059"
393
- description="\u672C\u65E5\u4E2D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
394
- actions={<Button size="small">\u78BA\u8A8D\u3059\u308B</Button>}
395
- closable onClose={dismiss}
396
- />`,
397
- storyPath: "feedback/Alert.stories.tsx",
398
- rules: [23]
703
+ example: `import { DatePicker, FormField } from "@godxjp/ui/data-entry";
704
+
705
+ <FormField id="valid-from" label="\u6709\u52B9\u958B\u59CB\u65E5">
706
+ <DatePicker id="valid-from" value={validFrom} onChange={setValidFrom} placeholder="\u65E5\u4ED8\u3092\u9078\u629E" />
707
+ </FormField>`,
708
+ storyPath: "data-entry/DatePicker.stories.tsx",
709
+ rules: []
399
710
  },
711
+ // ─── feedback ───────────────────────────────────────────────────────────
400
712
  {
401
- name: "Dialog / Modal",
713
+ name: "Dialog",
402
714
  group: "feedback",
403
- tagline: "Radix Dialog wrapped with title/footer slots. `Modal` is convenience preset.",
715
+ tagline: 'Compound modal. Controlled via open + onOpenChange. Parts available flat (DialogTrigger/DialogContent/\u2026) or as Dialog.Trigger/Dialog.Content. mode="confirm" switches to alertdialog.',
404
716
  props: [
405
- { name: "open / onOpenChange", type: "boolean / (b) => void", description: "Radix-style." },
406
- { name: "title / description / footer", type: "ReactNode", description: "Header / body / footer slots." },
407
- { name: "size", type: "SizeProp", description: "Width preset." }
717
+ { name: "open", type: "boolean", description: "Controlled open state." },
718
+ { name: "onOpenChange", type: "(open: boolean) => void", description: "Open-state change handler." },
719
+ { name: "mode", type: '"form" | "confirm"', defaultValue: '"form"', description: "form = Radix Dialog (\xD7 close); confirm = AlertDialog (no \xD7)." }
408
720
  ],
409
- example: `<Dialog open={open} onOpenChange={setOpen}
410
- title="\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664"
411
- footer={
412
- <>
413
- <Button variant="ghost" onClick={() => setOpen(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
414
- <Button variant="destructive" onClick={confirmDelete}>\u524A\u9664</Button>
415
- </>
416
- }
417
- >
418
- \u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002
419
- </Dialog>`,
721
+ example: `import { useState } from "react";
722
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
723
+ import { Button } from "@godxjp/ui/general";
724
+
725
+ function CreateDialog() {
726
+ const [open, setOpen] = useState(false);
727
+ return (
728
+ <Dialog open={open} onOpenChange={setOpen}>
729
+ <DialogTrigger asChild><Button size="sm">\u65B0\u898F\u4F5C\u6210</Button></DialogTrigger>
730
+ <DialogContent className="max-w-lg">
731
+ <DialogHeader>
732
+ <DialogTitle>\u65B0\u898F\u30AF\u30FC\u30DD\u30F3\u4F5C\u6210</DialogTitle>
733
+ <DialogDescription>\u30AF\u30FC\u30DD\u30F3\u60C5\u5831\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002</DialogDescription>
734
+ </DialogHeader>
735
+ {/* fields */}
736
+ <DialogFooter>
737
+ <Button variant="outline" onClick={() => setOpen(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
738
+ <Button onClick={() => setOpen(false)}>\u4FDD\u5B58</Button>
739
+ </DialogFooter>
740
+ </DialogContent>
741
+ </Dialog>
742
+ );
743
+ }`,
420
744
  storyPath: "feedback/Dialog.stories.tsx",
421
- rules: [3, 23]
745
+ rules: [23, 3]
422
746
  },
423
747
  {
424
- name: "Sheet / Drawer",
748
+ name: "Sheet",
425
749
  group: "feedback",
426
- tagline: "Side panel (Radix Dialog). 4 sides. Wider than Modal \u2014 for settings, filters, details.",
750
+ tagline: "Side-panel drawer (Radix Dialog). Parts: Sheet/SheetTrigger/SheetContent(side=right|left|top|bottom)/SheetHeader/SheetTitle/SheetFooter.",
427
751
  props: [
428
- { name: "open / onOpenChange", type: "boolean / fn", description: "Radix-style." },
429
- { name: "side", type: "SideProp", description: "top | right | bottom | left." },
430
- { name: "title / description", type: "ReactNode", description: "Header." }
752
+ { name: "open", type: "boolean", description: "Controlled open state." },
753
+ { name: "onOpenChange", type: "(open: boolean) => void", description: "Open-state change handler." }
431
754
  ],
432
- example: `<Sheet open={open} onOpenChange={setOpen} side="right" title="\u30D5\u30A3\u30EB\u30BF\u30FC">
433
- {/* filter form */}
755
+ example: `import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from "@godxjp/ui/feedback";
756
+ import { Button } from "@godxjp/ui/general";
757
+
758
+ <Sheet open={open} onOpenChange={setOpen}>
759
+ <SheetTrigger asChild><Button variant="outline" size="sm">\u7D5E\u308A\u8FBC\u307F</Button></SheetTrigger>
760
+ <SheetContent side="right">
761
+ <SheetHeader><SheetTitle>\u30D5\u30A3\u30EB\u30BF\u30FC\u8A2D\u5B9A</SheetTitle></SheetHeader>
762
+ {/* filter fields */}
763
+ </SheetContent>
434
764
  </Sheet>`,
435
- storyPath: "feedback/Drawer.stories.tsx",
436
- rules: [3, 23]
765
+ storyPath: "feedback/Sheet.stories.tsx",
766
+ rules: [3]
437
767
  },
438
768
  {
439
- name: "Toaster / toast",
769
+ name: "Alert",
440
770
  group: "feedback",
441
- tagline: "Sonner-backed toast notifications. Mount Toaster once in app root, call `toast.success(...)` anywhere.",
771
+ tagline: "Inline alert banner with variant-aware icon + optional dismiss. Parts: Alert/AlertTitle/AlertDescription/AlertActions/AlertQueryError.",
442
772
  props: [
443
- { name: "position", type: '"top-right" | "top-center" | "bottom-right" | ...', description: "Stack anchor." }
773
+ { name: "variant", type: '"default" | "destructive" | "warning" | "success"', defaultValue: '"default"', description: "Colour scheme + default icon." },
774
+ { name: "onDismiss", type: "() => void", description: "Renders an \xD7 dismiss button when provided." },
775
+ { name: "icon", type: "LucideIcon | false", description: "Override or hide (false) the icon." }
444
776
  ],
445
- example: `// App root
446
- <GodxConfigProvider><App /><Toaster position="top-right" /></GodxConfigProvider>
777
+ example: `import { Alert, AlertTitle, AlertDescription } from "@godxjp/ui/feedback";
447
778
 
448
- // Anywhere
449
- toast.success("\u4FDD\u5B58\u3057\u307E\u3057\u305F")
450
- toast.error("\u901A\u4FE1\u30A8\u30E9\u30FC", { description: "\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044" })`,
451
- storyPath: "providers/Toaster.stories.tsx",
452
- rules: [14, 23]
779
+ <Alert variant="warning">
780
+ <AlertTitle>3 \u4EF6\u306E\u6253\u523B\u6F0F\u308C\u304C\u3042\u308A\u307E\u3059</AlertTitle>
781
+ <AlertDescription>\u672C\u65E5\u4E2D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002</AlertDescription>
782
+ </Alert>`,
783
+ storyPath: "feedback/Alert.stories.tsx",
784
+ rules: []
453
785
  },
454
786
  {
455
- name: "Skeleton",
787
+ name: "SkeletonTable",
456
788
  group: "feedback",
457
- tagline: "Loading placeholder block. Use during initial data fetch (NOT during save \u2014 that's a spinner).",
789
+ tagline: "Loading placeholder matching the DataTable layout (header + N rows). Drop-in while data loads (deferred props).",
458
790
  props: [
459
- { name: "className", type: "string", description: "Set width/height via Tailwind (h-9, w-full, rounded-md)." }
791
+ { name: "rows", type: "number", defaultValue: "8", description: "Body skeleton rows." },
792
+ { name: "columns", type: "number", defaultValue: "5", description: "Columns in header + body." }
460
793
  ],
461
- example: `<Skeleton className="h-9 w-full rounded-md" />
462
- <Skeleton className="h-4 w-32 rounded-sm" />`,
794
+ example: `import { SkeletonTable } from "@godxjp/ui/feedback";
795
+
796
+ {!coupons ? <SkeletonTable rows={10} columns={6} /> : <DataTable data={coupons} columns={columns} />}`,
463
797
  storyPath: "feedback/Skeleton.stories.tsx",
464
- rules: [23]
798
+ rules: []
465
799
  },
466
800
  {
467
- name: "Spinner",
801
+ name: "SkeletonCard",
468
802
  group: "feedback",
469
- tagline: "Small inline circular loader. Use for active work (saving, revalidating).",
803
+ tagline: "Loading placeholder shaped like a CardStat tile. Use inside a ResponsiveGrid while KPIs load.",
804
+ props: [],
805
+ example: `import { SkeletonCard } from "@godxjp/ui/feedback";
806
+ import { ResponsiveGrid } from "@godxjp/ui/layout";
807
+
808
+ <ResponsiveGrid columns={4}><SkeletonCard /><SkeletonCard /><SkeletonCard /><SkeletonCard /></ResponsiveGrid>`,
809
+ storyPath: "feedback/Skeleton.stories.tsx",
810
+ rules: []
811
+ },
812
+ {
813
+ name: "Toaster",
814
+ group: "feedback",
815
+ tagline: 'Mount once at app root to enable toasts. IMPORTANT: trigger toasts via `import { toast } from "sonner"` \u2014 NOT from @godxjp/ui.',
470
816
  props: [
471
- { name: "size", type: "IconSizeProp", description: "sm | md | lg." },
472
- { name: "tone", type: "SpinnerTone", description: "Semantic color." }
817
+ { name: "position", type: '"top-right" | "top-center" | "bottom-right" | "\u2026"', defaultValue: '"bottom-right"', description: "Toast stack anchor." },
818
+ { name: "richColors", type: "boolean", description: "Enable Sonner rich variant colours." }
473
819
  ],
474
- example: `<Spinner size="sm" aria-label="\u8AAD\u307F\u8FBC\u307F\u4E2D" />`,
475
- storyPath: "feedback/Spin.stories.tsx",
476
- rules: [23]
820
+ example: `// app root \u2014 mount once
821
+ import { Toaster } from "@godxjp/ui/feedback";
822
+ <>{children}<Toaster richColors /></>
823
+
824
+ // anywhere \u2014 import toast from "sonner"
825
+ import { toast } from "sonner";
826
+ toast.success("\u30AF\u30FC\u30DD\u30F3\u3092\u516C\u958B\u3057\u307E\u3057\u305F");
827
+ toast.error("\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F");`,
828
+ storyPath: "feedback/Toaster.stories.tsx",
829
+ rules: []
477
830
  },
478
- // ─── navigation ─────────────────────────────────────────────────
831
+ // ─── navigation ─────────────────────────────────────────────────────────
479
832
  {
480
833
  name: "Tabs",
481
834
  group: "navigation",
482
- tagline: "Radix Tabs. Line or pills variant, horizontal/vertical, lazy mount.",
835
+ tagline: "Radix tab container. Compose Tabs/TabsList/TabsTrigger/TabsContent. Controlled (value/onValueChange) or uncontrolled (defaultValue).",
483
836
  props: [
484
- { name: "items", type: "Array<{ value, label, content, disabled? }>", required: true, description: "Tab list." },
485
- { name: "value / onValueChange", type: "string / fn", description: "Controlled active tab." },
486
- { name: "variant", type: '"line" | "pills"', description: "Visual." },
487
- { name: "orientation", type: "OrientationProp", description: "horizontal | vertical." }
837
+ { name: "value", type: "string", description: "Controlled active tab key." },
838
+ { name: "defaultValue", type: "string", description: "Uncontrolled initial tab key." },
839
+ { name: "onValueChange", type: "(value: string) => void", description: "Active-tab change handler." }
488
840
  ],
489
- example: `<Tabs items={[
490
- { value: "profile", label: "\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB", content: <ProfileForm /> },
491
- { value: "security", label: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3", content: <SecurityForm /> },
492
- ]} />`,
841
+ example: `import { Tabs, TabsList, TabsTrigger, TabsContent } from "@godxjp/ui/navigation";
842
+
843
+ <Tabs defaultValue="overview">
844
+ <TabsList>
845
+ <TabsTrigger value="overview">\u6982\u8981</TabsTrigger>
846
+ <TabsTrigger value="history">\u5C65\u6B74</TabsTrigger>
847
+ </TabsList>
848
+ <TabsContent value="overview">\u6982\u8981\u30B3\u30F3\u30C6\u30F3\u30C4</TabsContent>
849
+ <TabsContent value="history">\u5C65\u6B74\u30B3\u30F3\u30C6\u30F3\u30C4</TabsContent>
850
+ </Tabs>`,
493
851
  storyPath: "navigation/Tabs.stories.tsx",
494
- rules: [3, 23]
852
+ rules: []
495
853
  },
496
854
  {
497
- name: "Menu / DropdownMenu",
855
+ name: "FilterBar",
498
856
  group: "navigation",
499
- tagline: "Sidebar menu (sections + items + nested) / floating context menu (Radix DropdownMenu).",
857
+ tagline: "Standard list-page filter strip. Place ABOVE the table Card \u2014 NEVER inside CardContent flush (it strips padding). Compose with FilterGroup + SearchInput + Select.",
500
858
  props: [
501
- { name: "items", type: "MenuItem[]", description: "Sections + nested children." },
502
- { name: "activeId / onSelect", type: "string / fn", description: "Controlled selection." }
859
+ { name: "children", type: "ReactNode", required: true, description: "Filter controls + FilterGroup wrappers." },
860
+ { name: "hasActiveFilters", type: "boolean", description: "Shows a clear-all button when true." },
861
+ { name: "onClear", type: "() => void", description: "Clear-all handler." }
503
862
  ],
504
- example: `<Menu activeId={active} onSelect={setActive} items={[
505
- { id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
506
- { id: "settings", label: "Settings", icon: Settings, children: [
507
- { id: "team", label: "Team" },
508
- { id: "billing", label: "Billing" },
509
- ]},
510
- ]} />`,
511
- storyPath: "navigation/Menu.stories.tsx",
512
- rules: [3, 14, 23]
863
+ example: `import { FilterBar, FilterGroup } from "@godxjp/ui/navigation";
864
+ import { SearchInput, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@godxjp/ui/data-entry";
865
+
866
+ <FilterBar hasActiveFilters={search !== ""} onClear={() => setSearch("")}>
867
+ <SearchInput placeholder="\u540D\u524D\u3067\u691C\u7D22" value={search} onSearch={setSearch} />
868
+ <FilterGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
869
+ <Select value={status} onValueChange={setStatus}>
870
+ <SelectTrigger><SelectValue /></SelectTrigger>
871
+ <SelectContent>
872
+ <SelectItem value="all">\u3059\u3079\u3066</SelectItem>
873
+ <SelectItem value="active">\u6709\u52B9</SelectItem>
874
+ </SelectContent>
875
+ </Select>
876
+ </FilterGroup>
877
+ </FilterBar>`,
878
+ storyPath: "navigation/FilterBar.stories.tsx",
879
+ rules: [38, 40]
880
+ },
881
+ {
882
+ name: "FilterGroup",
883
+ group: "navigation",
884
+ tagline: "Labelled filter slot inside FilterBar \u2014 wraps a single Select/DatePicker.",
885
+ props: [
886
+ { name: "label", type: "ReactNode", required: true, description: "Label shown with the child control." },
887
+ { name: "children", type: "ReactNode", required: true, description: "The filter control." }
888
+ ],
889
+ example: `import { FilterGroup } from "@godxjp/ui/navigation";
890
+
891
+ <FilterGroup label="\u30B9\u30B3\u30FC\u30D7"><Select>{/* ... */}</Select></FilterGroup>`,
892
+ storyPath: "navigation/FilterBar.stories.tsx",
893
+ rules: [38]
513
894
  },
514
895
  {
515
896
  name: "Pagination",
516
897
  group: "navigation",
517
- tagline: "Page navigator. 3 variants \u2014 default (numbered) / simple (prev/next) / embedded (within toolbar). Justify matches FlexJustify.",
898
+ tagline: "Offset/page-based pagination bar. Sits below a table card.",
518
899
  props: [
519
- { name: "current / total / pageSize", type: "number", description: "Pagination state." },
520
- { name: "onChange / onPageSizeChange", type: "fn", description: "Page or page size change." },
521
- { name: "variant", type: '"default" | "simple" | "embedded"', description: "Feature mode." },
522
- { name: "justify", type: "PaginationJustify", description: "start | center | end | space-between." },
523
- { name: "showSizeChanger / showTotal / showFirstLast", type: "boolean", description: "Adjective toggles." }
900
+ { name: "current", type: "number", defaultValue: "1", description: "Current page (1-indexed)." },
901
+ { name: "total", type: "number", description: "Total number of items." },
902
+ { name: "pageSize", type: "number", defaultValue: "10", description: "Items per page." },
903
+ { name: "showTotal", type: "boolean | (total, range) => ReactNode", description: "Show total count, or a custom label fn." },
904
+ { name: "onChange", type: "(page: number, pageSize: number) => void", description: "Page / page-size change handler." }
524
905
  ],
525
- example: `<Pagination current={page} pageSize={20} total={1234}
526
- onChange={(p) => setPage(p)} showSizeChanger />`,
906
+ example: `import { Pagination } from "@godxjp/ui/navigation";
907
+
908
+ <Pagination current={page} total={filtered.length} pageSize={10} showTotal onChange={(p) => setPage(p)} />`,
527
909
  storyPath: "navigation/Pagination.stories.tsx",
528
- rules: [23]
910
+ rules: [40]
529
911
  },
530
- // ─── composites ─────────────────────────────────────────────────
531
- {
532
- name: "DataTable",
533
- group: "composites",
534
- tagline: "Packaged Table. Pairs slim <Table> primitive with hook-based state slices (useTablePagination, useTableSelection, useTableViews, useTableState). Adds toolbar / view tabs / batch action band / filter chips / pagination / column manager Sheet / save-view Dialog.",
535
- props: [
536
- { name: "columns / data / rowKey", type: "(forwards to Table)", required: true, description: "" },
537
- { name: "toolbar", type: "TableToolbarConfig", description: "search / filter / columns toggle / primary action." },
538
- { name: "views", type: "TableViewsConfig", description: "Saved view tabs (active + dropdown)." },
539
- { name: "batchActions", type: "TableBatchActionsConfig<T>", description: "Action bar shown when rows selected." },
540
- { name: "pagination", type: "TablePaginationVariantConfig", description: "Numbered / load-more / cursor (kintai)." },
541
- { name: "tableKey", type: "string", description: "Persist state (sort, filters, columns) to localStorage." }
542
- ],
543
- example: `<DataTable
544
- tableKey="employees-v1"
545
- columns={EMPLOYEE_COLUMNS}
546
- data={employees}
547
- rowKey="id"
548
- toolbar={{ search: { value: q, onValueChange: setQ }, primaryAction: { label: "\u65B0\u898F\u8FFD\u52A0" } }}
549
- pagination={{ current: page, pageSize: 20, total: total, onChange: setPage }}
550
- batchActions={{
551
- selectedRowKeys: selected,
552
- onSelectedRowKeysChange: setSelected,
553
- actions: <Button variant="destructive">\u524A\u9664</Button>,
554
- }}
555
- />`,
556
- docPath: "composites/DataTable.md",
557
- storyPath: "composites/DataTable.stories.tsx",
558
- rules: [3, 22, 23, 31, 34]
559
- },
560
- // ─── shell ──────────────────────────────────────────────────────
561
912
  {
562
- name: "AppShell",
563
- group: "shell",
564
- tagline: "Root chrome layout. Grid: sidebar / topbar / main / footer. Slot-based \u2014 every service composes this.",
913
+ name: "DropdownMenu",
914
+ group: "navigation",
915
+ tagline: "Radix dropdown menu. Compose DropdownMenu/DropdownMenuTrigger/DropdownMenuContent/DropdownMenuItem/DropdownMenuSeparator.",
565
916
  props: [
566
- { name: "sidebar", type: "ReactNode", description: "<Sidebar> instance." },
567
- { name: "topbar", type: "ReactNode", description: "<Topbar> + switchers." },
568
- { name: "footer", type: "ReactNode", description: "Footer band." },
569
- { name: "sidebarCollapsed", type: "boolean", description: "Collapse sidebar (icon-only mode)." }
917
+ { name: "open", type: "boolean", description: "Controlled open state." },
918
+ { name: "onOpenChange", type: "(open: boolean) => void", description: "Open-state change handler." }
570
919
  ],
571
- example: `<AppShell
572
- sidebar={<Sidebar activeId={active} onSelect={setActive} sections={SECTIONS} />}
573
- topbar={<Topbar product={product} project={project} onSearchOpen={...} />}
574
- >
575
- <PageContent title="Dashboard">\u2026</PageContent>
576
- </AppShell>`,
577
- docPath: "shell/AppShell.md",
578
- storyPath: "shell/AppShell.stories.tsx",
579
- rules: [19, 22, 23, 28]
580
- },
581
- {
582
- name: "PageContent",
583
- group: "shell",
584
- tagline: "Main column inside AppShell. Title / subtitle / breadcrumb / extra slot + padded body.",
920
+ example: `import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@godxjp/ui/navigation";
921
+ import { Button } from "@godxjp/ui/general";
922
+
923
+ <DropdownMenu>
924
+ <DropdownMenuTrigger asChild><Button variant="outline" size="sm">\u64CD\u4F5C</Button></DropdownMenuTrigger>
925
+ <DropdownMenuContent>
926
+ <DropdownMenuItem>\u7DE8\u96C6</DropdownMenuItem>
927
+ <DropdownMenuSeparator />
928
+ <DropdownMenuItem variant="destructive">\u524A\u9664</DropdownMenuItem>
929
+ </DropdownMenuContent>
930
+ </DropdownMenu>`,
931
+ storyPath: "navigation/DropdownMenu.stories.tsx",
932
+ rules: []
933
+ },
934
+ {
935
+ name: "Steps",
936
+ group: "navigation",
937
+ tagline: "Multi-step progress indicator \u2014 horizontal or vertical, default or dot style.",
585
938
  props: [
586
- { name: "title / subtitle / breadcrumb / extra", type: "ReactNode", description: "Header slots." },
587
- { name: "padding", type: "PaddingProp", description: "tight | default | cozy | none." }
939
+ { name: "items", type: "StepItemProp[]", description: "Array of { title, subTitle?, description?, icon?, status? }." },
940
+ { name: "current", type: "number", defaultValue: "0", description: "Active step index (0-based)." },
941
+ { name: "orientation", type: '"horizontal" | "vertical"', defaultValue: '"horizontal"', description: "Layout direction." }
588
942
  ],
589
- example: `<PageContent
590
- title="Dashboard"
591
- subtitle="Workspace activity"
592
- extra={<Button variant="primary">New issue</Button>}
593
- >
594
- {/* page body */}
595
- </PageContent>`,
596
- storyPath: "shell/PageContent.stories.tsx",
597
- rules: [22, 23]
943
+ example: `import { Steps } from "@godxjp/ui/navigation";
944
+
945
+ <Steps current={1} items={[{ title: "\u7533\u8ACB" }, { title: "\u5BE9\u67FB\u4E2D" }, { title: "\u5B8C\u4E86" }]} />`,
946
+ storyPath: "navigation/Steps.stories.tsx",
947
+ rules: []
598
948
  },
949
+ // ─── providers / datetime ───────────────────────────────────────────────
599
950
  {
600
- name: "Sidebar",
601
- group: "shell",
602
- tagline: "Left nav rail. Sections + nested items + optional product chip + footer slot. Collapsible to icon-only.",
603
- props: [
604
- { name: "sections", type: "SidebarSection[]", required: true, description: "Section groups + items." },
605
- { name: "activeId / onSelect", type: "string / fn", description: "Controlled active." },
606
- { name: "collapsed", type: "boolean", description: "Icon-only mode." },
607
- { name: "product", type: "ProductDescriptor", description: "Top product chip (with ProductSwitcher trigger)." },
608
- { name: "brand", type: "ReactNode", description: "Override product with custom brand block." }
609
- ],
610
- example: `<Sidebar
611
- activeId={active}
612
- onSelect={setActive}
613
- sections={[
614
- { label: "Workspace", items: [{ id: "dash", label: "Dashboard", icon: LayoutDashboard }] },
615
- ]}
616
- product={ACTIVE_PRODUCT}
617
- />`,
618
- storyPath: "shell/Sidebar.stories.tsx",
619
- rules: [22, 23, 27]
620
- },
621
- {
622
- name: "CommandPalette",
623
- group: "shell",
624
- tagline: "\u2318K / Ctrl+K global command launcher (cmdk). Groups + items + keyboard shortcuts.",
951
+ name: "AppProvider",
952
+ group: "providers",
953
+ tagline: "Root locale/timezone/date-time context \u2014 wrap the app ONCE. All pickers + formatDate read from it. Import from @godxjp/ui/app.",
625
954
  props: [
626
- { name: "commands", type: "CommandItem[]", required: true, description: "Groupable command list." },
627
- { name: "open / onOpenChange", type: "boolean / fn", description: "Controlled (or use built-in \u2318K hotkey)." }
955
+ { name: "defaultLocale", type: '"ja" | "en" | "vi"', defaultValue: '"vi"', description: "Initial locale." },
956
+ { name: "defaultTimezone", type: 'string | "browser" | "system"', defaultValue: '"browser"', description: "Initial IANA timezone." },
957
+ { name: "defaultDateFormat", type: '"iso" | "dmy" | "mdy" | "locale"', defaultValue: '"locale"', description: "Initial date display format." },
958
+ { name: "defaultTimeFormat", type: '"24h" | "12h" | "locale"', defaultValue: '"locale"', description: "Initial clock format." }
628
959
  ],
629
- example: `<CommandPalette open={open} onOpenChange={setOpen} commands={[
630
- { id: "new-issue", label: "\u65B0\u898FIssue\u4F5C\u6210", shortcut: ["\u2318", "I"], onSelect: () => \u2026},
631
- ]} />`,
632
- storyPath: "shell/CommandPalette.stories.tsx",
633
- rules: [14, 23]
960
+ example: `import { AppProvider } from "@godxjp/ui/app";
961
+
962
+ <AppProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo" defaultDateFormat="iso" defaultTimeFormat="24h">
963
+ {children}
964
+ </AppProvider>`,
965
+ storyPath: "app/AppProvider.stories.tsx",
966
+ rules: [5]
634
967
  },
635
- // ─── providers ──────────────────────────────────────────────────
636
968
  {
637
- name: "GodxConfigProvider",
969
+ name: "formatDate",
638
970
  group: "providers",
639
- tagline: "Root provider \u2014 locale, timezone, currency. Carries them to React Aria's I18nProvider so DatePicker / TimeField / formatters all pick up the locale.",
971
+ tagline: "MANDATORY for all date/time display. Auto-detects ISO date / HH:mm / instant; reads AppProvider context. Import from @godxjp/ui/datetime.",
640
972
  props: [
641
- { name: "defaultLocale", type: "string", description: 'BCP 47 \u2014 "ja", "en-US", "vi", "fil".' },
642
- { name: "defaultTimezone", type: "string", description: 'IANA \u2014 "Asia/Tokyo", "America/New_York".' },
643
- { name: "defaultCurrency", type: "string", description: "ISO 4217 \u2014 JPY, USD." },
644
- { name: "storage", type: '"localStorage" | "cookie" | "both"', description: "Persist user override." }
973
+ { name: "value", type: "string | Date | null | undefined", required: true, description: "ISO date, ISO datetime, HH:mm, or Date." },
974
+ { name: "options.kind", type: '"auto" | "date" | "datetime" | "time" | "long" | "relative"', defaultValue: '"auto"', description: "Output preset; auto infers from the value." }
645
975
  ],
646
- example: `<GodxConfigProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo" defaultCurrency="JPY">
647
- <App />
648
- <Toaster position="top-right" />
649
- </GodxConfigProvider>`,
650
- storyPath: "providers/GodxConfigProvider.stories.tsx",
651
- rules: [4, 14]
976
+ example: `import { formatDate } from "@godxjp/ui/datetime";
977
+
978
+ formatDate(coupon.validFrom); // "2026-05-01"
979
+ formatDate(order.createdAt, { kind: "relative" }); // "3\u65E5\u524D"`,
980
+ storyPath: "app/formatDate.stories.tsx",
981
+ rules: [5]
652
982
  }
653
983
  ];
654
984
  function findComponent(name) {
@@ -905,388 +1235,153 @@ function findRule(num) {
905
1235
  // src/data/patterns.ts
906
1236
  var PATTERNS = [
907
1237
  {
908
- name: "registration-form",
909
- tagline: "Card-wrapped sign-up form with zod validation + action bar.",
910
- tags: ["form", "auth", "sign-up", "zod", "validation"],
911
- code: `import { z } from "zod"
912
- import { zodResolver } from "@hookform/resolvers/zod"
913
- import {
914
- Card, Form, FormField, Input, InputPassword, Checkbox,
915
- Button, Separator, Typography,
916
- } from "@godxjp/ui"
1238
+ name: "signup-form",
1239
+ tagline: "Card-wrapped sign-up form using react-hook-form + zod with FormField/Input and a CardFooter action bar (real @godxjp/ui API).",
1240
+ tags: ["form", "auth", "sign-up", "zod", "validation", "react-hook-form"],
1241
+ code: `import { useForm } from "react-hook-form";
1242
+ import { zodResolver } from "@hookform/resolvers/zod";
1243
+ import { z } from "zod";
1244
+ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@godxjp/ui/data-display";
1245
+ import { FormField, Input } from "@godxjp/ui/data-entry";
1246
+ import { Button } from "@godxjp/ui/general";
1247
+ import { Stack } from "@godxjp/ui/layout";
917
1248
 
918
1249
  const schema = z.object({
919
1250
  name: z.string().min(1, "\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059"),
920
1251
  email: z.string().email("\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044"),
921
- password: z.string()
922
- .min(8, "8 \u6587\u5B57\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044")
923
- .regex(/[A-Z]/, "\u5927\u6587\u5B57\u3092 1 \u3064\u4EE5\u4E0A\u542B\u3081\u3066\u304F\u3060\u3055\u3044")
924
- .regex(/\\d/, "\u6570\u5B57\u3092 1 \u3064\u4EE5\u4E0A\u542B\u3081\u3066\u304F\u3060\u3055\u3044"),
925
- agree: z.literal(true, { message: "\u5229\u7528\u898F\u7D04\u3078\u306E\u540C\u610F\u304C\u5FC5\u8981\u3067\u3059" }),
926
- })
927
- type SignUpValues = z.infer<typeof schema>
1252
+ });
1253
+ type Values = z.infer<typeof schema>;
928
1254
 
929
1255
  export function SignUpCard() {
1256
+ const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<Values>({ resolver: zodResolver(schema) });
1257
+ const onSubmit = handleSubmit(async (v) => {
1258
+ await fetch("/api/signup", { method: "POST", body: JSON.stringify(v) });
1259
+ });
930
1260
  return (
931
- <Card
932
- title="\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210"
933
- subtitle="30 \u79D2\u3067\u5B8C\u4E86\u3057\u307E\u3059\u3002"
934
- style={{ maxWidth: 420, margin: "0 auto" }}
935
- >
936
- <Form<SignUpValues>
937
- resolver={zodResolver(schema)}
938
- defaultValues={{ name: "", email: "", password: "", agree: true }}
939
- onSubmit={async (values) => {
940
- // POST to your API
941
- await fetch("/api/signup", { method: "POST", body: JSON.stringify(values) })
942
- }}
943
- >
944
- <FormField name="name" label="\u6C0F\u540D" required>
945
- <Input placeholder="\u5C71\u7530 \u592A\u90CE" />
946
- </FormField>
947
- <FormField name="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required>
948
- <Input type="email" placeholder="taro@example.com" />
949
- </FormField>
950
- <FormField name="password" label="\u30D1\u30B9\u30EF\u30FC\u30C9" required
951
- description="8 \u6587\u5B57\u4EE5\u4E0A / \u5927\u6587\u5B57 1 / \u6570\u5B57 1">
952
- <InputPassword placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" />
953
- </FormField>
954
- <FormField name="agree">
955
- <Checkbox>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>
956
- </FormField>
957
- <Button type="submit" variant="primary" block>
958
- \u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210
959
- </Button>
960
- </Form>
961
- <Separator style={{ margin: "var(--spacing-3) 0" }} />
962
- <Typography.Text color="secondary" style={{ textAlign: "center", display: "block" }}>
963
- \u65E2\u306B\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u304A\u6301\u3061\u3067\u3059\u304B? <a href="/login">\u30ED\u30B0\u30A4\u30F3</a>
964
- </Typography.Text>
1261
+ <Card>
1262
+ <CardHeader><CardTitle>\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210</CardTitle></CardHeader>
1263
+ <CardContent>
1264
+ <form id="signup" onSubmit={onSubmit}>
1265
+ <Stack gap="md">
1266
+ <FormField id="name" label="\u6C0F\u540D" required error={errors.name?.message}>
1267
+ <Input id="name" {...register("name")} />
1268
+ </FormField>
1269
+ <FormField id="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required error={errors.email?.message}>
1270
+ <Input id="email" type="email" {...register("email")} />
1271
+ </FormField>
1272
+ </Stack>
1273
+ </form>
1274
+ </CardContent>
1275
+ <CardFooter separated>
1276
+ <Button type="submit" form="signup" disabled={isSubmitting}>\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210</Button>
1277
+ </CardFooter>
965
1278
  </Card>
966
- )
1279
+ );
967
1280
  }`
968
1281
  },
969
1282
  {
970
- name: "settings-page",
971
- tagline: "Sectioned settings Card \u2014 \u57FA\u672C\u60C5\u5831 / \u516C\u958B\u7BC4\u56F2 / \u901A\u77E5, horizontal layout, action bar.",
972
- tags: ["settings", "form", "admin", "horizontal"],
973
- code: `import { z } from "zod"
974
- import { zodResolver } from "@hookform/resolvers/zod"
975
- import {
976
- Card, Form, FormField, Input, Select, Switch,
977
- Button, Separator, Typography, Flex,
978
- } from "@godxjp/ui"
979
-
980
- const schema = z.object({
981
- workspaceName: z.string().min(1),
982
- visibility: z.string(),
983
- notifyOnComment: z.boolean(),
984
- notifyOnMention: z.boolean(),
985
- digestFrequency: z.string(),
986
- })
987
- type SettingsValues = z.infer<typeof schema>
1283
+ name: "settings-tabs",
1284
+ tagline: "Sectioned settings inside a Card with Tabs + FormField + Select + Switch (real @godxjp/ui API).",
1285
+ tags: ["settings", "form", "tabs", "admin"],
1286
+ code: `import { Card, CardContent } from "@godxjp/ui/data-display";
1287
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@godxjp/ui/navigation";
1288
+ import { FormField, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Switch, Label } from "@godxjp/ui/data-entry";
1289
+ import { Stack } from "@godxjp/ui/layout";
988
1290
 
989
1291
  export function WorkspaceSettings() {
990
1292
  return (
991
- <Card title="\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u8A2D\u5B9A" subtitle="Acme Forge \xB7 /acme-forge"
992
- style={{ maxWidth: 820 }}>
993
- <Form<SettingsValues> layout="horizontal"
994
- resolver={zodResolver(schema)}
995
- defaultValues={{
996
- workspaceName: "Acme Forge",
997
- visibility: "internal",
998
- notifyOnComment: true,
999
- notifyOnMention: true,
1000
- digestFrequency: "weekly",
1001
- }}
1002
- onSubmit={(v) => save(v)}
1003
- >
1004
- <Typography.Title size={5}>\u57FA\u672C\u60C5\u5831</Typography.Title>
1005
- <FormField name="workspaceName" label="\u540D\u524D" required>
1006
- <Input />
1007
- </FormField>
1008
-
1009
- <Separator />
1010
- <Typography.Title size={5}>\u516C\u958B\u7BC4\u56F2</Typography.Title>
1011
- <FormField name="visibility" label="\u95B2\u89A7\u7BC4\u56F2" required>
1012
- <Select options={[
1013
- { value: "private", label: "\u30D7\u30E9\u30A4\u30D9\u30FC\u30C8" },
1014
- { value: "internal", label: "\u793E\u5185\u516C\u958B" },
1015
- { value: "public", label: "\u516C\u958B" },
1016
- ]} />
1017
- </FormField>
1018
-
1019
- <Separator />
1020
- <Typography.Title size={5}>\u901A\u77E5</Typography.Title>
1021
- <FormField name="notifyOnComment" label="\u30B3\u30E1\u30F3\u30C8"><Switch /></FormField>
1022
- <FormField name="notifyOnMention" label="\u30E1\u30F3\u30B7\u30E7\u30F3"><Switch /></FormField>
1023
- <FormField name="digestFrequency" label="\u30C0\u30A4\u30B8\u30A7\u30B9\u30C8">
1024
- <Select options={[
1025
- { value: "off", label: "\u9001\u4FE1\u3057\u306A\u3044" },
1026
- { value: "daily", label: "\u6BCE\u671D" },
1027
- { value: "weekly", label: "\u9031\u6B21" },
1028
- ]} />
1029
- </FormField>
1030
-
1031
- <Separator />
1032
- <Flex gap="small" justify="end">
1033
- <Button variant="ghost" type="button">\u5909\u66F4\u3092\u7834\u68C4</Button>
1034
- <Button type="submit" variant="primary">\u8A2D\u5B9A\u3092\u4FDD\u5B58</Button>
1035
- </Flex>
1036
- </Form>
1293
+ <Card>
1294
+ <CardContent>
1295
+ <Tabs defaultValue="general">
1296
+ <TabsList>
1297
+ <TabsTrigger value="general">\u57FA\u672C\u60C5\u5831</TabsTrigger>
1298
+ <TabsTrigger value="notify">\u901A\u77E5</TabsTrigger>
1299
+ </TabsList>
1300
+ <TabsContent value="general">
1301
+ <Stack gap="md">
1302
+ <FormField id="ws-name" label="\u540D\u524D" required><Input id="ws-name" /></FormField>
1303
+ <FormField id="visibility" label="\u516C\u958B\u7BC4\u56F2">
1304
+ <Select defaultValue="internal">
1305
+ <SelectTrigger><SelectValue /></SelectTrigger>
1306
+ <SelectContent>
1307
+ <SelectItem value="private">\u30D7\u30E9\u30A4\u30D9\u30FC\u30C8</SelectItem>
1308
+ <SelectItem value="internal">\u793E\u5185\u516C\u958B</SelectItem>
1309
+ <SelectItem value="public">\u516C\u958B</SelectItem>
1310
+ </SelectContent>
1311
+ </Select>
1312
+ </FormField>
1313
+ </Stack>
1314
+ </TabsContent>
1315
+ <TabsContent value="notify">
1316
+ <div className="flex items-center gap-2">
1317
+ <Switch id="notify-comment" defaultChecked />
1318
+ <Label htmlFor="notify-comment">\u30B3\u30E1\u30F3\u30C8\u901A\u77E5\u3092\u53D7\u3051\u53D6\u308B</Label>
1319
+ </div>
1320
+ </TabsContent>
1321
+ </Tabs>
1322
+ </CardContent>
1037
1323
  </Card>
1038
- )
1039
- }`
1040
- },
1041
- {
1042
- name: "data-table",
1043
- tagline: "DataTable composite with toolbar + pagination + batch actions + sticky columns.",
1044
- tags: ["table", "data", "pagination", "selection", "batch"],
1045
- code: `import { useState } from "react"
1046
- import {
1047
- DataTable, Badge, Avatar, Flex, Typography, Button,
1048
- type TableColumn,
1049
- } from "@godxjp/ui"
1050
-
1051
- interface Employee {
1052
- id: string
1053
- name: string
1054
- role: string
1055
- shop: string
1056
- status: "active" | "pending" | "leave"
1057
- }
1058
-
1059
- const columns: TableColumn<Employee>[] = [
1060
- {
1061
- accessorKey: "name",
1062
- header: "\u6C0F\u540D",
1063
- minSize: 180,
1064
- cell: ({ row }) => (
1065
- <Flex align="center" gap="small">
1066
- <Avatar size="sm" alt={row.original.name} />
1067
- <Typography.Text strong>{row.original.name}</Typography.Text>
1068
- </Flex>
1069
- ),
1070
- meta: { sticky: { side: "left", from: "md" } },
1071
- },
1072
- { accessorKey: "role", header: "\u5F79\u8077", minSize: 120 },
1073
- { accessorKey: "shop", header: "\u5E97\u8217", minSize: 96 },
1074
- {
1075
- accessorKey: "status",
1076
- header: "\u72B6\u614B",
1077
- minSize: 96,
1078
- cell: ({ row }) => {
1079
- if (row.original.status === "active") return <Badge variant="success" dot>\u7A3C\u50CD\u4E2D</Badge>
1080
- if (row.original.status === "pending") return <Badge variant="warning" dot>\u7533\u8ACB\u4E2D</Badge>
1081
- return <Badge variant="neutral" dot>\u4F11\u8077</Badge>
1082
- },
1083
- },
1084
- ]
1085
-
1086
- export function EmployeeTable() {
1087
- const [page, setPage] = useState(1)
1088
- const [selected, setSelected] = useState<string[]>([])
1089
- const [query, setQuery] = useState("")
1090
-
1091
- // Replace with your real API
1092
- const { data, total, loading } = useEmployees({ page, pageSize: 20, query })
1093
-
1094
- return (
1095
- <DataTable
1096
- tableKey="employees-v1"
1097
- columns={columns}
1098
- data={data}
1099
- rowKey="id"
1100
- toolbar={{
1101
- search: { value: query, onValueChange: setQuery },
1102
- primaryAction: { label: "\u65B0\u898F\u8FFD\u52A0", onClick: () => openNewModal() },
1103
- }}
1104
- pagination={{
1105
- current: page, pageSize: 20, total,
1106
- onChange: setPage,
1107
- }}
1108
- batchActions={{
1109
- selectedRowKeys: selected,
1110
- onSelectedRowKeysChange: setSelected,
1111
- actions: ({ clearSelection }) => (
1112
- <Flex gap="small">
1113
- <Button variant="ghost" onClick={clearSelection}>\u89E3\u9664</Button>
1114
- <Button variant="destructive">\u524A\u9664</Button>
1115
- </Flex>
1116
- ),
1117
- }}
1118
- />
1119
- )
1324
+ );
1120
1325
  }`
1121
1326
  },
1122
1327
  {
1123
1328
  name: "confirm-destructive",
1124
- tagline: "Destructive-action confirmation Dialog with typed-name verification.",
1329
+ tagline: 'Type-to-confirm destructive dialog \u2014 Dialog mode="confirm" + Input gate + toast (real @godxjp/ui API).',
1125
1330
  tags: ["dialog", "confirm", "destructive", "delete"],
1126
- code: `import { useState } from "react"
1127
- import { Card, Form, FormField, Input, Button, Flex, Separator, toast } from "@godxjp/ui"
1128
- import { z } from "zod"
1129
- import { zodResolver } from "@hookform/resolvers/zod"
1130
-
1131
- const schema = z.object({ confirm: z.string() })
1132
-
1133
- export function DeleteProjectCard({ projectSlug }: { projectSlug: string }) {
1331
+ code: `import { useState } from "react";
1332
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@godxjp/ui/feedback";
1333
+ import { Input } from "@godxjp/ui/data-entry";
1334
+ import { Button } from "@godxjp/ui/general";
1335
+ import { Stack } from "@godxjp/ui/layout";
1336
+ import { toast } from "sonner";
1337
+
1338
+ export function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {
1339
+ const [confirm, setConfirm] = useState("");
1134
1340
  return (
1135
- <Card
1136
- title="\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664"
1137
- subtitle="\u524A\u9664\u3059\u308B\u3068\u3001\u95A2\u9023\u3059\u308B\u3059\u3079\u3066\u306E\u30BF\u30B9\u30AF\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u304C\u5FA9\u5143\u3067\u304D\u306A\u304F\u306A\u308A\u307E\u3059\u3002"
1138
- accent="destructive"
1139
- style={{ maxWidth: 560 }}
1140
- >
1141
- <Form resolver={zodResolver(schema)} defaultValues={{ confirm: "" }}
1142
- onSubmit={async (v) => {
1143
- if (v.confirm !== projectSlug) {
1144
- toast.error("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093")
1145
- return
1146
- }
1147
- await fetch(\`/api/projects/\${projectSlug}\`, { method: "DELETE" })
1148
- toast.success("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664\u3057\u307E\u3057\u305F")
1149
- }}
1150
- >
1151
- <FormField name="confirm" label={\`\u78BA\u8A8D\u306E\u305F\u3081 "\${projectSlug}" \u3068\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\`} required>
1152
- <Input placeholder={projectSlug} />
1153
- </FormField>
1154
- <Separator />
1155
- <Flex gap="small" justify="end">
1156
- <Button variant="ghost" type="button">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
1157
- <Button type="submit" variant="destructive">\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u5B8C\u5168\u306B\u524A\u9664</Button>
1158
- </Flex>
1159
- </Form>
1160
- </Card>
1161
- )
1162
- }`
1163
- },
1164
- {
1165
- name: "app-shell",
1166
- tagline: "AppShell wiring \u2014 Sidebar + Topbar + PageContent + Toaster.",
1167
- tags: ["shell", "layout", "navigation", "app-root"],
1168
- code: `import {
1169
- AppShell, Sidebar, Topbar, PageContent,
1170
- GodxConfigProvider, Toaster, Button, Typography,
1171
- } from "@godxjp/ui"
1172
- import { LayoutDashboard, FolderGit2, GitBranch, Settings } from "lucide-react"
1173
- import { useState } from "react"
1174
-
1175
- const SECTIONS = [
1176
- {
1177
- label: "Workspace",
1178
- items: [
1179
- { id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
1180
- { id: "projects", label: "Projects", icon: FolderGit2, badge: 8 },
1181
- { id: "branches", label: "Branches", icon: GitBranch },
1182
- ],
1183
- },
1184
- {
1185
- label: "Admin",
1186
- items: [
1187
- { id: "settings", label: "Settings", icon: Settings },
1188
- ],
1189
- },
1190
- ]
1191
-
1192
- export function App() {
1193
- const [active, setActive] = useState("dashboard")
1194
-
1195
- return (
1196
- <GodxConfigProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo" defaultCurrency="JPY">
1197
- <AppShell
1198
- sidebar={<Sidebar activeId={active} onSelect={setActive} sections={SECTIONS} />}
1199
- topbar={<Topbar product={{ id: "godx", label: "GoDX Forge" }} project={{ id: "p1", label: "Acme" }} />}
1200
- >
1201
- <PageContent
1202
- title="Dashboard"
1203
- subtitle="Workspace activity, KPIs"
1204
- extra={<Button variant="primary">New issue</Button>}
1205
- >
1206
- <Typography.Paragraph>Page body goes here.</Typography.Paragraph>
1207
- </PageContent>
1208
- </AppShell>
1209
- <Toaster position="top-right" />
1210
- </GodxConfigProvider>
1211
- )
1341
+ <Dialog open={open} onOpenChange={onOpenChange} mode="confirm">
1342
+ <DialogContent>
1343
+ <DialogHeader>
1344
+ <DialogTitle>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664</DialogTitle>
1345
+ <DialogDescription>\u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002\u78BA\u8A8D\u306E\u305F\u3081\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D "{slug}" \u3068\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002</DialogDescription>
1346
+ </DialogHeader>
1347
+ <Stack gap="md">
1348
+ <Input value={confirm} onChange={(e) => setConfirm(e.target.value)} placeholder={slug} />
1349
+ </Stack>
1350
+ <DialogFooter>
1351
+ <Button variant="outline" onClick={() => onOpenChange(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
1352
+ <Button variant="destructive" disabled={confirm !== slug} onClick={() => { toast.success("\u524A\u9664\u3057\u307E\u3057\u305F"); onOpenChange(false); }}>\u5B8C\u5168\u306B\u524A\u9664</Button>
1353
+ </DialogFooter>
1354
+ </DialogContent>
1355
+ </Dialog>
1356
+ );
1212
1357
  }`
1213
1358
  },
1214
1359
  {
1215
- name: "filter-bar",
1216
- tagline: "Inline filter bar above a table \u2014 search + selects + reset.",
1217
- tags: ["filter", "search", "inline", "table"],
1218
- code: `import { useState } from "react"
1219
- import { Form, FormField, Input, Select, Button } from "@godxjp/ui"
1360
+ name: "deferred-loading",
1361
+ tagline: "Inertia deferred props with a Skeleton fallback \u2014 SkeletonTable while data loads, then DataTable (real @godxjp/ui API).",
1362
+ tags: ["loading", "skeleton", "deferred", "inertia", "table"],
1363
+ code: `// Server (Laravel): defer the heavy prop
1364
+ // Inertia::render('crm/coupons/index', [
1365
+ // 'coupons' => Inertia::defer(fn () => Coupon::all()),
1366
+ // ]);
1367
+ import { Card, CardContent, DataTable } from "@godxjp/ui/data-display";
1368
+ import type { ColumnDef } from "@godxjp/ui/data-display";
1369
+ import { SkeletonTable } from "@godxjp/ui/feedback";
1220
1370
 
1221
- export function EmployeeFilter({ onChange }: { onChange: (v: any) => void }) {
1222
- return (
1223
- <Form layout="inline" defaultValues={{ query: "", status: "active", shop: "shibuya" }}
1224
- onSubmit={(v) => onChange(v)}
1225
- >
1226
- <FormField name="query" label="\u30AD\u30FC\u30EF\u30FC\u30C9">
1227
- <Input placeholder="\u540D\u524D / \u30E1\u30FC\u30EB / \u96FB\u8A71\u756A\u53F7" style={{ width: "16rem" }} />
1228
- </FormField>
1229
- <FormField name="status" label="\u30B9\u30C6\u30FC\u30BF\u30B9">
1230
- <Select options={[
1231
- { value: "active", label: "\u7A3C\u50CD\u4E2D" },
1232
- { value: "paused", label: "\u4E00\u6642\u505C\u6B62" },
1233
- ]} />
1234
- </FormField>
1235
- <FormField name="shop" label="\u5E97\u8217">
1236
- <Select options={[
1237
- { value: "shibuya", label: "\u6E0B\u8C37\u5E97" },
1238
- { value: "shinjuku", label: "\u65B0\u5BBF\u5E97" },
1239
- ]} />
1240
- </FormField>
1241
- <Button type="submit" variant="primary">\u691C\u7D22</Button>
1242
- <Button type="reset" variant="ghost">\u30EA\u30BB\u30C3\u30C8</Button>
1243
- </Form>
1244
- )
1245
- }`
1246
- },
1247
- {
1248
- name: "loading-states",
1249
- tagline: "Skeleton on init fetch + Spinner on save (UX nuance).",
1250
- tags: ["loading", "skeleton", "spinner", "form"],
1251
- code: `import { useState, useEffect } from "react"
1252
- import {
1253
- Card, Form, FormField, Input, Textarea, Button, Flex, Separator,
1254
- } from "@godxjp/ui"
1255
-
1256
- export function ProfileEditor() {
1257
- const [submitting, setSubmitting] = useState(false)
1258
- const [initialFetched, setInitialFetched] = useState(false)
1259
- const [values, setValues] = useState({ name: "", bio: "" })
1260
-
1261
- // Initial fetch
1262
- useEffect(() => {
1263
- fetch("/api/me").then(r => r.json()).then(data => {
1264
- setValues(data)
1265
- setInitialFetched(true)
1266
- })
1267
- }, [])
1371
+ type Coupon = { id: string; name: string };
1372
+ const columns: ColumnDef<Coupon>[] = [{ key: "name", header: "\u30AF\u30FC\u30DD\u30F3\u540D" }];
1268
1373
 
1374
+ // coupons is undefined until the deferred request resolves
1375
+ export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
1269
1376
  return (
1270
- <Card title="\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB" style={{ maxWidth: 640 }}>
1271
- <Form
1272
- loading={!initialFetched ? { kind: "skeleton" } : submitting}
1273
- defaultValues={values}
1274
- onSubmit={async (v) => {
1275
- setSubmitting(true)
1276
- await fetch("/api/me", { method: "PUT", body: JSON.stringify(v) })
1277
- setSubmitting(false)
1278
- }}
1279
- >
1280
- <FormField name="name" label="\u6C0F\u540D" required><Input /></FormField>
1281
- <FormField name="bio" label="\u81EA\u5DF1\u7D39\u4ECB"><Textarea rows={3} /></FormField>
1282
- <Separator />
1283
- <Flex gap="small" justify="end">
1284
- <Button variant="ghost" type="button" disabled={submitting}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
1285
- <Button type="submit" variant="primary" loading={submitting}>\u4FDD\u5B58</Button>
1286
- </Flex>
1287
- </Form>
1377
+ <Card>
1378
+ <CardContent flush>
1379
+ {!coupons
1380
+ ? <SkeletonTable rows={10} columns={6} />
1381
+ : <DataTable data={coupons} columns={columns} getRowId={(c) => c.id} />}
1382
+ </CardContent>
1288
1383
  </Card>
1289
- )
1384
+ );
1290
1385
  }`
1291
1386
  },
1292
1387
  {