@godxjp/ui-mcp 0.2.1 → 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 +1117 -812
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,643 +12,973 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// src/data/components.ts
|
|
14
14
|
var COMPONENTS = [
|
|
15
|
-
// ───
|
|
15
|
+
// ─── layout ─────────────────────────────────────────────────────────────
|
|
16
16
|
{
|
|
17
|
-
name: "
|
|
18
|
-
group: "
|
|
19
|
-
tagline: "
|
|
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: "
|
|
22
|
-
{ name: "
|
|
23
|
-
{ name: "
|
|
24
|
-
{ name: "
|
|
25
|
-
{ name: "
|
|
26
|
-
{ name: "
|
|
27
|
-
{ name: "
|
|
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:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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: "
|
|
42
|
-
group: "
|
|
43
|
-
tagline: "
|
|
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: "
|
|
46
|
-
{ name: "
|
|
47
|
-
{ name: "
|
|
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:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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: "
|
|
143
|
+
name: "Sidebar",
|
|
59
144
|
group: "layout",
|
|
60
|
-
tagline: "
|
|
145
|
+
tagline: "Navigation sidebar with sections, items, product header, and collapsible icon-only mode.",
|
|
61
146
|
props: [
|
|
62
|
-
{ name: "
|
|
63
|
-
{ name: "
|
|
64
|
-
{ name: "
|
|
65
|
-
{ name: "
|
|
66
|
-
{ name: "
|
|
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:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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: "
|
|
177
|
+
name: "Topbar",
|
|
78
178
|
group: "layout",
|
|
79
|
-
tagline: "
|
|
179
|
+
tagline: "Application topbar with product/project switcher and search/notification slots (or use AppShell's topbarRight).",
|
|
80
180
|
props: [
|
|
81
|
-
{ name: "
|
|
82
|
-
{ name: "
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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: "
|
|
193
|
+
name: "PageInset",
|
|
94
194
|
group: "layout",
|
|
95
|
-
tagline:
|
|
195
|
+
tagline: 'Padded horizontal strip aligned with the page header \u2014 use inside variant="flush" for filter bars / intros.',
|
|
96
196
|
props: [
|
|
97
|
-
{ name: "
|
|
98
|
-
{ name: "
|
|
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:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
303
|
+
tagline: "Surface container with optional accent stripe, variant fill, size, and density. Compose with CardHeader/CardTitle/CardContent/CardFooter.",
|
|
113
304
|
props: [
|
|
114
|
-
{ name: "
|
|
115
|
-
{ name: "
|
|
116
|
-
{ name: "
|
|
117
|
-
{ name: "
|
|
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:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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: [
|
|
317
|
+
rules: []
|
|
140
318
|
},
|
|
141
319
|
{
|
|
142
|
-
name: "
|
|
320
|
+
name: "CardContent",
|
|
143
321
|
group: "data-display",
|
|
144
|
-
tagline: "
|
|
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: "
|
|
147
|
-
{ name: "
|
|
148
|
-
{ name: "
|
|
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:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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: "
|
|
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: '"
|
|
178
|
-
{ name: "
|
|
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:
|
|
182
|
-
|
|
183
|
-
<Badge variant="
|
|
184
|
-
|
|
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: [
|
|
394
|
+
rules: [35]
|
|
187
395
|
},
|
|
188
396
|
{
|
|
189
|
-
name: "
|
|
397
|
+
name: "KeyValueGrid",
|
|
190
398
|
group: "data-display",
|
|
191
|
-
tagline: "
|
|
399
|
+
tagline: "Responsive definition grid for detail-page metadata. COMPOUND \u2014 value goes in KeyValueGrid.Item children.",
|
|
192
400
|
props: [
|
|
193
|
-
{ name: "
|
|
194
|
-
{ name: "
|
|
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:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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: "
|
|
415
|
+
name: "EmptyState",
|
|
208
416
|
group: "data-display",
|
|
209
|
-
tagline: "
|
|
417
|
+
tagline: "Centred empty placeholder with icon, title, description, and optional CTA.",
|
|
210
418
|
props: [
|
|
211
|
-
{ name: "
|
|
212
|
-
{ name: "
|
|
213
|
-
{ name: "
|
|
214
|
-
{ name: "
|
|
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:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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: "
|
|
446
|
+
name: "Timeline",
|
|
223
447
|
group: "data-display",
|
|
224
|
-
tagline: "
|
|
448
|
+
tagline: "Vertical event list with an icon rail. Current item gets a highlighted glyph.",
|
|
449
|
+
props: [
|
|
450
|
+
{ name: "items", type: "TimelineItem[]", required: true, description: "Array of { title, location?, time?, note?, current? }." }
|
|
451
|
+
],
|
|
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.",
|
|
225
535
|
props: [
|
|
226
|
-
{ name: "
|
|
227
|
-
{ name: "
|
|
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." }
|
|
228
542
|
],
|
|
229
|
-
example:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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: "
|
|
554
|
+
tagline: "Styled wrapper around native <input>; accepts all HTML input attributes. Pair with FormField for labelled fields.",
|
|
239
555
|
props: [
|
|
240
|
-
{ name: "
|
|
241
|
-
{ name: "
|
|
242
|
-
{ name: "
|
|
243
|
-
{ name: "
|
|
244
|
-
{ name: "
|
|
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:
|
|
247
|
-
|
|
248
|
-
|
|
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: [
|
|
566
|
+
rules: []
|
|
255
567
|
},
|
|
256
568
|
{
|
|
257
|
-
name: "
|
|
569
|
+
name: "SearchInput",
|
|
258
570
|
group: "data-entry",
|
|
259
|
-
tagline: "
|
|
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: "
|
|
262
|
-
{ name: "
|
|
263
|
-
{ name: "
|
|
264
|
-
{ name: "
|
|
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:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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: "
|
|
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: "
|
|
281
|
-
{ name: "
|
|
282
|
-
{ name: "
|
|
283
|
-
{ name: "
|
|
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:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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: [
|
|
606
|
+
rules: [23]
|
|
296
607
|
},
|
|
297
608
|
{
|
|
298
|
-
name: "
|
|
609
|
+
name: "Switch",
|
|
299
610
|
group: "data-entry",
|
|
300
|
-
tagline: "
|
|
611
|
+
tagline: "Radix toggle switch (bare). For a labelled row with a hidden form input use SwitchField.",
|
|
301
612
|
props: [
|
|
302
|
-
{ name: "
|
|
303
|
-
{ name: "
|
|
304
|
-
{ name: "
|
|
305
|
-
{ name: "
|
|
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:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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: "
|
|
628
|
+
name: "Textarea",
|
|
326
629
|
group: "data-entry",
|
|
327
|
-
tagline: "
|
|
630
|
+
tagline: "Styled wrapper around native <textarea>. Pair with FormField for labelled fields.",
|
|
328
631
|
props: [
|
|
329
|
-
{ name: "
|
|
330
|
-
{ name: "
|
|
331
|
-
{ name: "
|
|
332
|
-
{ name: "
|
|
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:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
storyPath: "data-entry/
|
|
338
|
-
rules: [
|
|
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: "
|
|
644
|
+
name: "Label",
|
|
342
645
|
group: "data-entry",
|
|
343
|
-
tagline: "
|
|
646
|
+
tagline: "Styled Radix Label; use htmlFor to associate with a control.",
|
|
344
647
|
props: [
|
|
345
|
-
{ name: "
|
|
346
|
-
{ name: "children", type: "ReactNode", description: "Label
|
|
648
|
+
{ name: "htmlFor", type: "string", description: "Id of the associated control." },
|
|
649
|
+
{ name: "children", type: "ReactNode", description: "Label content." }
|
|
347
650
|
],
|
|
348
|
-
example:
|
|
349
|
-
|
|
350
|
-
</
|
|
351
|
-
storyPath: "data-entry/
|
|
352
|
-
rules: [
|
|
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: "
|
|
658
|
+
name: "Checkbox",
|
|
356
659
|
group: "data-entry",
|
|
357
|
-
tagline: "
|
|
660
|
+
tagline: "Radix checkbox; standalone or via CheckboxGroup with an options array.",
|
|
358
661
|
props: [
|
|
359
|
-
{ name: "checked
|
|
360
|
-
{ name: "
|
|
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:
|
|
363
|
-
|
|
364
|
-
|
|
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: "
|
|
676
|
+
name: "RadioGroup",
|
|
368
677
|
group: "data-entry",
|
|
369
|
-
tagline: "
|
|
678
|
+
tagline: "Radio group accepting an options array or RadioItem children.",
|
|
370
679
|
props: [
|
|
371
|
-
{ name: "value
|
|
372
|
-
{ name: "
|
|
373
|
-
{ name: "
|
|
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:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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: "
|
|
383
|
-
group: "
|
|
384
|
-
tagline: "
|
|
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: "
|
|
387
|
-
{ name: "
|
|
388
|
-
{ name: "
|
|
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:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
storyPath: "
|
|
398
|
-
rules: [
|
|
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
|
|
713
|
+
name: "Dialog",
|
|
402
714
|
group: "feedback",
|
|
403
|
-
tagline:
|
|
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
|
|
406
|
-
{ name: "
|
|
407
|
-
{ name: "
|
|
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:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
>
|
|
418
|
-
|
|
419
|
-
|
|
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: [
|
|
745
|
+
rules: [23, 3]
|
|
422
746
|
},
|
|
423
747
|
{
|
|
424
|
-
name: "Sheet
|
|
748
|
+
name: "Sheet",
|
|
425
749
|
group: "feedback",
|
|
426
|
-
tagline: "Side
|
|
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
|
|
429
|
-
{ name: "
|
|
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:
|
|
433
|
-
|
|
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/
|
|
436
|
-
rules: [3
|
|
765
|
+
storyPath: "feedback/Sheet.stories.tsx",
|
|
766
|
+
rules: [3]
|
|
437
767
|
},
|
|
438
768
|
{
|
|
439
|
-
name: "
|
|
769
|
+
name: "Alert",
|
|
440
770
|
group: "feedback",
|
|
441
|
-
tagline: "
|
|
771
|
+
tagline: "Inline alert banner with variant-aware icon + optional dismiss. Parts: Alert/AlertTitle/AlertDescription/AlertActions/AlertQueryError.",
|
|
442
772
|
props: [
|
|
443
|
-
{ name: "
|
|
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:
|
|
446
|
-
<GodxConfigProvider><App /><Toaster position="top-right" /></GodxConfigProvider>
|
|
777
|
+
example: `import { Alert, AlertTitle, AlertDescription } from "@godxjp/ui/feedback";
|
|
447
778
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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: "
|
|
787
|
+
name: "SkeletonTable",
|
|
456
788
|
group: "feedback",
|
|
457
|
-
tagline: "Loading placeholder
|
|
789
|
+
tagline: "Loading placeholder matching the DataTable layout (header + N rows). Drop-in while data loads (deferred props).",
|
|
458
790
|
props: [
|
|
459
|
-
{ name: "
|
|
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:
|
|
462
|
-
|
|
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: [
|
|
798
|
+
rules: []
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "SkeletonCard",
|
|
802
|
+
group: "feedback",
|
|
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: []
|
|
465
811
|
},
|
|
466
812
|
{
|
|
467
|
-
name: "
|
|
813
|
+
name: "Toaster",
|
|
468
814
|
group: "feedback",
|
|
469
|
-
tagline:
|
|
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: "
|
|
472
|
-
{ name: "
|
|
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:
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
835
|
+
tagline: "Radix tab container. Compose Tabs/TabsList/TabsTrigger/TabsContent. Controlled (value/onValueChange) or uncontrolled (defaultValue).",
|
|
483
836
|
props: [
|
|
484
|
-
{ name: "
|
|
485
|
-
{ name: "
|
|
486
|
-
{ name: "
|
|
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:
|
|
490
|
-
|
|
491
|
-
|
|
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: [
|
|
852
|
+
rules: []
|
|
495
853
|
},
|
|
496
854
|
{
|
|
497
|
-
name: "
|
|
855
|
+
name: "FilterBar",
|
|
498
856
|
group: "navigation",
|
|
499
|
-
tagline: "
|
|
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: "
|
|
502
|
-
{ name: "
|
|
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:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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: "
|
|
898
|
+
tagline: "Offset/page-based pagination bar. Sits below a table card.",
|
|
518
899
|
props: [
|
|
519
|
-
{ name: "current
|
|
520
|
-
{ name: "
|
|
521
|
-
{ name: "
|
|
522
|
-
{ name: "
|
|
523
|
-
{ name: "
|
|
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:
|
|
526
|
-
|
|
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: [
|
|
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: "
|
|
563
|
-
group: "
|
|
564
|
-
tagline: "
|
|
913
|
+
name: "DropdownMenu",
|
|
914
|
+
group: "navigation",
|
|
915
|
+
tagline: "Radix dropdown menu. Compose DropdownMenu/DropdownMenuTrigger/DropdownMenuContent/DropdownMenuItem/DropdownMenuSeparator.",
|
|
565
916
|
props: [
|
|
566
|
-
{ name: "
|
|
567
|
-
{ name: "
|
|
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:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
>
|
|
575
|
-
<
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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: "
|
|
587
|
-
{ name: "
|
|
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:
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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: "
|
|
601
|
-
group: "
|
|
602
|
-
tagline: "
|
|
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: "
|
|
627
|
-
{ name: "
|
|
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:
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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: "
|
|
969
|
+
name: "formatDate",
|
|
638
970
|
group: "providers",
|
|
639
|
-
tagline: "
|
|
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: "
|
|
642
|
-
{ name: "
|
|
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:
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
storyPath: "
|
|
651
|
-
rules: [
|
|
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) {
|
|
@@ -890,7 +1220,13 @@ var CARDINAL_RULES = [
|
|
|
890
1220
|
{ number: 31, title: "No nested wrapper / convenience primitives", body: "One Radix base = one framework primitive. `<SimpleX>` over `<X>` is forbidden; add a prop to `<X>` instead. Composites under `src/components/composites/` that combine multiple primitives are NOT wrappers." },
|
|
891
1221
|
{ number: 32, title: "No redundant props", body: "Before adding a prop / item field / variant, grep the existing surface; if a field already covers the concept, use it. Top-level prop that re-expresses an item field (Timeline `pending` \u2194 `items[i].animate`) is rejected." },
|
|
892
1222
|
{ number: 33, title: "Stories / source / docs name-synchronized", body: "No two names for the same export across the framework surface; no legacy aliases in stories / docs (source may keep an alias for a deprecation cycle, but the marketing surfaces use the canonical name only). Rename PR runs `grep -rn '<oldName>' src docs` and clears it." },
|
|
893
|
-
{ number: 34, title: "Storybook source panel = real, copy-paste-ready code", body: 'Storybook\'s react-docgen serializer strips every function value (`cell: ({row}) => <JSX/>`, `render: ({field}) => <Input/>`, `rowClassName`, `renderItem`, \u2026) to `() => {}`. Any story whose `render` passes a function-valued prop, references a module-level helper (`StatusBadge`, `EMPLOYEE_COLUMNS`, etc.), or uses a render-prop pattern MUST override `parameters.docs.source.code` with the literal copy-paste-ready snippet \u2014 type aliases, helper functions spelled out, column definitions with cell JSX visible, inline data array. The `render()` callback stays as-is (module-level constants are fine for runtime performance); `source.code` is the marketing surface. Skip ONLY for stories whose JSX is purely static primitives Storybook can serialize verbatim (`<Button variant="primary">Click</Button>`). The exemplar is `Table.Default` in `src/stories/data-display/Table.stories.tsx`.' }
|
|
1223
|
+
{ number: 34, title: "Storybook source panel = real, copy-paste-ready code", body: 'Storybook\'s react-docgen serializer strips every function value (`cell: ({row}) => <JSX/>`, `render: ({field}) => <Input/>`, `rowClassName`, `renderItem`, \u2026) to `() => {}`. Any story whose `render` passes a function-valued prop, references a module-level helper (`StatusBadge`, `EMPLOYEE_COLUMNS`, etc.), or uses a render-prop pattern MUST override `parameters.docs.source.code` with the literal copy-paste-ready snippet \u2014 type aliases, helper functions spelled out, column definitions with cell JSX visible, inline data array. The `render()` callback stays as-is (module-level constants are fine for runtime performance); `source.code` is the marketing surface. Skip ONLY for stories whose JSX is purely static primitives Storybook can serialize verbatim (`<Button variant="primary">Click</Button>`). The exemplar is `Table.Default` in `src/stories/data-display/Table.stories.tsx`.' },
|
|
1224
|
+
{ number: 35, title: "Status chips never wrap", body: "A `StatusBadge` / `Badge` reads as one atomic unit. Its label must never break across lines \u2014 pin `white-space: nowrap` on the chip (done in `badge-layout.css`), especially inside narrow `DataTable` cells (\u30B9\u30B3\u30FC\u30D7 / \u30B9\u30C6\u30FC\u30BF\u30B9 columns). If a cell is too tight, widen the column or shorten the label; never let the chip wrap." },
|
|
1225
|
+
{ number: 36, title: "StatusBadge tone/icon are the colour escape hatch", body: "`StatusBadge` auto-maps a fixed set of English lifecycle keys (active, draft, pending, scheduled, cancelled, failed, \u2026) to tone + icon. For ANY other value \u2014 localized labels (\u516C\u958B\u4E2D, \u30A2\u30AF\u30C6\u30A3\u30D6) or categorical tiers (\u4F1A\u54E1\u30E9\u30F3\u30AF, \u5951\u7D04\u30D7\u30E9\u30F3) \u2014 pass `tone` explicitly (success | warning | destructive | info | neutral) and, for non-lifecycle tiers, `icon={null}` to drop the misleading glyph. Don't let domain labels fall back to neutral grey + \u25CB. Map domain\u2192tone in the CONSUMER layer; the framework only provides the props." },
|
|
1226
|
+
{ number: 37, title: "DataTable is full-width \u2014 never inside a narrow grid column", body: "A multi-column `DataTable` occupies its OWN row at the page's full width: `<Card><CardContent flush><DataTable \u2026/></CardContent></Card>`. Never nest it in a `lg:col-span-2` of a `ResponsiveGrid columns={3}` beside a chart \u2014 the columns get squeezed until CJK text collapses to one character per line. Charts / KPI cards go in their own row ABOVE the table. (See the `inertia-list-page` pattern.)" },
|
|
1227
|
+
{ number: 38, title: "FilterBar stays OUT of CardContent flush", body: "`CardContent flush` strips horizontal padding for edge-to-edge tables. A `FilterBar` placed inside it loses all padding and sticks to the card edge. Render `FilterBar` as a STANDALONE block above the table card; wrap ONLY the `DataTable` / `EmptyState` in the `Card` + `CardContent flush`. Order on a list page: KPIs \u2192 FilterBar \u2192 table card." },
|
|
1228
|
+
{ number: 39, title: "Long text columns get an explicit width", body: "For columns whose value can be long (name / title / segment / address), set `col.width` to a Tailwind width class (e.g. `w-64`, `w-48`) so the column reserves space instead of shrinking and wrapping to many lines; leave numeric / status columns auto. Table cells default to `white-space: nowrap`, so an over-tight table scrolls horizontally rather than crushing \u2014 give the important columns real widths so the default layout reads well before any scroll." },
|
|
1229
|
+
{ number: 40, title: "Pages are mobile-first", body: "Author and verify every page at 320\u2013390px FIRST. Spacing comes only from `Stack` / `Inline` `gap` + `ResponsiveGrid columns={2|3|4}` (which collapse to a single column on narrow screens) \u2014 never raw `p-*` / `gap-*` / `space-*` utilities for page layout. Wide tables scroll horizontally on small screens (don't force-fit them); dialogs and sheets are full-height on mobile. Touch targets \u2265 44\xD744px." }
|
|
894
1230
|
];
|
|
895
1231
|
function findRule(num) {
|
|
896
1232
|
return CARDINAL_RULES.find((r) => r.number === num);
|
|
@@ -899,389 +1235,358 @@ function findRule(num) {
|
|
|
899
1235
|
// src/data/patterns.ts
|
|
900
1236
|
var PATTERNS = [
|
|
901
1237
|
{
|
|
902
|
-
name: "
|
|
903
|
-
tagline: "Card-wrapped sign-up form
|
|
904
|
-
tags: ["form", "auth", "sign-up", "zod", "validation"],
|
|
905
|
-
code: `import {
|
|
906
|
-
import { zodResolver } from "@hookform/resolvers/zod"
|
|
907
|
-
import {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
} 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";
|
|
911
1248
|
|
|
912
1249
|
const schema = z.object({
|
|
913
1250
|
name: z.string().min(1, "\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059"),
|
|
914
1251
|
email: z.string().email("\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044"),
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
.regex(/[A-Z]/, "\u5927\u6587\u5B57\u3092 1 \u3064\u4EE5\u4E0A\u542B\u3081\u3066\u304F\u3060\u3055\u3044")
|
|
918
|
-
.regex(/\\d/, "\u6570\u5B57\u3092 1 \u3064\u4EE5\u4E0A\u542B\u3081\u3066\u304F\u3060\u3055\u3044"),
|
|
919
|
-
agree: z.literal(true, { message: "\u5229\u7528\u898F\u7D04\u3078\u306E\u540C\u610F\u304C\u5FC5\u8981\u3067\u3059" }),
|
|
920
|
-
})
|
|
921
|
-
type SignUpValues = z.infer<typeof schema>
|
|
1252
|
+
});
|
|
1253
|
+
type Values = z.infer<typeof schema>;
|
|
922
1254
|
|
|
923
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
|
+
});
|
|
924
1260
|
return (
|
|
925
|
-
<Card
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
</
|
|
941
|
-
|
|
942
|
-
<Input type="email" placeholder="taro@example.com" />
|
|
943
|
-
</FormField>
|
|
944
|
-
<FormField name="password" label="\u30D1\u30B9\u30EF\u30FC\u30C9" required
|
|
945
|
-
description="8 \u6587\u5B57\u4EE5\u4E0A / \u5927\u6587\u5B57 1 / \u6570\u5B57 1">
|
|
946
|
-
<InputPassword placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" />
|
|
947
|
-
</FormField>
|
|
948
|
-
<FormField name="agree">
|
|
949
|
-
<Checkbox>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>
|
|
950
|
-
</FormField>
|
|
951
|
-
<Button type="submit" variant="primary" block>
|
|
952
|
-
\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210
|
|
953
|
-
</Button>
|
|
954
|
-
</Form>
|
|
955
|
-
<Separator style={{ margin: "var(--spacing-3) 0" }} />
|
|
956
|
-
<Typography.Text color="secondary" style={{ textAlign: "center", display: "block" }}>
|
|
957
|
-
\u65E2\u306B\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u304A\u6301\u3061\u3067\u3059\u304B? <a href="/login">\u30ED\u30B0\u30A4\u30F3</a>
|
|
958
|
-
</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>
|
|
959
1278
|
</Card>
|
|
960
|
-
)
|
|
1279
|
+
);
|
|
961
1280
|
}`
|
|
962
1281
|
},
|
|
963
1282
|
{
|
|
964
|
-
name: "settings-
|
|
965
|
-
tagline: "Sectioned settings Card
|
|
966
|
-
tags: ["settings", "form", "
|
|
967
|
-
code: `import {
|
|
968
|
-
import {
|
|
969
|
-
import {
|
|
970
|
-
|
|
971
|
-
Button, Separator, Typography, Flex,
|
|
972
|
-
} from "@godxjp/ui"
|
|
973
|
-
|
|
974
|
-
const schema = z.object({
|
|
975
|
-
workspaceName: z.string().min(1),
|
|
976
|
-
visibility: z.string(),
|
|
977
|
-
notifyOnComment: z.boolean(),
|
|
978
|
-
notifyOnMention: z.boolean(),
|
|
979
|
-
digestFrequency: z.string(),
|
|
980
|
-
})
|
|
981
|
-
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";
|
|
982
1290
|
|
|
983
1291
|
export function WorkspaceSettings() {
|
|
984
1292
|
return (
|
|
985
|
-
<Card
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
<FormField name="notifyOnComment" label="\u30B3\u30E1\u30F3\u30C8"><Switch /></FormField>
|
|
1016
|
-
<FormField name="notifyOnMention" label="\u30E1\u30F3\u30B7\u30E7\u30F3"><Switch /></FormField>
|
|
1017
|
-
<FormField name="digestFrequency" label="\u30C0\u30A4\u30B8\u30A7\u30B9\u30C8">
|
|
1018
|
-
<Select options={[
|
|
1019
|
-
{ value: "off", label: "\u9001\u4FE1\u3057\u306A\u3044" },
|
|
1020
|
-
{ value: "daily", label: "\u6BCE\u671D" },
|
|
1021
|
-
{ value: "weekly", label: "\u9031\u6B21" },
|
|
1022
|
-
]} />
|
|
1023
|
-
</FormField>
|
|
1024
|
-
|
|
1025
|
-
<Separator />
|
|
1026
|
-
<Flex gap="small" justify="end">
|
|
1027
|
-
<Button variant="ghost" type="button">\u5909\u66F4\u3092\u7834\u68C4</Button>
|
|
1028
|
-
<Button type="submit" variant="primary">\u8A2D\u5B9A\u3092\u4FDD\u5B58</Button>
|
|
1029
|
-
</Flex>
|
|
1030
|
-
</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>
|
|
1031
1323
|
</Card>
|
|
1032
|
-
)
|
|
1324
|
+
);
|
|
1033
1325
|
}`
|
|
1034
1326
|
},
|
|
1035
1327
|
{
|
|
1036
|
-
name: "
|
|
1037
|
-
tagline:
|
|
1038
|
-
tags: ["
|
|
1039
|
-
code: `import { useState } from "react"
|
|
1040
|
-
import {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
} from "@godxjp/ui"
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
role: string
|
|
1049
|
-
shop: string
|
|
1050
|
-
status: "active" | "pending" | "leave"
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
const columns: TableColumn<Employee>[] = [
|
|
1054
|
-
{
|
|
1055
|
-
accessorKey: "name",
|
|
1056
|
-
header: "\u6C0F\u540D",
|
|
1057
|
-
minSize: 180,
|
|
1058
|
-
cell: ({ row }) => (
|
|
1059
|
-
<Flex align="center" gap="small">
|
|
1060
|
-
<Avatar size="sm" alt={row.original.name} />
|
|
1061
|
-
<Typography.Text strong>{row.original.name}</Typography.Text>
|
|
1062
|
-
</Flex>
|
|
1063
|
-
),
|
|
1064
|
-
meta: { sticky: { side: "left", from: "md" } },
|
|
1065
|
-
},
|
|
1066
|
-
{ accessorKey: "role", header: "\u5F79\u8077", minSize: 120 },
|
|
1067
|
-
{ accessorKey: "shop", header: "\u5E97\u8217", minSize: 96 },
|
|
1068
|
-
{
|
|
1069
|
-
accessorKey: "status",
|
|
1070
|
-
header: "\u72B6\u614B",
|
|
1071
|
-
minSize: 96,
|
|
1072
|
-
cell: ({ row }) => {
|
|
1073
|
-
if (row.original.status === "active") return <Badge variant="success" dot>\u7A3C\u50CD\u4E2D</Badge>
|
|
1074
|
-
if (row.original.status === "pending") return <Badge variant="warning" dot>\u7533\u8ACB\u4E2D</Badge>
|
|
1075
|
-
return <Badge variant="neutral" dot>\u4F11\u8077</Badge>
|
|
1076
|
-
},
|
|
1077
|
-
},
|
|
1078
|
-
]
|
|
1079
|
-
|
|
1080
|
-
export function EmployeeTable() {
|
|
1081
|
-
const [page, setPage] = useState(1)
|
|
1082
|
-
const [selected, setSelected] = useState<string[]>([])
|
|
1083
|
-
const [query, setQuery] = useState("")
|
|
1084
|
-
|
|
1085
|
-
// Replace with your real API
|
|
1086
|
-
const { data, total, loading } = useEmployees({ page, pageSize: 20, query })
|
|
1087
|
-
|
|
1328
|
+
name: "confirm-destructive",
|
|
1329
|
+
tagline: 'Type-to-confirm destructive dialog \u2014 Dialog mode="confirm" + Input gate + toast (real @godxjp/ui API).',
|
|
1330
|
+
tags: ["dialog", "confirm", "destructive", "delete"],
|
|
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("");
|
|
1088
1340
|
return (
|
|
1089
|
-
<
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
actions: ({ clearSelection }) => (
|
|
1106
|
-
<Flex gap="small">
|
|
1107
|
-
<Button variant="ghost" onClick={clearSelection}>\u89E3\u9664</Button>
|
|
1108
|
-
<Button variant="destructive">\u524A\u9664</Button>
|
|
1109
|
-
</Flex>
|
|
1110
|
-
),
|
|
1111
|
-
}}
|
|
1112
|
-
/>
|
|
1113
|
-
)
|
|
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
|
+
);
|
|
1114
1357
|
}`
|
|
1115
1358
|
},
|
|
1116
1359
|
{
|
|
1117
|
-
name: "
|
|
1118
|
-
tagline: "
|
|
1119
|
-
tags: ["
|
|
1120
|
-
code:
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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";
|
|
1124
1370
|
|
|
1125
|
-
|
|
1371
|
+
type Coupon = { id: string; name: string };
|
|
1372
|
+
const columns: ColumnDef<Coupon>[] = [{ key: "name", header: "\u30AF\u30FC\u30DD\u30F3\u540D" }];
|
|
1126
1373
|
|
|
1127
|
-
|
|
1374
|
+
// coupons is undefined until the deferred request resolves
|
|
1375
|
+
export default function Coupons({ coupons }: { coupons?: Coupon[] }) {
|
|
1128
1376
|
return (
|
|
1129
|
-
<Card
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
<Form resolver={zodResolver(schema)} defaultValues={{ confirm: "" }}
|
|
1136
|
-
onSubmit={async (v) => {
|
|
1137
|
-
if (v.confirm !== projectSlug) {
|
|
1138
|
-
toast.error("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093")
|
|
1139
|
-
return
|
|
1140
|
-
}
|
|
1141
|
-
await fetch(\`/api/projects/\${projectSlug}\`, { method: "DELETE" })
|
|
1142
|
-
toast.success("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664\u3057\u307E\u3057\u305F")
|
|
1143
|
-
}}
|
|
1144
|
-
>
|
|
1145
|
-
<FormField name="confirm" label={\`\u78BA\u8A8D\u306E\u305F\u3081 "\${projectSlug}" \u3068\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\`} required>
|
|
1146
|
-
<Input placeholder={projectSlug} />
|
|
1147
|
-
</FormField>
|
|
1148
|
-
<Separator />
|
|
1149
|
-
<Flex gap="small" justify="end">
|
|
1150
|
-
<Button variant="ghost" type="button">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
|
|
1151
|
-
<Button type="submit" variant="destructive">\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u5B8C\u5168\u306B\u524A\u9664</Button>
|
|
1152
|
-
</Flex>
|
|
1153
|
-
</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>
|
|
1154
1383
|
</Card>
|
|
1155
|
-
)
|
|
1384
|
+
);
|
|
1156
1385
|
}`
|
|
1157
1386
|
},
|
|
1158
1387
|
{
|
|
1159
|
-
name: "
|
|
1160
|
-
tagline: "
|
|
1161
|
-
tags: ["
|
|
1162
|
-
code: `import {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
} from "@godxjp/ui"
|
|
1166
|
-
import {
|
|
1167
|
-
import {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
{
|
|
1179
|
-
label: "Admin",
|
|
1180
|
-
items: [
|
|
1181
|
-
{ id: "settings", label: "Settings", icon: Settings },
|
|
1182
|
-
],
|
|
1183
|
-
},
|
|
1184
|
-
]
|
|
1388
|
+
name: "inertia-list-page",
|
|
1389
|
+
tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable + StatusBadge + Pagination (current primitive API).",
|
|
1390
|
+
tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
|
|
1391
|
+
code: `import { Head, router } from "@inertiajs/react"
|
|
1392
|
+
import { useMemo, useState } from "react"
|
|
1393
|
+
import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
|
|
1394
|
+
import { Card, CardContent, CardStat, DataTable, EmptyState, StatusBadge, type ColumnDef } from "@godxjp/ui/data-display"
|
|
1395
|
+
import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
|
|
1396
|
+
import { FilterBar, FilterGroup, Pagination } from "@godxjp/ui/navigation"
|
|
1397
|
+
import { formatDate } from "@godxjp/ui/datetime"
|
|
1398
|
+
import { withCrmLayout } from "@/layouts/crm-layout" // see "inertia-persistent-layout"
|
|
1399
|
+
|
|
1400
|
+
type Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }
|
|
1401
|
+
const PAGE_SIZE = 10
|
|
1402
|
+
|
|
1403
|
+
function Coupons({ coupons }: { coupons: Coupon[] }) {
|
|
1404
|
+
const [q, setQ] = useState("")
|
|
1405
|
+
const [status, setStatus] = useState("all")
|
|
1406
|
+
const [page, setPage] = useState(1)
|
|
1185
1407
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1408
|
+
const filtered = useMemo(() => coupons.filter((c) => {
|
|
1409
|
+
if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false
|
|
1410
|
+
if (status !== "all" && c.status !== status) return false
|
|
1411
|
+
return true
|
|
1412
|
+
}), [coupons, q, status])
|
|
1413
|
+
const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
|
1414
|
+
|
|
1415
|
+
// ColumnDef = { key, header, render?, align?: "left"|"center"|"right", sortable?, width? }
|
|
1416
|
+
const columns: ColumnDef<Coupon>[] = [
|
|
1417
|
+
{ key: "name", header: "\u30AF\u30FC\u30DD\u30F3\u540D", render: (c) => <span className="font-medium">{c.name}</span> },
|
|
1418
|
+
{ key: "scope", header: "\u30B9\u30B3\u30FC\u30D7", render: (c) => <StatusBadge status={c.scope} tone="info" icon={null} /> },
|
|
1419
|
+
{ key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (c) => <StatusBadge status={c.status} /> },
|
|
1420
|
+
{ key: "valid", header: "\u6709\u52B9\u671F\u9593", render: (c) => \`\${formatDate(c.validFrom)} \u301C \${formatDate(c.validTo)}\` },
|
|
1421
|
+
{ key: "usage", header: "\u5229\u7528\u6570", align: "right", render: (c) => c.usage.toLocaleString() },
|
|
1422
|
+
]
|
|
1188
1423
|
|
|
1189
1424
|
return (
|
|
1190
|
-
|
|
1191
|
-
<
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1425
|
+
<>
|
|
1426
|
+
<Head title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" />
|
|
1427
|
+
{/* RULE: every page wraps in PageContainer; spacing via Stack/ResponsiveGrid, never p-*/gap-* */}
|
|
1428
|
+
<PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
|
|
1429
|
+
<Stack gap="lg">
|
|
1430
|
+
<ResponsiveGrid columns={3}>
|
|
1431
|
+
<CardStat label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
|
|
1432
|
+
<CardStat label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
|
|
1433
|
+
<CardStat label="\u4EF6\u6570" value={coupons.length} />
|
|
1434
|
+
</ResponsiveGrid>
|
|
1435
|
+
|
|
1436
|
+
<FilterBar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
|
|
1437
|
+
{/* SearchInput is value + onSearch(v) \u2014 NOT onChange */}
|
|
1438
|
+
<SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />
|
|
1439
|
+
<FilterGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
|
|
1440
|
+
<Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>
|
|
1441
|
+
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
1442
|
+
<SelectContent>
|
|
1443
|
+
<SelectItem value="all">\u5168\u30B9\u30C6\u30FC\u30BF\u30B9</SelectItem>
|
|
1444
|
+
<SelectItem value="\u516C\u958B\u4E2D">\u516C\u958B\u4E2D</SelectItem>
|
|
1445
|
+
<SelectItem value="\u4E0B\u66F8\u304D">\u4E0B\u66F8\u304D</SelectItem>
|
|
1446
|
+
</SelectContent>
|
|
1447
|
+
</Select>
|
|
1448
|
+
</FilterGroup>
|
|
1449
|
+
</FilterBar>
|
|
1450
|
+
|
|
1451
|
+
<Card>
|
|
1452
|
+
<CardContent flush>
|
|
1453
|
+
{filtered.length === 0
|
|
1454
|
+
? <EmptyState title="\u8A72\u5F53\u3059\u308B\u30AF\u30FC\u30DD\u30F3\u304C\u3042\u308A\u307E\u305B\u3093" description="\u691C\u7D22\u6761\u4EF6\u3092\u5909\u66F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002" />
|
|
1455
|
+
: <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(\`/coupons/\${c.id}\`)} />}
|
|
1456
|
+
</CardContent>
|
|
1457
|
+
</Card>
|
|
1458
|
+
|
|
1459
|
+
{filtered.length > PAGE_SIZE && (
|
|
1460
|
+
<Pagination current={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onChange={(p) => setPage(p)} />
|
|
1461
|
+
)}
|
|
1462
|
+
</Stack>
|
|
1463
|
+
</PageContainer>
|
|
1464
|
+
</>
|
|
1205
1465
|
)
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
Coupons.layout = withCrmLayout
|
|
1469
|
+
export default Coupons`
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
name: "inertia-detail-page",
|
|
1473
|
+
tagline: "Inertia detail page \u2014 receives {id} prop, KeyValueGrid (compound) + CardStat + EmptyState fallback.",
|
|
1474
|
+
tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
|
|
1475
|
+
code: `import { Head, router } from "@inertiajs/react"
|
|
1476
|
+
import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
|
|
1477
|
+
import { Card, CardContent, CardStat, EmptyState, KeyValueGrid, StatusBadge } from "@godxjp/ui/data-display"
|
|
1478
|
+
import { Button } from "@godxjp/ui/general"
|
|
1479
|
+
import { formatDate } from "@godxjp/ui/datetime"
|
|
1480
|
+
import { ArrowLeft } from "lucide-react"
|
|
1481
|
+
import { withCrmLayout } from "@/layouts/crm-layout"
|
|
1482
|
+
|
|
1483
|
+
// Detail routes pass the param as an Inertia prop:
|
|
1484
|
+
// Route::get('/members/{id}', fn ($id) => Inertia::render('crm/members/show', ['id' => $id]))
|
|
1485
|
+
function MemberShow({ id }: { id: string }) {
|
|
1486
|
+
const member = MEMBERS.find((m) => m.id === id)
|
|
1487
|
+
|
|
1488
|
+
if (!member) {
|
|
1489
|
+
return (
|
|
1490
|
+
<>
|
|
1491
|
+
<Head title="\u4F1A\u54E1\u8A73\u7D30" />
|
|
1492
|
+
<PageContainer title="\u4F1A\u54E1\u8A73\u7D30" subtitle="\u4F1A\u54E1\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093">
|
|
1493
|
+
<EmptyState title="\u4F1A\u54E1\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" description={\`ID\u300C\${id}\u300D\u306F\u5B58\u5728\u3057\u307E\u305B\u3093\u3002\`} />
|
|
1494
|
+
<Button variant="outline" onClick={() => router.visit("/members")}><ArrowLeft className="size-4" />\u4E00\u89A7\u3078\u623B\u308B</Button>
|
|
1495
|
+
</PageContainer>
|
|
1496
|
+
</>
|
|
1497
|
+
)
|
|
1498
|
+
}
|
|
1214
1499
|
|
|
1215
|
-
export function EmployeeFilter({ onChange }: { onChange: (v: any) => void }) {
|
|
1216
1500
|
return (
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1501
|
+
<>
|
|
1502
|
+
<Head title={member.name} />
|
|
1503
|
+
<PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
|
|
1504
|
+
<Stack gap="lg">
|
|
1505
|
+
<ResponsiveGrid columns={4}>
|
|
1506
|
+
<CardStat label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
|
|
1507
|
+
<CardStat label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
|
|
1508
|
+
<CardStat label="\u30DD\u30A4\u30F3\u30C8" value={member.points.toLocaleString()} />
|
|
1509
|
+
<CardStat label="LTV" value={\`\xA5\${member.ltv.toLocaleString()}\`} />
|
|
1510
|
+
</ResponsiveGrid>
|
|
1511
|
+
<Card>
|
|
1512
|
+
<CardContent>
|
|
1513
|
+
{/* KeyValueGrid is COMPOUND \u2014 value goes in children, not a prop */}
|
|
1514
|
+
<KeyValueGrid columns={2}>
|
|
1515
|
+
<KeyValueGrid.Item label="\u6C0F\u540D">{member.name}</KeyValueGrid.Item>
|
|
1516
|
+
<KeyValueGrid.Item label="\u30E9\u30F3\u30AF"><StatusBadge status={member.rank} tone="info" icon={null} /></KeyValueGrid.Item>
|
|
1517
|
+
<KeyValueGrid.Item label="\u30B9\u30C6\u30FC\u30BF\u30B9"><StatusBadge status={member.status} /></KeyValueGrid.Item>
|
|
1518
|
+
<KeyValueGrid.Item label="\u767B\u9332\u65E5">{formatDate(member.registeredAt)}</KeyValueGrid.Item>
|
|
1519
|
+
</KeyValueGrid>
|
|
1520
|
+
</CardContent>
|
|
1521
|
+
</Card>
|
|
1522
|
+
</Stack>
|
|
1523
|
+
</PageContainer>
|
|
1524
|
+
</>
|
|
1238
1525
|
)
|
|
1239
|
-
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
MemberShow.layout = withCrmLayout
|
|
1529
|
+
export default MemberShow`
|
|
1240
1530
|
},
|
|
1241
1531
|
{
|
|
1242
|
-
name: "
|
|
1243
|
-
tagline: "
|
|
1244
|
-
tags: ["
|
|
1245
|
-
code:
|
|
1246
|
-
import {
|
|
1247
|
-
|
|
1248
|
-
} from "
|
|
1249
|
-
|
|
1250
|
-
export function ProfileEditor() {
|
|
1251
|
-
const [submitting, setSubmitting] = useState(false)
|
|
1252
|
-
const [initialFetched, setInitialFetched] = useState(false)
|
|
1253
|
-
const [values, setValues] = useState({ name: "", bio: "" })
|
|
1254
|
-
|
|
1255
|
-
// Initial fetch
|
|
1256
|
-
useEffect(() => {
|
|
1257
|
-
fetch("/api/me").then(r => r.json()).then(data => {
|
|
1258
|
-
setValues(data)
|
|
1259
|
-
setInitialFetched(true)
|
|
1260
|
-
})
|
|
1261
|
-
}, [])
|
|
1532
|
+
name: "inertia-persistent-layout",
|
|
1533
|
+
tagline: "Inertia persistent layout (AppShell+Sidebar) \u2014 the array-form gotcha + the SSR/Math.random gotcha.",
|
|
1534
|
+
tags: ["inertia", "layout", "appshell", "sidebar", "ssr", "hydration", "gotcha"],
|
|
1535
|
+
code: `// resources/js/layouts/crm-layout.tsx
|
|
1536
|
+
import { router, usePage } from "@inertiajs/react"
|
|
1537
|
+
import { AppShell, Sidebar } from "@godxjp/ui/layout"
|
|
1538
|
+
import { LayoutDashboard } from "lucide-react"
|
|
1539
|
+
import type { ReactNode } from "react"
|
|
1262
1540
|
|
|
1541
|
+
export function CrmLayout({ children }: { children: ReactNode }) {
|
|
1542
|
+
const { url } = usePage()
|
|
1543
|
+
const sections = [{ label: "\u30E1\u30A4\u30F3", items: [{ id: "/dashboard", label: "\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9", icon: LayoutDashboard }] }]
|
|
1263
1544
|
return (
|
|
1264
|
-
<
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
defaultValues={values}
|
|
1268
|
-
onSubmit={async (v) => {
|
|
1269
|
-
setSubmitting(true)
|
|
1270
|
-
await fetch("/api/me", { method: "PUT", body: JSON.stringify(v) })
|
|
1271
|
-
setSubmitting(false)
|
|
1272
|
-
}}
|
|
1273
|
-
>
|
|
1274
|
-
<FormField name="name" label="\u6C0F\u540D" required><Input /></FormField>
|
|
1275
|
-
<FormField name="bio" label="\u81EA\u5DF1\u7D39\u4ECB"><Textarea rows={3} /></FormField>
|
|
1276
|
-
<Separator />
|
|
1277
|
-
<Flex gap="small" justify="end">
|
|
1278
|
-
<Button variant="ghost" type="button" disabled={submitting}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
|
|
1279
|
-
<Button type="submit" variant="primary" loading={submitting}>\u4FDD\u5B58</Button>
|
|
1280
|
-
</Flex>
|
|
1281
|
-
</Form>
|
|
1282
|
-
</Card>
|
|
1545
|
+
<AppShell sidebar={<Sidebar activeId={url} onSelect={(id) => router.visit(id)} sections={sections} product={{ name: "JOVY CRM" }} />}>
|
|
1546
|
+
{children}
|
|
1547
|
+
</AppShell>
|
|
1283
1548
|
)
|
|
1284
|
-
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// \u26A0\uFE0F GOTCHA 1 \u2014 persistent layout MUST be the ARRAY form.
|
|
1552
|
+
// A render fn \`(page) => <CrmLayout>{page}</CrmLayout>\` is indistinguishable from a
|
|
1553
|
+
// component; Inertia React calls it with the page-PROPS object and renders that
|
|
1554
|
+
// object as a child \u2192 "Objects are not valid as a React child {errors, auth, \u2026}".
|
|
1555
|
+
export const withCrmLayout = [CrmLayout] // \u2705 array \u2192 Inertia passes the page as children
|
|
1556
|
+
// page usage: Dashboard.layout = withCrmLayout
|
|
1557
|
+
|
|
1558
|
+
// \u26A0\uFE0F GOTCHA 2 \u2014 Inertia v3 SSRs even in \`npm run dev\`. NEVER call Math.random() or
|
|
1559
|
+
// argless new Date() during render (e.g. fabricating chart/demo numbers) \u2192 React
|
|
1560
|
+
// hydration mismatch ("server rendered text didn't match the client"). Seed
|
|
1561
|
+
// deterministically by index, or compute inside an event handler:
|
|
1562
|
+
const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; return x - Math.floor(x) }`
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
name: "status-badge-coloring",
|
|
1566
|
+
tagline: "Colour a StatusBadge for localized labels and tiers via tone + icon (escape-hatch props, @godxjp/ui \u2265 6.1).",
|
|
1567
|
+
tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
|
|
1568
|
+
code: `import { StatusBadge } from "@godxjp/ui/data-display"
|
|
1569
|
+
|
|
1570
|
+
// StatusBadge auto-colours a fixed set of English LIFECYCLE keys:
|
|
1571
|
+
// active/completed (success \u2713) \xB7 draft (neutral \u25CB) \xB7 pending/temporary (warning \u23F1)
|
|
1572
|
+
// scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
|
|
1573
|
+
// Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
|
|
1574
|
+
|
|
1575
|
+
// 1) Lifecycle with localized text \u2014 map to the key, keep JP via \`label\` (icon stays):
|
|
1576
|
+
<StatusBadge status="active" label="\u516C\u958B\u4E2D" /> // green \u2713 \u516C\u958B\u4E2D
|
|
1577
|
+
|
|
1578
|
+
// 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
|
|
1579
|
+
<StatusBadge status="\u516C\u958B\u4E2D" tone="success" />
|
|
1580
|
+
|
|
1581
|
+
// 3) Tier / category \u2014 coloured pill, drop the misleading glyph with icon={null}:
|
|
1582
|
+
<StatusBadge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} />
|
|
1583
|
+
<StatusBadge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
|
|
1584
|
+
<StatusBadge status="\u6CD5\u4EBA\u5171\u901A" tone="info" icon={null} />
|
|
1585
|
+
|
|
1586
|
+
// tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type StatusBadgeTone)
|
|
1587
|
+
// RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
|
|
1588
|
+
// narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
|
|
1589
|
+
// import that instead of the raw StatusBadge across pages.`
|
|
1285
1590
|
}
|
|
1286
1591
|
];
|
|
1287
1592
|
function findPattern(name) {
|
|
@@ -2617,10 +2922,10 @@ var TOOL_DEFINITIONS = [
|
|
|
2617
2922
|
},
|
|
2618
2923
|
{
|
|
2619
2924
|
name: "get_rule",
|
|
2620
|
-
description: "Read one cardinal rule from CLAUDE.md (
|
|
2925
|
+
description: "Read one cardinal rule from CLAUDE.md (by number) OR all if no number.",
|
|
2621
2926
|
inputSchema: {
|
|
2622
2927
|
type: "object",
|
|
2623
|
-
properties: { number: { type: "number", description: "Rule number 1-
|
|
2928
|
+
properties: { number: { type: "number", description: "Rule number (1-N)." } }
|
|
2624
2929
|
}
|
|
2625
2930
|
},
|
|
2626
2931
|
{
|