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