@godxjp/ui-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3317 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ListToolsRequestSchema,
10
+ ReadResourceRequestSchema
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+
13
+ // src/data/components.ts
14
+ var COMPONENTS = [
15
+ // ─── general ────────────────────────────────────────────────────
16
+ {
17
+ name: "Button",
18
+ group: "general",
19
+ tagline: "Canonical action primitive. 6 variants \xD7 4 sizes. Loading state, icon slots, Slot-based asChild for routing wrap.",
20
+ props: [
21
+ { name: "variant", type: '"primary" | "secondary" | "outline" | "ghost" | "destructive" | "link"', description: "Visual treatment.", defaultValue: '"primary"' },
22
+ { name: "size", type: "SizeWithXSProp", description: '"x-small" | "small" | "default" | "large".', defaultValue: '"default"' },
23
+ { name: "block", type: "boolean", description: "Stretch to fill parent width (mobile submit pattern)." },
24
+ { name: "loading", type: "boolean", description: "Show spinner + disable interaction." },
25
+ { name: "startContent", type: "ReactNode", description: "Left icon slot." },
26
+ { name: "endContent", type: "ReactNode", description: "Right icon slot." },
27
+ { name: "asChild", type: "boolean", description: "Radix Slot \u2014 wrap a Link / RouterLink without nesting." }
28
+ ],
29
+ example: `<Button variant="primary" size="default" startContent={<Save size={14} />}>
30
+ \u4FDD\u5B58
31
+ </Button>
32
+
33
+ <Button variant="ghost" asChild>
34
+ <Link to="/settings">\u8A2D\u5B9A</Link>
35
+ </Button>`,
36
+ docPath: "data-entry/Button.md",
37
+ storyPath: "general/Button.stories.tsx",
38
+ rules: [14, 23]
39
+ },
40
+ {
41
+ name: "Typography",
42
+ group: "general",
43
+ tagline: "Headings / paragraph / text / link primitives. `Typography.Title size={1..5}`, `Typography.Text`, `Typography.Paragraph`, `Typography.Link`.",
44
+ props: [
45
+ { name: "size (Title)", type: "1 | 2 | 3 | 4 | 5", description: "Heading level (h1\u2013h5)." },
46
+ { name: "color", type: "TypographyColor", description: "default | secondary | success | warning | attention | info | destructive." },
47
+ { name: "strong", type: "boolean", description: "Bold weight." },
48
+ { name: "truncate", type: '"ellipsis" | { lines: number }', description: "Single-line or multi-line truncation." }
49
+ ],
50
+ example: `<Typography.Title size={2}>\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u8A2D\u5B9A</Typography.Title>
51
+ <Typography.Paragraph>\u5909\u66F4\u306F\u5373\u5EA7\u306B\u53CD\u6620\u3055\u308C\u307E\u3059\u3002</Typography.Paragraph>
52
+ <Typography.Text color="secondary">\u66F4\u65B0\u65E5: 2026-05-19</Typography.Text>`,
53
+ storyPath: "general/Typography.stories.tsx",
54
+ rules: [23]
55
+ },
56
+ // ─── layout ─────────────────────────────────────────────────────
57
+ {
58
+ name: "Flex",
59
+ group: "layout",
60
+ tagline: "Flexbox container with prop-driven config (Ant-style).",
61
+ props: [
62
+ { name: "vertical", type: "boolean", description: "Column instead of row." },
63
+ { name: "gap", type: "FlexGap", description: "number | small | default | large." },
64
+ { name: "justify", type: "FlexJustify", description: "start | end | center | space-between | space-around | space-evenly." },
65
+ { name: "align", type: "AlignProp", description: "start | end | center | stretch | baseline." },
66
+ { name: "wrap", type: "boolean", description: "Allow wrapping." }
67
+ ],
68
+ example: `<Flex vertical gap="default" align="stretch">
69
+ <Input placeholder="\u540D\u524D" />
70
+ <Input placeholder="\u30E1\u30FC\u30EB" />
71
+ <Button type="submit">\u9001\u4FE1</Button>
72
+ </Flex>`,
73
+ storyPath: "layout/Flex.stories.tsx",
74
+ rules: [23, 24]
75
+ },
76
+ {
77
+ name: "Grid",
78
+ group: "layout",
79
+ tagline: "CSS Grid container. `cols` number or template string. Mobile-first gap.",
80
+ props: [
81
+ { name: "cols", type: "number | string", description: "Equal columns OR full grid-template-columns string." },
82
+ { name: "gap", type: "GridGap", description: "number | small | default | large." }
83
+ ],
84
+ example: `<Grid cols={3} gap="default">
85
+ <Card title="\u58F2\u4E0A">\xA5120,000</Card>
86
+ <Card title="\u5229\u76CA">\xA545,000</Card>
87
+ <Card title="\u9867\u5BA2">+12</Card>
88
+ </Grid>`,
89
+ storyPath: "layout/Grid.stories.tsx",
90
+ rules: [23, 24]
91
+ },
92
+ {
93
+ name: "Row / Col",
94
+ group: "layout",
95
+ tagline: "24-column responsive grid (Ant-style). `gutter` rebound per breakpoint.",
96
+ props: [
97
+ { name: "gutter", type: "number | [h,v] | { xs, sm, md, lg, xl, xxl }", description: "Pixel gap, optionally per breakpoint." },
98
+ { name: "xs / sm / md / lg / xl", type: "number", description: "Column span at that breakpoint (1-24)." }
99
+ ],
100
+ example: `<Row gutter={{ xs: 8, md: 16 }}>
101
+ <Col xs={24} md={8}><Card>One</Card></Col>
102
+ <Col xs={24} md={8}><Card>Two</Card></Col>
103
+ <Col xs={24} md={8}><Card>Three</Card></Col>
104
+ </Row>`,
105
+ storyPath: "layout/Grid.stories.tsx",
106
+ rules: [23, 24]
107
+ },
108
+ // ─── data-display ───────────────────────────────────────────────
109
+ {
110
+ name: "Card",
111
+ group: "data-display",
112
+ tagline: "Surface container. Title + subtitle + body + footer slots, accent / band decorations, padding scale, hoverable.",
113
+ props: [
114
+ { name: "title", type: "ReactNode", description: "Header title." },
115
+ { name: "subtitle", type: "ReactNode", description: "Sub-text under title." },
116
+ { name: "kicker", type: "ReactNode", description: "Small uppercase kicker above title." },
117
+ { name: "extra", type: "ReactNode", description: "Right-aligned action slot in header." },
118
+ { name: "padding", type: "PaddingProp", description: "tight | default | cozy | none." },
119
+ { name: "tone", type: "CardTone", description: "default | muted | outline-only." },
120
+ { name: "accent", type: "CardAccent", description: "Edge accent + 'featured' ring." },
121
+ { name: "band", type: "CardBand", description: "Top-edge color strip." },
122
+ { name: "actions", type: "ReactNode", description: "Right-aligned footer action bar." },
123
+ { name: "hoverable", type: "boolean", description: "Hover lift + border tint." }
124
+ ],
125
+ example: `<Card
126
+ title="\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB"
127
+ subtitle="\u4ED6\u306E\u30E1\u30F3\u30D0\u30FC\u306B\u516C\u958B\u3055\u308C\u308B\u60C5\u5831\u3067\u3059\u3002"
128
+ actions={
129
+ <>
130
+ <Button variant="ghost">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
131
+ <Button variant="primary">\u4FDD\u5B58</Button>
132
+ </>
133
+ }
134
+ >
135
+ {/* form fields */}
136
+ </Card>`,
137
+ docPath: "data-display/Card.md",
138
+ storyPath: "data-display/Card.stories.tsx",
139
+ rules: [22, 23]
140
+ },
141
+ {
142
+ name: "Table",
143
+ group: "data-display",
144
+ tagline: "Slim primitive. Columns \xD7 rows + interaction features per cell/row (sort, resize, expand, edit, tree, group, sticky). NO chrome \u2014 use DataTable composite for toolbar/views/batch/pagination.",
145
+ props: [
146
+ { name: "columns", type: "TableColumn<T>[]", required: true, description: "TanStack ColumnDef array. `meta.sticky` for responsive sticky columns." },
147
+ { name: "data", type: "T[]", required: true, description: "Row data." },
148
+ { name: "rowKey", type: "string | (row) => string", required: true, description: "Stable row identity." },
149
+ { name: "density", type: "DensityProp", description: "compact | default | comfortable." },
150
+ { name: "bordered", type: "boolean", description: "Inner border lines." },
151
+ { name: "selection", type: "TableSelectionConfig", description: "Checkbox column + selected row keys." },
152
+ { name: "sortable", type: "TableSortState | boolean", description: "Controlled multi-sort." },
153
+ { name: "expandable / tree / groupBy / editing / sticky", type: "configs", description: "Optional features." }
154
+ ],
155
+ example: `<Table
156
+ columns={[
157
+ { accessorKey: "name", header: "\u6C0F\u540D", meta: { sticky: { side: "left", from: "md" } } },
158
+ { accessorKey: "role", header: "\u5F79\u8077" },
159
+ { accessorKey: "status", header: "\u72B6\u614B",
160
+ cell: ({ row }) => row.original.status === "active"
161
+ ? <Badge variant="success" dot>\u7A3C\u50CD\u4E2D</Badge>
162
+ : <Badge variant="neutral" dot>\u4F11\u8077</Badge>
163
+ },
164
+ ]}
165
+ data={employees}
166
+ rowKey="id"
167
+ />`,
168
+ docPath: "data-display/Table.md",
169
+ storyPath: "data-display/Table.stories.tsx",
170
+ rules: [22, 23, 34]
171
+ },
172
+ {
173
+ name: "Badge",
174
+ group: "data-display",
175
+ tagline: "Status pill (numeric or short word). Semantic color via `variant`, visual treatment via `appearance`.",
176
+ props: [
177
+ { name: "variant", type: '"primary" | "success" | "warning" | "info" | "destructive" | "attention" | "neutral" | "outline"', description: "Semantic color slot. Use `destructive` (NOT `error` \u2014 Tag migration v5.0)." },
178
+ { name: "appearance", type: '"soft" | "solid" | "outline"', description: "Visual treatment.", defaultValue: '"soft"' },
179
+ { name: "dot", type: "boolean", description: "Show colored dot before label.", defaultValue: "true" }
180
+ ],
181
+ example: `<Badge variant="success" dot>\u627F\u8A8D\u6E08</Badge>
182
+ <Badge variant="destructive" appearance="solid">\u5374\u4E0B</Badge>
183
+ <Badge variant="neutral" dot={false}>\u4E0B\u66F8\u304D</Badge>`,
184
+ docPath: "data-display/Badge.md",
185
+ storyPath: "data-display/Badge.stories.tsx",
186
+ rules: [23]
187
+ },
188
+ {
189
+ name: "Tag",
190
+ group: "data-display",
191
+ tagline: "Label chip (closable, removable). Distinct from Badge \u2014 multiple in a row, often deletable.",
192
+ props: [
193
+ { name: "color", type: "TagPresetColor | string", description: "Preset hue OR any CSS color." },
194
+ { name: "bordered", type: "boolean", description: "Show outline.", defaultValue: "true" },
195
+ { name: "closable", type: "boolean", description: "Render \xD7 button." },
196
+ { name: "onClose", type: "(e) => void", description: "Called when \xD7 clicked." },
197
+ { name: "icon", type: "ReactNode", description: "Leading icon." }
198
+ ],
199
+ example: `<Tag color="primary" closable onClose={() => removeTag(id)}>
200
+ React
201
+ </Tag>`,
202
+ docPath: "data-display/Tag.md",
203
+ storyPath: "data-display/Tag.stories.tsx",
204
+ rules: [23]
205
+ },
206
+ {
207
+ name: "Avatar",
208
+ group: "data-display",
209
+ tagline: "Profile picture with initials fallback. Circle / square. Size = token or pixel number.",
210
+ props: [
211
+ { name: "alt", type: "string", description: "Display name \u2014 first 2 initials are the fallback." },
212
+ { name: "size", type: '"xs" | "sm" | "default" | "lg" | "xl" | number', description: "Token or pixel size." },
213
+ { name: "shape", type: '"circle" | "square"', description: "Geometric form.", defaultValue: '"circle"' },
214
+ { name: "src", type: "string", description: "Image URL. Falls back to initials on load error." }
215
+ ],
216
+ example: `<Avatar size="lg" alt="\u5C71\u7530 \u592A\u90CE" />
217
+ <Avatar size={56} alt="Hanako" src="/avatar.jpg" />`,
218
+ storyPath: "data-display/Avatar.stories.tsx",
219
+ rules: [23]
220
+ },
221
+ {
222
+ name: "Separator",
223
+ group: "data-display",
224
+ tagline: "Horizontal or vertical divider. Radix-backed for ARIA semantics.",
225
+ props: [
226
+ { name: "orientation", type: "OrientationProp", description: "horizontal | vertical." },
227
+ { name: "decorative", type: "boolean", description: "If true, role=none (not announced)." }
228
+ ],
229
+ example: `<Separator />
230
+ <Separator orientation="vertical" />`,
231
+ storyPath: "data-display/Separator.stories.tsx",
232
+ rules: [3, 23]
233
+ },
234
+ // ─── data-entry ─────────────────────────────────────────────────
235
+ {
236
+ name: "Input",
237
+ group: "data-entry",
238
+ tagline: "Text input. `prefix` / `suffix` slots, `status` for validation, `addonBefore` / `addonAfter` for connected groups.",
239
+ props: [
240
+ { name: "value / onChange", type: "string / (e) => void", description: "Controlled value (or use `defaultValue`)." },
241
+ { name: "size", type: "InputSize (= SizeProp)", description: "small | default | large." },
242
+ { name: "status", type: "InputStatus (= StatusProp)", description: "default | success | warning | error. Use `error` for form validation." },
243
+ { name: "prefix / suffix", type: "ReactNode", description: "Icon slot inside the input box." },
244
+ { name: "addonBefore / addonAfter", type: "ReactNode", description: "Connected group (e.g. URL prefix, currency)." }
245
+ ],
246
+ example: `<Input
247
+ placeholder="https://"
248
+ prefix={<Globe size={14} />}
249
+ addonBefore="https://"
250
+ addonAfter=".com"
251
+ />`,
252
+ docPath: "data-entry/Input.md",
253
+ storyPath: "data-entry/Input.stories.tsx",
254
+ rules: [14, 23]
255
+ },
256
+ {
257
+ name: "InputNumber",
258
+ group: "data-entry",
259
+ tagline: "Numeric input with stepper. `min` / `max` / `step`, `precision`, `formatter` / `parser` for currency.",
260
+ props: [
261
+ { name: "value / onValueChange", type: "number | null / (n) => void", description: "Radix-style controlled state." },
262
+ { name: "min / max / step", type: "number", description: "Numeric bounds + increment." },
263
+ { name: "precision", type: "number", description: "Decimal places." },
264
+ { name: "formatter / parser", type: "(value) => string / string", description: "Custom display (currency, units)." }
265
+ ],
266
+ example: `<InputNumber
267
+ defaultValue={1000}
268
+ min={0} max={10000} step={100}
269
+ formatter={(v) => \`\xA5\${v.toLocaleString()}\`}
270
+ parser={(v) => Number(v.replace(/[^0-9]/g, ""))}
271
+ />`,
272
+ storyPath: "data-entry/InputNumber.stories.tsx",
273
+ rules: [23]
274
+ },
275
+ {
276
+ name: "Select",
277
+ group: "data-entry",
278
+ tagline: "Single-select dropdown (Radix). `options` array. `searchable` for cmdk filter.",
279
+ props: [
280
+ { name: "options", type: "Array<{ value, label, disabled? }> | OptionGroup[]", required: true, description: "Option list, optionally grouped." },
281
+ { name: "value / onValueChange", type: "string / (v) => void", description: "Controlled selection." },
282
+ { name: "size", type: "InputSize", description: "small | default | large." },
283
+ { name: "searchable", type: "boolean", description: "cmdk-powered keyboard search." },
284
+ { name: "loading", type: "boolean", description: "Show loading row (searchable mode)." }
285
+ ],
286
+ example: `<Select
287
+ options={[
288
+ { value: "ja", label: "\u65E5\u672C\u8A9E" },
289
+ { value: "en-US", label: "English" },
290
+ ]}
291
+ value={locale}
292
+ onValueChange={setLocale}
293
+ />`,
294
+ storyPath: "data-entry/Select.stories.tsx",
295
+ rules: [3, 23]
296
+ },
297
+ {
298
+ name: "Form",
299
+ group: "data-entry",
300
+ tagline: "react-hook-form + zod wrapper. `defaultValues` + `resolver` + `onSubmit` \u2014 children = inline `<FormField>` per row.",
301
+ props: [
302
+ { name: "form", type: "UseFormReturn<T>", description: "Pass `useForm()` return for external control." },
303
+ { name: "defaultValues / resolver / mode", type: "configs", description: "Shorthand for internal useForm." },
304
+ { name: "onSubmit", type: "(values: T) => void | Promise<void>", description: "Submit handler (handleSubmit-wrapped)." },
305
+ { name: "layout", type: "FormLayout", description: "vertical (default) | horizontal | inline. Mobile-first." },
306
+ { name: "loading", type: "LoadingProp", description: "Cascade loading to every FormField \u2014 true=spinner, {kind:'skeleton'}=skeleton." }
307
+ ],
308
+ example: `<Form<RegistrationValues>
309
+ resolver={zodResolver(registrationSchema)}
310
+ defaultValues={{ name: "", email: "", age: 18, agree: true }}
311
+ onSubmit={(values) => api.register(values)}
312
+ >
313
+ <FormField name="name" label="\u6C0F\u540D" required>
314
+ <Input placeholder="\u5C71\u7530 \u592A\u90CE" />
315
+ </FormField>
316
+ <FormField name="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required>
317
+ <Input type="email" />
318
+ </FormField>
319
+ <Button type="submit" variant="primary">\u767B\u9332</Button>
320
+ </Form>`,
321
+ storyPath: "data-entry/Form.stories.tsx",
322
+ rules: [3, 23, 34]
323
+ },
324
+ {
325
+ name: "FormField",
326
+ group: "data-entry",
327
+ tagline: "Form-wired Field. Reads value/onChange/error from <Form> via useController, clones child with the right adapter (Input/Checkbox/Switch/Select/Slider/Cascader/TreeSelect/Transfer/etc.).",
328
+ props: [
329
+ { name: "name", type: "FieldPath<T>", required: true, description: "Schema field path." },
330
+ { name: "label / description / required / optional", type: "Field-style props", description: "Label + help + status badges." },
331
+ { name: "loading", type: "LoadingProp", description: "Per-field override of Form-level loading." },
332
+ { name: "children", type: "ReactElement", required: true, description: "Single data-entry primitive (Input/Select/Checkbox/...)." }
333
+ ],
334
+ example: `<FormField name="phone" label="\u96FB\u8A71\u756A\u53F7" required description="\u30CF\u30A4\u30D5\u30F3\u7121\u3057\u3067\u5165\u529B">
335
+ <Input placeholder="08012345678" />
336
+ </FormField>`,
337
+ storyPath: "data-entry/Form.stories.tsx",
338
+ rules: [23, 34]
339
+ },
340
+ {
341
+ name: "Checkbox",
342
+ group: "data-entry",
343
+ tagline: "Boolean toggle (Radix). Label as children. Use `CheckboxGroup` for multi-select.",
344
+ props: [
345
+ { name: "checked / onCheckedChange", type: "boolean / (next) => void", description: "Radix-style." },
346
+ { name: "children", type: "ReactNode", description: "Label text (clickable area extends to it)." }
347
+ ],
348
+ example: `<Checkbox checked={agree} onCheckedChange={setAgree}>
349
+ \u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B
350
+ </Checkbox>`,
351
+ storyPath: "data-entry/Checkbox.stories.tsx",
352
+ rules: [3, 23]
353
+ },
354
+ {
355
+ name: "Switch",
356
+ group: "data-entry",
357
+ tagline: "Boolean toggle, slider style (Radix). Use in settings / feature flags.",
358
+ props: [
359
+ { name: "checked / onCheckedChange", type: "boolean / (next) => void", description: "Radix-style." },
360
+ { name: "disabled", type: "boolean", description: "Lock state." }
361
+ ],
362
+ example: `<Switch checked={notifEnabled} onCheckedChange={setNotifEnabled} />`,
363
+ storyPath: "data-entry/Switch.stories.tsx",
364
+ rules: [3, 23]
365
+ },
366
+ {
367
+ name: "DatePicker / DateField / DateRangePicker / TimeField",
368
+ group: "data-entry",
369
+ tagline: "React Aria-backed date/time inputs. Locale-aware (via GodxConfigProvider), keyboard-complete, IME-friendly.",
370
+ props: [
371
+ { name: "value / onChange", type: "DateValue | TimeValue", description: "@internationalized/date values." },
372
+ { name: "label / description", type: "ReactNode", description: "Inline label." },
373
+ { name: "minValue / maxValue", type: "DateValue", description: "Bounds." }
374
+ ],
375
+ example: `<DateField label="\u751F\u5E74\u6708\u65E5" defaultValue={parseDate("2000-01-01")} />
376
+ <TimeField label="\u51FA\u52E4\u6642\u523B" defaultValue={new Time(9, 0)} />`,
377
+ storyPath: "data-entry/DatePicker.stories.tsx",
378
+ rules: [3, 14, 23]
379
+ },
380
+ // ─── feedback ───────────────────────────────────────────────────
381
+ {
382
+ name: "Alert",
383
+ group: "feedback",
384
+ tagline: "Inline notice banner. 5 semantic colors \xD7 outlined/banner variants.",
385
+ props: [
386
+ { name: "color", type: "FeedbackColorProp", description: "default | info | success | warning | destructive." },
387
+ { name: "variant", type: '"outlined" | "banner"', description: "Card style or full-width banner." },
388
+ { name: "title / description", type: "ReactNode", description: "Two-line stack." },
389
+ { name: "actions", type: "ReactNode", description: "Right-aligned action slot." },
390
+ { name: "closable / onClose", type: "boolean / fn", description: "\xD7 button." }
391
+ ],
392
+ example: `<Alert color="warning" title="3 \u4EF6\u306E\u6253\u523B\u6F0F\u308C\u304C\u3042\u308A\u307E\u3059"
393
+ description="\u672C\u65E5\u4E2D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
394
+ actions={<Button size="small">\u78BA\u8A8D\u3059\u308B</Button>}
395
+ closable onClose={dismiss}
396
+ />`,
397
+ storyPath: "feedback/Alert.stories.tsx",
398
+ rules: [23]
399
+ },
400
+ {
401
+ name: "Dialog / Modal",
402
+ group: "feedback",
403
+ tagline: "Radix Dialog wrapped with title/footer slots. `Modal` is convenience preset.",
404
+ props: [
405
+ { name: "open / onOpenChange", type: "boolean / (b) => void", description: "Radix-style." },
406
+ { name: "title / description / footer", type: "ReactNode", description: "Header / body / footer slots." },
407
+ { name: "size", type: "SizeProp", description: "Width preset." }
408
+ ],
409
+ example: `<Dialog open={open} onOpenChange={setOpen}
410
+ title="\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664"
411
+ footer={
412
+ <>
413
+ <Button variant="ghost" onClick={() => setOpen(false)}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
414
+ <Button variant="destructive" onClick={confirmDelete}>\u524A\u9664</Button>
415
+ </>
416
+ }
417
+ >
418
+ \u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002
419
+ </Dialog>`,
420
+ storyPath: "feedback/Dialog.stories.tsx",
421
+ rules: [3, 23]
422
+ },
423
+ {
424
+ name: "Sheet / Drawer",
425
+ group: "feedback",
426
+ tagline: "Side panel (Radix Dialog). 4 sides. Wider than Modal \u2014 for settings, filters, details.",
427
+ props: [
428
+ { name: "open / onOpenChange", type: "boolean / fn", description: "Radix-style." },
429
+ { name: "side", type: "SideProp", description: "top | right | bottom | left." },
430
+ { name: "title / description", type: "ReactNode", description: "Header." }
431
+ ],
432
+ example: `<Sheet open={open} onOpenChange={setOpen} side="right" title="\u30D5\u30A3\u30EB\u30BF\u30FC">
433
+ {/* filter form */}
434
+ </Sheet>`,
435
+ storyPath: "feedback/Drawer.stories.tsx",
436
+ rules: [3, 23]
437
+ },
438
+ {
439
+ name: "Toaster / toast",
440
+ group: "feedback",
441
+ tagline: "Sonner-backed toast notifications. Mount Toaster once in app root, call `toast.success(...)` anywhere.",
442
+ props: [
443
+ { name: "position", type: '"top-right" | "top-center" | "bottom-right" | ...', description: "Stack anchor." }
444
+ ],
445
+ example: `// App root
446
+ <GodxConfigProvider><App /><Toaster position="top-right" /></GodxConfigProvider>
447
+
448
+ // Anywhere
449
+ toast.success("\u4FDD\u5B58\u3057\u307E\u3057\u305F")
450
+ toast.error("\u901A\u4FE1\u30A8\u30E9\u30FC", { description: "\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044" })`,
451
+ storyPath: "providers/Toaster.stories.tsx",
452
+ rules: [14, 23]
453
+ },
454
+ {
455
+ name: "Skeleton",
456
+ group: "feedback",
457
+ tagline: "Loading placeholder block. Use during initial data fetch (NOT during save \u2014 that's a spinner).",
458
+ props: [
459
+ { name: "className", type: "string", description: "Set width/height via Tailwind (h-9, w-full, rounded-md)." }
460
+ ],
461
+ example: `<Skeleton className="h-9 w-full rounded-md" />
462
+ <Skeleton className="h-4 w-32 rounded-sm" />`,
463
+ storyPath: "feedback/Skeleton.stories.tsx",
464
+ rules: [23]
465
+ },
466
+ {
467
+ name: "Spinner",
468
+ group: "feedback",
469
+ tagline: "Small inline circular loader. Use for active work (saving, revalidating).",
470
+ props: [
471
+ { name: "size", type: "IconSizeProp", description: "sm | md | lg." },
472
+ { name: "tone", type: "SpinnerTone", description: "Semantic color." }
473
+ ],
474
+ example: `<Spinner size="sm" aria-label="\u8AAD\u307F\u8FBC\u307F\u4E2D" />`,
475
+ storyPath: "feedback/Spin.stories.tsx",
476
+ rules: [23]
477
+ },
478
+ // ─── navigation ─────────────────────────────────────────────────
479
+ {
480
+ name: "Tabs",
481
+ group: "navigation",
482
+ tagline: "Radix Tabs. Line or pills variant, horizontal/vertical, lazy mount.",
483
+ props: [
484
+ { name: "items", type: "Array<{ value, label, content, disabled? }>", required: true, description: "Tab list." },
485
+ { name: "value / onValueChange", type: "string / fn", description: "Controlled active tab." },
486
+ { name: "variant", type: '"line" | "pills"', description: "Visual." },
487
+ { name: "orientation", type: "OrientationProp", description: "horizontal | vertical." }
488
+ ],
489
+ example: `<Tabs items={[
490
+ { value: "profile", label: "\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB", content: <ProfileForm /> },
491
+ { value: "security", label: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3", content: <SecurityForm /> },
492
+ ]} />`,
493
+ storyPath: "navigation/Tabs.stories.tsx",
494
+ rules: [3, 23]
495
+ },
496
+ {
497
+ name: "Menu / DropdownMenu",
498
+ group: "navigation",
499
+ tagline: "Sidebar menu (sections + items + nested) / floating context menu (Radix DropdownMenu).",
500
+ props: [
501
+ { name: "items", type: "MenuItem[]", description: "Sections + nested children." },
502
+ { name: "activeId / onSelect", type: "string / fn", description: "Controlled selection." }
503
+ ],
504
+ example: `<Menu activeId={active} onSelect={setActive} items={[
505
+ { id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
506
+ { id: "settings", label: "Settings", icon: Settings, children: [
507
+ { id: "team", label: "Team" },
508
+ { id: "billing", label: "Billing" },
509
+ ]},
510
+ ]} />`,
511
+ storyPath: "navigation/Menu.stories.tsx",
512
+ rules: [3, 14, 23]
513
+ },
514
+ {
515
+ name: "Pagination",
516
+ group: "navigation",
517
+ tagline: "Page navigator. 3 variants \u2014 default (numbered) / simple (prev/next) / embedded (within toolbar). Justify matches FlexJustify.",
518
+ props: [
519
+ { name: "current / total / pageSize", type: "number", description: "Pagination state." },
520
+ { name: "onChange / onPageSizeChange", type: "fn", description: "Page or page size change." },
521
+ { name: "variant", type: '"default" | "simple" | "embedded"', description: "Feature mode." },
522
+ { name: "justify", type: "PaginationJustify", description: "start | center | end | space-between." },
523
+ { name: "showSizeChanger / showTotal / showFirstLast", type: "boolean", description: "Adjective toggles." }
524
+ ],
525
+ example: `<Pagination current={page} pageSize={20} total={1234}
526
+ onChange={(p) => setPage(p)} showSizeChanger />`,
527
+ storyPath: "navigation/Pagination.stories.tsx",
528
+ rules: [23]
529
+ },
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
+ {
562
+ name: "AppShell",
563
+ group: "shell",
564
+ tagline: "Root chrome layout. Grid: sidebar / topbar / main / footer. Slot-based \u2014 every service composes this.",
565
+ props: [
566
+ { name: "sidebar", type: "ReactNode", description: "<Sidebar> instance." },
567
+ { name: "topbar", type: "ReactNode", description: "<Topbar> + switchers." },
568
+ { name: "footer", type: "ReactNode", description: "Footer band." },
569
+ { name: "sidebarCollapsed", type: "boolean", description: "Collapse sidebar (icon-only mode)." }
570
+ ],
571
+ example: `<AppShell
572
+ sidebar={<Sidebar activeId={active} onSelect={setActive} sections={SECTIONS} />}
573
+ topbar={<Topbar product={product} project={project} onSearchOpen={...} />}
574
+ >
575
+ <PageContent title="Dashboard">\u2026</PageContent>
576
+ </AppShell>`,
577
+ docPath: "shell/AppShell.md",
578
+ storyPath: "shell/AppShell.stories.tsx",
579
+ rules: [19, 22, 23, 28]
580
+ },
581
+ {
582
+ name: "PageContent",
583
+ group: "shell",
584
+ tagline: "Main column inside AppShell. Title / subtitle / breadcrumb / extra slot + padded body.",
585
+ props: [
586
+ { name: "title / subtitle / breadcrumb / extra", type: "ReactNode", description: "Header slots." },
587
+ { name: "padding", type: "PaddingProp", description: "tight | default | cozy | none." }
588
+ ],
589
+ example: `<PageContent
590
+ title="Dashboard"
591
+ subtitle="Workspace activity"
592
+ extra={<Button variant="primary">New issue</Button>}
593
+ >
594
+ {/* page body */}
595
+ </PageContent>`,
596
+ storyPath: "shell/PageContent.stories.tsx",
597
+ rules: [22, 23]
598
+ },
599
+ {
600
+ name: "Sidebar",
601
+ group: "shell",
602
+ tagline: "Left nav rail. Sections + nested items + optional product chip + footer slot. Collapsible to icon-only.",
603
+ props: [
604
+ { name: "sections", type: "SidebarSection[]", required: true, description: "Section groups + items." },
605
+ { name: "activeId / onSelect", type: "string / fn", description: "Controlled active." },
606
+ { name: "collapsed", type: "boolean", description: "Icon-only mode." },
607
+ { name: "product", type: "ProductDescriptor", description: "Top product chip (with ProductSwitcher trigger)." },
608
+ { name: "brand", type: "ReactNode", description: "Override product with custom brand block." }
609
+ ],
610
+ example: `<Sidebar
611
+ activeId={active}
612
+ onSelect={setActive}
613
+ sections={[
614
+ { label: "Workspace", items: [{ id: "dash", label: "Dashboard", icon: LayoutDashboard }] },
615
+ ]}
616
+ product={ACTIVE_PRODUCT}
617
+ />`,
618
+ storyPath: "shell/Sidebar.stories.tsx",
619
+ rules: [22, 23, 27]
620
+ },
621
+ {
622
+ name: "CommandPalette",
623
+ group: "shell",
624
+ tagline: "\u2318K / Ctrl+K global command launcher (cmdk). Groups + items + keyboard shortcuts.",
625
+ props: [
626
+ { name: "commands", type: "CommandItem[]", required: true, description: "Groupable command list." },
627
+ { name: "open / onOpenChange", type: "boolean / fn", description: "Controlled (or use built-in \u2318K hotkey)." }
628
+ ],
629
+ example: `<CommandPalette open={open} onOpenChange={setOpen} commands={[
630
+ { id: "new-issue", label: "\u65B0\u898FIssue\u4F5C\u6210", shortcut: ["\u2318", "I"], onSelect: () => \u2026},
631
+ ]} />`,
632
+ storyPath: "shell/CommandPalette.stories.tsx",
633
+ rules: [14, 23]
634
+ },
635
+ // ─── providers ──────────────────────────────────────────────────
636
+ {
637
+ name: "GodxConfigProvider",
638
+ group: "providers",
639
+ tagline: "Root provider \u2014 locale, timezone, currency. Carries them to React Aria's I18nProvider so DatePicker / TimeField / formatters all pick up the locale.",
640
+ props: [
641
+ { name: "defaultLocale", type: "string", description: 'BCP 47 \u2014 "ja", "en-US", "vi", "fil".' },
642
+ { name: "defaultTimezone", type: "string", description: 'IANA \u2014 "Asia/Tokyo", "America/New_York".' },
643
+ { name: "defaultCurrency", type: "string", description: "ISO 4217 \u2014 JPY, USD." },
644
+ { name: "storage", type: '"localStorage" | "cookie" | "both"', description: "Persist user override." }
645
+ ],
646
+ example: `<GodxConfigProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo" defaultCurrency="JPY">
647
+ <App />
648
+ <Toaster position="top-right" />
649
+ </GodxConfigProvider>`,
650
+ storyPath: "providers/GodxConfigProvider.stories.tsx",
651
+ rules: [4, 14]
652
+ }
653
+ ];
654
+ function findComponent(name) {
655
+ const normalized = name.trim().toLowerCase();
656
+ return COMPONENTS.find((c) => c.name.toLowerCase() === normalized);
657
+ }
658
+ function componentsByGroup(group) {
659
+ return COMPONENTS.filter((c) => c.group === group);
660
+ }
661
+
662
+ // src/data/prop-vocabulary.ts
663
+ var PROP_VOCABULARY = [
664
+ {
665
+ name: "SizeProp",
666
+ concept: "Dimensional scale for most primitives.",
667
+ values: ["small", "default", "large"],
668
+ usedBy: [
669
+ "InputSize",
670
+ "CheckboxGroupSize",
671
+ "ColorPickerSize",
672
+ "MediaUploadSize",
673
+ "ProgressSize",
674
+ "RadioGroupSize",
675
+ "RateSize",
676
+ "TransferSize",
677
+ "SegmentedControlSize (subset)",
678
+ "SpaceSize (+ number)",
679
+ "FlexGap (+ number)",
680
+ "GridGap (+ number)",
681
+ "MasonryGap (+ number)"
682
+ ]
683
+ },
684
+ {
685
+ name: "SizeWithXSProp",
686
+ concept: 'Extension of `SizeProp` with `"x-small"` for compact icon-bar / table-row contexts.',
687
+ values: ["x-small", "small", "default", "large"],
688
+ usedBy: ["ButtonSize"]
689
+ },
690
+ {
691
+ name: "IconSizeProp",
692
+ concept: "Icon-symbol shaped primitives (visual axis = glyph size, not height).",
693
+ values: ["sm", "md", "lg"],
694
+ usedBy: ["SpinnerSize", "IconButtonSize"]
695
+ },
696
+ {
697
+ name: "StatusProp",
698
+ concept: "Form-field validation state.",
699
+ values: ["default", "error", "warning", "success"],
700
+ usedBy: ["InputStatus"],
701
+ notes: 'Form errors use `"error"` (not `"destructive"`) \u2014 different concern from destructive actions.'
702
+ },
703
+ {
704
+ name: "ToneProp",
705
+ concept: "Same values as StatusProp \u2014 name chosen when describing surface colouring rather than validation.",
706
+ values: ["default", "error", "warning", "success"],
707
+ usedBy: ["(alias of StatusProp)"]
708
+ },
709
+ {
710
+ name: "HelpToneProp",
711
+ concept: "Help-line / Alert colour ladder. Adds `info` + `warn` to StatusProp.",
712
+ values: ["default", "info", "warn", "error", "success"],
713
+ usedBy: ["FieldHelpTone"]
714
+ },
715
+ {
716
+ name: "OrientationProp",
717
+ concept: "Layout axis.",
718
+ values: ["horizontal", "vertical"],
719
+ usedBy: [
720
+ "AnchorOrientation",
721
+ "MenuOrientation",
722
+ "RadioGroupOrientation",
723
+ "CheckboxGroupOrientation",
724
+ "SegmentedControlOrientation",
725
+ "StepsOrientation",
726
+ "TabsOrientation"
727
+ ]
728
+ },
729
+ {
730
+ name: "DensityProp",
731
+ concept: "Internal row-height / padding scale (distinct from SizeProp \u2014 density rescales chrome, size rescales the primitive's own visual axis).",
732
+ values: ["compact", "default", "comfortable"],
733
+ usedBy: ["TreeDensity", "TableDensity"]
734
+ },
735
+ {
736
+ name: "SideProp",
737
+ concept: "Edge a floating panel docks against.",
738
+ values: ["top", "right", "bottom", "left"],
739
+ usedBy: ["SheetSide", "TabsPlacement", "TableStickySide (subset)"]
740
+ },
741
+ {
742
+ name: "PlacementProp",
743
+ concept: "Extension of SideProp with a centred anchor (Tabs placement, Tour spotlight).",
744
+ values: ["top", "right", "bottom", "left", "center"],
745
+ usedBy: ["TourPlacement"]
746
+ },
747
+ {
748
+ name: "PaddingProp",
749
+ concept: "Outer gutter scale for surface containers.",
750
+ values: ["tight", "default", "cozy", "none"],
751
+ usedBy: ["CardPadding", "PageHeaderPadding", "PageContentPadding"]
752
+ },
753
+ {
754
+ name: "AlignProp",
755
+ concept: "CSS-flexbox cross-axis alignment ladder.",
756
+ values: ["start", "end", "center", "stretch", "baseline"],
757
+ usedBy: ["FlexAlign"]
758
+ },
759
+ {
760
+ name: "ColorProp",
761
+ concept: "Full semantic palette \u2014 every CSS variable slot.",
762
+ values: ["default", "info", "success", "warning", "destructive", "attention", "primary", "secondary"],
763
+ usedBy: ["TagPresetColor (\u2212 secondary)", "TimelineColor (\u2212 secondary)", "SpinnerTone (+ muted)"],
764
+ notes: "Each value maps 1:1 to a CSS variable (`--info`, `--success`, `--warning`, `--destructive`, `--attention`, `--primary`, `--secondary`). The `data-accent` axis rebinds `--primary`'s hue without renaming the slot."
765
+ },
766
+ {
767
+ name: "FeedbackColorProp",
768
+ concept: "Subset of ColorProp accepted by feedback primitives (no brand `primary` since they are themselves informational).",
769
+ values: ["default", "info", "success", "warning", "destructive"],
770
+ usedBy: ["AlertColor", "ResultColor", "ProgressColor"]
771
+ },
772
+ {
773
+ name: "LoadingProp",
774
+ concept: "Loading-state union \u2014 shared across Form / FormField / data-entry primitives.",
775
+ values: ["true", "false", '{ kind: "spinner" }', '{ kind: "skeleton" }', "{ kind, label }"],
776
+ usedBy: ["FormProps.loading", "FormFieldProps.loading"],
777
+ notes: 'Cascade: `<Form loading>` sets a default for every nested `<FormField>`. Per-field `loading` overrides Form\'s. `true` \u2192 spinner (default). `{ kind: "skeleton" }` \u2192 use for INITIAL fetch state. UX nuance: skeleton on init, spinner on save.'
778
+ }
779
+ ];
780
+ function findVocab(name) {
781
+ const normalized = name.trim().toLowerCase().replace(/prop$/i, "");
782
+ return PROP_VOCABULARY.find(
783
+ (v) => v.name.toLowerCase().replace(/prop$/i, "") === normalized
784
+ );
785
+ }
786
+
787
+ // src/data/tokens.ts
788
+ var TOKENS = [
789
+ // Color — semantic slots (rebound by `data-theme` + `data-accent`)
790
+ { name: "--background", category: "color", role: "Base surface", axis: "data-theme" },
791
+ { name: "--foreground", category: "color", role: "Base text", axis: "data-theme" },
792
+ { name: "--card", category: "color", role: "Card surface", axis: "data-theme" },
793
+ { name: "--popover", category: "color", role: "Popover / dropdown surface", axis: "data-theme" },
794
+ { name: "--popover-foreground", category: "color", role: "Popover text", axis: "data-theme" },
795
+ { name: "--primary", category: "color", role: "Brand accent (Buttons, links)", axis: "data-accent" },
796
+ { name: "--primary-foreground", category: "color", role: "Text on primary surface", axis: "data-accent" },
797
+ { name: "--secondary", category: "color", role: "Secondary surface / text dimming", axis: "data-theme" },
798
+ { name: "--accent", category: "color", role: "Hover / focus tint" },
799
+ { name: "--muted", category: "color", role: "Muted surface" },
800
+ { name: "--muted-foreground", category: "color", role: "Muted text" },
801
+ { name: "--border", category: "color", role: "Default border color", axis: "data-theme" },
802
+ { name: "--input", category: "color", role: "Input field border" },
803
+ { name: "--ring", category: "color", role: "Focus ring", axis: "data-accent" },
804
+ { name: "--success", category: "color", role: "Success semantic slot" },
805
+ { name: "--warning", category: "color", role: "Warning semantic slot" },
806
+ { name: "--destructive", category: "color", role: "Danger / destructive action slot" },
807
+ { name: "--info", category: "color", role: "Info / neutral notice slot" },
808
+ { name: "--attention", category: "color", role: "Attention / non-destructive alert slot" },
809
+ // Spacing — fixed scale
810
+ { name: "--spacing-1", category: "spacing", role: "4px", value: "0.25rem" },
811
+ { name: "--spacing-2", category: "spacing", role: "8px", value: "0.5rem" },
812
+ { name: "--spacing-3", category: "spacing", role: "12px", value: "0.75rem" },
813
+ { name: "--spacing-4", category: "spacing", role: "16px", value: "1rem" },
814
+ { name: "--spacing-5", category: "spacing", role: "20px", value: "1.25rem" },
815
+ { name: "--spacing-6", category: "spacing", role: "24px", value: "1.5rem" },
816
+ { name: "--spacing-8", category: "spacing", role: "32px", value: "2rem" },
817
+ // Typography — fixed scale
818
+ { name: "--text-2xs", category: "typography", role: "10px", value: "0.625rem" },
819
+ { name: "--text-xs", category: "typography", role: "12px", value: "0.75rem" },
820
+ { name: "--text-sm", category: "typography", role: "14px", value: "0.875rem" },
821
+ { name: "--text-base", category: "typography", role: "16px", value: "1rem" },
822
+ { name: "--text-lg", category: "typography", role: "18px", value: "1.125rem" },
823
+ { name: "--text-xl", category: "typography", role: "20px", value: "1.25rem" },
824
+ { name: "--text-2xl", category: "typography", role: "24px", value: "1.5rem" },
825
+ { name: "--font-mono", category: "typography", role: "Monospace stack" },
826
+ // Radius — fixed scale
827
+ { name: "--radius-sm", category: "radius", role: "Small (chips, inputs)", value: "0.25rem" },
828
+ { name: "--radius-md", category: "radius", role: "Medium (cards)", value: "0.5rem" },
829
+ { name: "--radius-lg", category: "radius", role: "Large (dialogs)", value: "0.75rem" },
830
+ { name: "--radius-full", category: "radius", role: "Pill / circle", value: "9999px" },
831
+ // Breakpoints — mobile-first min-widths
832
+ { name: "--breakpoint-xs", category: "breakpoint", role: "Mobile-first base (\u22650px)", value: "0" },
833
+ { name: "--breakpoint-sm", category: "breakpoint", role: "Phone landscape / tablet portrait", value: "640px" },
834
+ { name: "--breakpoint-md", category: "breakpoint", role: "Tablet landscape", value: "768px" },
835
+ { name: "--breakpoint-lg", category: "breakpoint", role: "Laptop", value: "1024px" },
836
+ { name: "--breakpoint-xl", category: "breakpoint", role: "Desktop", value: "1280px" },
837
+ { name: "--breakpoint-xxl", category: "breakpoint", role: "Wide desktop", value: "1536px" },
838
+ // Density — rebound by `data-density`
839
+ { name: "--density-element", category: "density", role: "Element height (Input/Button)", axis: "data-density" },
840
+ { name: "--density-element-sm", category: "density", role: "Small element", axis: "data-density" },
841
+ { name: "--density-element-lg", category: "density", role: "Large element", axis: "data-density" },
842
+ { name: "--density-card", category: "density", role: "Card padding", axis: "data-density" },
843
+ { name: "--density-page", category: "density", role: "Page (PageContent) padding", axis: "data-density" },
844
+ { name: "--density-section", category: "density", role: "Section padding (cozy variant)", axis: "data-density" },
845
+ { name: "--header-height", category: "density", role: "Topbar height", axis: "data-density" },
846
+ { name: "--sidebar-width", category: "density", role: "Sidebar width (expanded)", axis: "data-density" },
847
+ { name: "--sidebar-width-collapsed", category: "density", role: "Sidebar icon-only width", axis: "data-density" },
848
+ { name: "--touch-target-min", category: "density", role: "Mobile touch target (does NOT scale)", value: "44px" },
849
+ // Motion — fixed timings
850
+ { name: "--transition-base", category: "motion", role: "Standard transition duration", value: "200ms" },
851
+ { name: "--ease-out", category: "motion", role: "Out easing curve", value: "cubic-bezier(0, 0, 0.2, 1)" },
852
+ { name: "--ease-in-out", category: "motion", role: "In-out easing", value: "cubic-bezier(0.4, 0, 0.2, 1)" }
853
+ ];
854
+ function tokensByCategory(category) {
855
+ return TOKENS.filter((t) => t.category === category);
856
+ }
857
+
858
+ // src/data/rules.ts
859
+ var CARDINAL_RULES = [
860
+ { number: 1, title: "Storybook is mandatory", body: "Every primitive / shell / composite has a paired story under `src/stories/<group>/<Name>.stories.tsx` covering every variant + state on light + dark." },
861
+ { number: 2, title: "Tokens, not utilities", body: "Visual values come from CSS custom properties in `src/tokens/` + `src/styles/theme.css`. Token-named Tailwind utilities (`bg-background`) are fine; raw value utilities (`bg-blue-500`) are forbidden. (ADR-0003)" },
862
+ { number: 3, title: "Radix for interactive primitives", body: "Anything with keyboard / ARIA / portal wraps the relevant Radix primitive. (ADR-0001)" },
863
+ { number: 4, title: "shadcn-style ownership", body: "Primitives are thin wrappers; consumers can fork the source in place. (ADR-0002)" },
864
+ { number: 5, title: "One i18next singleton", body: "`initI18n()` in `src/i18n/index.ts` is THE instance; consumers extend via `addResourceBundle`. (ADR-0004)" },
865
+ { number: 6, title: "WCAG 2.1 AA baseline", body: "Every interactive primitive passes axe-core (keyboard nav, ARIA, focus-visible, 4.5:1 contrast, `prefers-reduced-motion`). Stories double as a11y test surfaces." },
866
+ { number: 7, title: "SemVer 2.0 + Keep a Changelog 1.1", body: "Every release-worthy change updates `CHANGELOG.md` under `## Unreleased` in the same PR." },
867
+ { number: 8, title: "Inclusive naming", body: "`allowlist` / `denylist`, `main` / `primary` / `replica` / `secondary`, `they/them`. Never `whitelist` / `blacklist` / `master` / `slave`. Lint-enforced." },
868
+ { number: 9, title: "No marketing speak", body: 'Banned: "powerful", "robust", "blazing fast", "best-in-class", "seamless", "enterprise-grade". State what it does.' },
869
+ { number: 10, title: "English is canonical for docs", body: "Localised docs at `docs/i18n/<bcp47>/`; front-matter tracks staleness." },
870
+ { number: 11, title: "Submodule discipline", body: "Two-PR workflow: (1) submodule PR \u2192 `main`, (2) umbrella PR \u2192 bump pin. Never push a pin to a SHA not on the submodule remote." },
871
+ { number: 12, title: "Branch + PR workflow", body: "`feat/<scope>` / `fix/<scope>` \u2192 submodule `main`. CI green + squash-merge. No direct push to `main`. `--no-verify` forbidden." },
872
+ { number: 13, title: "TypeScript strict", body: "Explicit types on every export. `forwardRef` for components; `ComponentPropsWithoutRef` for extension. No `any`. No `@ts-ignore` without comment + issue link." },
873
+ { number: 14, title: "Every third-party library is shadcn / Radix-recommended", body: "Locked stack: Radix UI, cmdk, sonner, lucide-react, react-aria-components + `@internationalized/date`, i18next + react-i18next, class-variance-authority + clsx + tailwind-merge. New peer \u2192 ADR documenting why it's the canonical choice." },
874
+ { number: 15, title: "No `@apply` re-encoding tokens", body: "Inside a primitive `.tsx`, don't `@apply` a Tailwind utility that re-encodes a token \u2014 reference the canonical CSS class from `tokens.css` instead. Composite token-named utilities remain fine." },
875
+ { number: 16, title: "CSS source-of-truth is `src/tokens/` + `src/styles/theme.css`", body: "A primitive that needs a new color / spacing / radius adds it there FIRST, then references it." },
876
+ { number: 17, title: "`src/stories/` \u2194 `src/components/` parity", body: "Story set matches primitive set under each group. CI-checked via `scripts/check-stories-parity.mjs`." },
877
+ { number: 18, title: "`docs/reference/<group>/` \u2194 `src/components/<group>/` parity", body: "Every primitive has a reference page; every page maps to a primitive. CI-checked via `scripts/check-docs-parity.mjs`." },
878
+ { number: 19, title: "No service-specific anything", body: '`me-service`, `forge-service`, `admin-service` never appear in source / comments / prop names. Per-deployment brand color lives at `[data-accent="<palette>"]`.' },
879
+ { number: 20, title: 'No "platform-only" exports', body: "Every primitive ships via `package.json::exports`. Internal-only helpers stay un-exported." },
880
+ { number: 21, title: "Every component honours every theme axis", body: "`data-theme` (light / dark), `data-accent` (6 palettes), `data-density` (compact / default / comfortable), `data-font-size` (sm / base / lg / xl). Read from tokens, never hardcode values. Verify every PR via the Storybook toolbar sweep." },
881
+ { number: 22, title: "100% match to the design canon", body: 'Every visual literal comes from `design-handoff/ui-system/<latest-bundle>/`. Token-pin canon literals; never substitute "close enough". If the bundle doesn\'t cover a case \u2014 STOP, ask the user to mock it.' },
882
+ { number: 23, title: "Concept-first prop API", body: "One concept per prop. Reuse shared vocabulary (`size`, `variant`, `color`, `tone`, `accent`, `padding`, `density`, `orientation`, `placement`, `current`, `value` / `defaultValue` / `onValueChange`, `open` / `defaultOpen` / `onOpenChange`, `justify`, `sticky`, `offset`). Before adding a new prop or token: grep for an existing one." },
883
+ { number: 24, title: "Mobile-first", body: "Defaults target `xs` (\u22650px); progressive enhancement via `sm:` / `md:` / `lg:` / `xl:` / `2xl:`. Touch targets \u2265 44 \xD7 44 px (`--touch-target-min`, does NOT scale with density). Runtime viewport via `useBreakpoint`, never `window.innerWidth`. Stories render at narrow viewport first." },
884
+ { number: 25, title: "Stories are docs; UI is the primitive", body: "When a story looks wrong, fix the primitive / CSS / token. Never paper over with a story tweak. Story-only diff without a paired primitive / CSS / token diff is rejected." },
885
+ { number: 26, title: "Library isolation", body: "`dist/` ships only the consumer surface. Storybook, tests, scripts, design-handoff, `dev-probe/` stay out of npm. Every `dependencies` entry is `external` in `tsup`. Verification via `pnpm pack` + grep of `dist/`." },
886
+ { number: 27, title: "Per-group folder structure", body: "Primitives at `src/components/<group>/<Name>.tsx`; six canonical groups (general, layout, data-display, data-entry, feedback, navigation). Barrel = `src/components/primitives.ts` (single file). Stories + reference docs mirror the same group hierarchy." },
887
+ { number: 28, title: "`src/` folder taxonomy", body: "Three classes: consumer surface (matched by `tsup` entry + `package.json::exports`), Storybook-only (`src/stories/`), build-input-only (`cn.ts`, per-group sources consumed via the barrel). No `src/lib/`, `src/utils/`, `src/internal/`, `src/clients/`, `src/screens/`. Service clients live with the composite that uses them." },
888
+ { number: 29, title: "Stories consume framework primitives only", body: "No raw `<button>` / `<input>` / hand-rolled chips when a primitive exists. HTML semantics (`<section>`, `<article>`, \u2026) for structure are fine. Inline `style={{}}` limited to layout / positioning; no colour / radius / typography overrides." },
889
+ { number: 30, title: "Story `render` returns JSX directly", body: "No opaque `<XyzDemo />` wrapper components, no zero-arg `Demo` helpers. Use `render: function StoryName() { \u2026 }` so Storybook's source panel shows runnable JSX, not `<XyzDemo />`." },
890
+ { number: 31, title: "No nested wrapper / convenience primitives", body: "One Radix base = one framework primitive. `<SimpleX>` over `<X>` is forbidden; add a prop to `<X>` instead. Composites under `src/components/composites/` that combine multiple primitives are NOT wrappers." },
891
+ { number: 32, title: "No redundant props", body: "Before adding a prop / item field / variant, grep the existing surface; if a field already covers the concept, use it. Top-level prop that re-expresses an item field (Timeline `pending` \u2194 `items[i].animate`) is rejected." },
892
+ { number: 33, title: "Stories / source / docs name-synchronized", body: "No two names for the same export across the framework surface; no legacy aliases in stories / docs (source may keep an alias for a deprecation cycle, but the marketing surfaces use the canonical name only). Rename PR runs `grep -rn '<oldName>' src docs` and clears it." },
893
+ { number: 34, title: "Storybook source panel = real, copy-paste-ready code", body: 'Storybook\'s react-docgen serializer strips every function value (`cell: ({row}) => <JSX/>`, `render: ({field}) => <Input/>`, `rowClassName`, `renderItem`, \u2026) to `() => {}`. Any story whose `render` passes a function-valued prop, references a module-level helper (`StatusBadge`, `EMPLOYEE_COLUMNS`, etc.), or uses a render-prop pattern MUST override `parameters.docs.source.code` with the literal copy-paste-ready snippet \u2014 type aliases, helper functions spelled out, column definitions with cell JSX visible, inline data array. The `render()` callback stays as-is (module-level constants are fine for runtime performance); `source.code` is the marketing surface. Skip ONLY for stories whose JSX is purely static primitives Storybook can serialize verbatim (`<Button variant="primary">Click</Button>`). The exemplar is `Table.Default` in `src/stories/data-display/Table.stories.tsx`.' }
894
+ ];
895
+ function findRule(num) {
896
+ return CARDINAL_RULES.find((r) => r.number === num);
897
+ }
898
+
899
+ // src/data/patterns.ts
900
+ var PATTERNS = [
901
+ {
902
+ name: "registration-form",
903
+ tagline: "Card-wrapped sign-up form with zod validation + action bar.",
904
+ tags: ["form", "auth", "sign-up", "zod", "validation"],
905
+ code: `import { z } from "zod"
906
+ import { zodResolver } from "@hookform/resolvers/zod"
907
+ import {
908
+ Card, Form, FormField, Input, InputPassword, Checkbox,
909
+ Button, Separator, Typography,
910
+ } from "@godxjp/ui"
911
+
912
+ const schema = z.object({
913
+ name: z.string().min(1, "\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059"),
914
+ email: z.string().email("\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044"),
915
+ password: z.string()
916
+ .min(8, "8 \u6587\u5B57\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044")
917
+ .regex(/[A-Z]/, "\u5927\u6587\u5B57\u3092 1 \u3064\u4EE5\u4E0A\u542B\u3081\u3066\u304F\u3060\u3055\u3044")
918
+ .regex(/\\d/, "\u6570\u5B57\u3092 1 \u3064\u4EE5\u4E0A\u542B\u3081\u3066\u304F\u3060\u3055\u3044"),
919
+ agree: z.literal(true, { message: "\u5229\u7528\u898F\u7D04\u3078\u306E\u540C\u610F\u304C\u5FC5\u8981\u3067\u3059" }),
920
+ })
921
+ type SignUpValues = z.infer<typeof schema>
922
+
923
+ export function SignUpCard() {
924
+ return (
925
+ <Card
926
+ title="\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210"
927
+ subtitle="30 \u79D2\u3067\u5B8C\u4E86\u3057\u307E\u3059\u3002"
928
+ style={{ maxWidth: 420, margin: "0 auto" }}
929
+ >
930
+ <Form<SignUpValues>
931
+ resolver={zodResolver(schema)}
932
+ defaultValues={{ name: "", email: "", password: "", agree: true }}
933
+ onSubmit={async (values) => {
934
+ // POST to your API
935
+ await fetch("/api/signup", { method: "POST", body: JSON.stringify(values) })
936
+ }}
937
+ >
938
+ <FormField name="name" label="\u6C0F\u540D" required>
939
+ <Input placeholder="\u5C71\u7530 \u592A\u90CE" />
940
+ </FormField>
941
+ <FormField name="email" label="\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9" required>
942
+ <Input type="email" placeholder="taro@example.com" />
943
+ </FormField>
944
+ <FormField name="password" label="\u30D1\u30B9\u30EF\u30FC\u30C9" required
945
+ description="8 \u6587\u5B57\u4EE5\u4E0A / \u5927\u6587\u5B57 1 / \u6570\u5B57 1">
946
+ <InputPassword placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" />
947
+ </FormField>
948
+ <FormField name="agree">
949
+ <Checkbox>\u5229\u7528\u898F\u7D04\u306B\u540C\u610F\u3059\u308B</Checkbox>
950
+ </FormField>
951
+ <Button type="submit" variant="primary" block>
952
+ \u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210
953
+ </Button>
954
+ </Form>
955
+ <Separator style={{ margin: "var(--spacing-3) 0" }} />
956
+ <Typography.Text color="secondary" style={{ textAlign: "center", display: "block" }}>
957
+ \u65E2\u306B\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u304A\u6301\u3061\u3067\u3059\u304B? <a href="/login">\u30ED\u30B0\u30A4\u30F3</a>
958
+ </Typography.Text>
959
+ </Card>
960
+ )
961
+ }`
962
+ },
963
+ {
964
+ name: "settings-page",
965
+ tagline: "Sectioned settings Card \u2014 \u57FA\u672C\u60C5\u5831 / \u516C\u958B\u7BC4\u56F2 / \u901A\u77E5, horizontal layout, action bar.",
966
+ tags: ["settings", "form", "admin", "horizontal"],
967
+ code: `import { z } from "zod"
968
+ import { zodResolver } from "@hookform/resolvers/zod"
969
+ import {
970
+ Card, Form, FormField, Input, Select, Switch,
971
+ Button, Separator, Typography, Flex,
972
+ } from "@godxjp/ui"
973
+
974
+ const schema = z.object({
975
+ workspaceName: z.string().min(1),
976
+ visibility: z.string(),
977
+ notifyOnComment: z.boolean(),
978
+ notifyOnMention: z.boolean(),
979
+ digestFrequency: z.string(),
980
+ })
981
+ type SettingsValues = z.infer<typeof schema>
982
+
983
+ export function WorkspaceSettings() {
984
+ return (
985
+ <Card title="\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u8A2D\u5B9A" subtitle="Acme Forge \xB7 /acme-forge"
986
+ style={{ maxWidth: 820 }}>
987
+ <Form<SettingsValues> layout="horizontal"
988
+ resolver={zodResolver(schema)}
989
+ defaultValues={{
990
+ workspaceName: "Acme Forge",
991
+ visibility: "internal",
992
+ notifyOnComment: true,
993
+ notifyOnMention: true,
994
+ digestFrequency: "weekly",
995
+ }}
996
+ onSubmit={(v) => save(v)}
997
+ >
998
+ <Typography.Title size={5}>\u57FA\u672C\u60C5\u5831</Typography.Title>
999
+ <FormField name="workspaceName" label="\u540D\u524D" required>
1000
+ <Input />
1001
+ </FormField>
1002
+
1003
+ <Separator />
1004
+ <Typography.Title size={5}>\u516C\u958B\u7BC4\u56F2</Typography.Title>
1005
+ <FormField name="visibility" label="\u95B2\u89A7\u7BC4\u56F2" required>
1006
+ <Select options={[
1007
+ { value: "private", label: "\u30D7\u30E9\u30A4\u30D9\u30FC\u30C8" },
1008
+ { value: "internal", label: "\u793E\u5185\u516C\u958B" },
1009
+ { value: "public", label: "\u516C\u958B" },
1010
+ ]} />
1011
+ </FormField>
1012
+
1013
+ <Separator />
1014
+ <Typography.Title size={5}>\u901A\u77E5</Typography.Title>
1015
+ <FormField name="notifyOnComment" label="\u30B3\u30E1\u30F3\u30C8"><Switch /></FormField>
1016
+ <FormField name="notifyOnMention" label="\u30E1\u30F3\u30B7\u30E7\u30F3"><Switch /></FormField>
1017
+ <FormField name="digestFrequency" label="\u30C0\u30A4\u30B8\u30A7\u30B9\u30C8">
1018
+ <Select options={[
1019
+ { value: "off", label: "\u9001\u4FE1\u3057\u306A\u3044" },
1020
+ { value: "daily", label: "\u6BCE\u671D" },
1021
+ { value: "weekly", label: "\u9031\u6B21" },
1022
+ ]} />
1023
+ </FormField>
1024
+
1025
+ <Separator />
1026
+ <Flex gap="small" justify="end">
1027
+ <Button variant="ghost" type="button">\u5909\u66F4\u3092\u7834\u68C4</Button>
1028
+ <Button type="submit" variant="primary">\u8A2D\u5B9A\u3092\u4FDD\u5B58</Button>
1029
+ </Flex>
1030
+ </Form>
1031
+ </Card>
1032
+ )
1033
+ }`
1034
+ },
1035
+ {
1036
+ name: "data-table",
1037
+ tagline: "DataTable composite with toolbar + pagination + batch actions + sticky columns.",
1038
+ tags: ["table", "data", "pagination", "selection", "batch"],
1039
+ code: `import { useState } from "react"
1040
+ import {
1041
+ DataTable, Badge, Avatar, Flex, Typography, Button,
1042
+ type TableColumn,
1043
+ } from "@godxjp/ui"
1044
+
1045
+ interface Employee {
1046
+ id: string
1047
+ name: string
1048
+ role: string
1049
+ shop: string
1050
+ status: "active" | "pending" | "leave"
1051
+ }
1052
+
1053
+ const columns: TableColumn<Employee>[] = [
1054
+ {
1055
+ accessorKey: "name",
1056
+ header: "\u6C0F\u540D",
1057
+ minSize: 180,
1058
+ cell: ({ row }) => (
1059
+ <Flex align="center" gap="small">
1060
+ <Avatar size="sm" alt={row.original.name} />
1061
+ <Typography.Text strong>{row.original.name}</Typography.Text>
1062
+ </Flex>
1063
+ ),
1064
+ meta: { sticky: { side: "left", from: "md" } },
1065
+ },
1066
+ { accessorKey: "role", header: "\u5F79\u8077", minSize: 120 },
1067
+ { accessorKey: "shop", header: "\u5E97\u8217", minSize: 96 },
1068
+ {
1069
+ accessorKey: "status",
1070
+ header: "\u72B6\u614B",
1071
+ minSize: 96,
1072
+ cell: ({ row }) => {
1073
+ if (row.original.status === "active") return <Badge variant="success" dot>\u7A3C\u50CD\u4E2D</Badge>
1074
+ if (row.original.status === "pending") return <Badge variant="warning" dot>\u7533\u8ACB\u4E2D</Badge>
1075
+ return <Badge variant="neutral" dot>\u4F11\u8077</Badge>
1076
+ },
1077
+ },
1078
+ ]
1079
+
1080
+ export function EmployeeTable() {
1081
+ const [page, setPage] = useState(1)
1082
+ const [selected, setSelected] = useState<string[]>([])
1083
+ const [query, setQuery] = useState("")
1084
+
1085
+ // Replace with your real API
1086
+ const { data, total, loading } = useEmployees({ page, pageSize: 20, query })
1087
+
1088
+ return (
1089
+ <DataTable
1090
+ tableKey="employees-v1"
1091
+ columns={columns}
1092
+ data={data}
1093
+ rowKey="id"
1094
+ toolbar={{
1095
+ search: { value: query, onValueChange: setQuery },
1096
+ primaryAction: { label: "\u65B0\u898F\u8FFD\u52A0", onClick: () => openNewModal() },
1097
+ }}
1098
+ pagination={{
1099
+ current: page, pageSize: 20, total,
1100
+ onChange: setPage,
1101
+ }}
1102
+ batchActions={{
1103
+ selectedRowKeys: selected,
1104
+ onSelectedRowKeysChange: setSelected,
1105
+ actions: ({ clearSelection }) => (
1106
+ <Flex gap="small">
1107
+ <Button variant="ghost" onClick={clearSelection}>\u89E3\u9664</Button>
1108
+ <Button variant="destructive">\u524A\u9664</Button>
1109
+ </Flex>
1110
+ ),
1111
+ }}
1112
+ />
1113
+ )
1114
+ }`
1115
+ },
1116
+ {
1117
+ name: "confirm-destructive",
1118
+ tagline: "Destructive-action confirmation Dialog with typed-name verification.",
1119
+ tags: ["dialog", "confirm", "destructive", "delete"],
1120
+ code: `import { useState } from "react"
1121
+ import { Card, Form, FormField, Input, Button, Flex, Separator, toast } from "@godxjp/ui"
1122
+ import { z } from "zod"
1123
+ import { zodResolver } from "@hookform/resolvers/zod"
1124
+
1125
+ const schema = z.object({ confirm: z.string() })
1126
+
1127
+ export function DeleteProjectCard({ projectSlug }: { projectSlug: string }) {
1128
+ return (
1129
+ <Card
1130
+ title="\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664"
1131
+ subtitle="\u524A\u9664\u3059\u308B\u3068\u3001\u95A2\u9023\u3059\u308B\u3059\u3079\u3066\u306E\u30BF\u30B9\u30AF\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u6DFB\u4ED8\u30D5\u30A1\u30A4\u30EB\u304C\u5FA9\u5143\u3067\u304D\u306A\u304F\u306A\u308A\u307E\u3059\u3002"
1132
+ accent="destructive"
1133
+ style={{ maxWidth: 560 }}
1134
+ >
1135
+ <Form resolver={zodResolver(schema)} defaultValues={{ confirm: "" }}
1136
+ onSubmit={async (v) => {
1137
+ if (v.confirm !== projectSlug) {
1138
+ toast.error("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093")
1139
+ return
1140
+ }
1141
+ await fetch(\`/api/projects/\${projectSlug}\`, { method: "DELETE" })
1142
+ toast.success("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u524A\u9664\u3057\u307E\u3057\u305F")
1143
+ }}
1144
+ >
1145
+ <FormField name="confirm" label={\`\u78BA\u8A8D\u306E\u305F\u3081 "\${projectSlug}" \u3068\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\`} required>
1146
+ <Input placeholder={projectSlug} />
1147
+ </FormField>
1148
+ <Separator />
1149
+ <Flex gap="small" justify="end">
1150
+ <Button variant="ghost" type="button">\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
1151
+ <Button type="submit" variant="destructive">\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u5B8C\u5168\u306B\u524A\u9664</Button>
1152
+ </Flex>
1153
+ </Form>
1154
+ </Card>
1155
+ )
1156
+ }`
1157
+ },
1158
+ {
1159
+ name: "app-shell",
1160
+ tagline: "AppShell wiring \u2014 Sidebar + Topbar + PageContent + Toaster.",
1161
+ tags: ["shell", "layout", "navigation", "app-root"],
1162
+ code: `import {
1163
+ AppShell, Sidebar, Topbar, PageContent,
1164
+ GodxConfigProvider, Toaster, Button, Typography,
1165
+ } from "@godxjp/ui"
1166
+ import { LayoutDashboard, FolderGit2, GitBranch, Settings } from "lucide-react"
1167
+ import { useState } from "react"
1168
+
1169
+ const SECTIONS = [
1170
+ {
1171
+ label: "Workspace",
1172
+ items: [
1173
+ { id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
1174
+ { id: "projects", label: "Projects", icon: FolderGit2, badge: 8 },
1175
+ { id: "branches", label: "Branches", icon: GitBranch },
1176
+ ],
1177
+ },
1178
+ {
1179
+ label: "Admin",
1180
+ items: [
1181
+ { id: "settings", label: "Settings", icon: Settings },
1182
+ ],
1183
+ },
1184
+ ]
1185
+
1186
+ export function App() {
1187
+ const [active, setActive] = useState("dashboard")
1188
+
1189
+ return (
1190
+ <GodxConfigProvider defaultLocale="ja" defaultTimezone="Asia/Tokyo" defaultCurrency="JPY">
1191
+ <AppShell
1192
+ sidebar={<Sidebar activeId={active} onSelect={setActive} sections={SECTIONS} />}
1193
+ topbar={<Topbar product={{ id: "godx", label: "GoDX Forge" }} project={{ id: "p1", label: "Acme" }} />}
1194
+ >
1195
+ <PageContent
1196
+ title="Dashboard"
1197
+ subtitle="Workspace activity, KPIs"
1198
+ extra={<Button variant="primary">New issue</Button>}
1199
+ >
1200
+ <Typography.Paragraph>Page body goes here.</Typography.Paragraph>
1201
+ </PageContent>
1202
+ </AppShell>
1203
+ <Toaster position="top-right" />
1204
+ </GodxConfigProvider>
1205
+ )
1206
+ }`
1207
+ },
1208
+ {
1209
+ name: "filter-bar",
1210
+ tagline: "Inline filter bar above a table \u2014 search + selects + reset.",
1211
+ tags: ["filter", "search", "inline", "table"],
1212
+ code: `import { useState } from "react"
1213
+ import { Form, FormField, Input, Select, Button } from "@godxjp/ui"
1214
+
1215
+ export function EmployeeFilter({ onChange }: { onChange: (v: any) => void }) {
1216
+ return (
1217
+ <Form layout="inline" defaultValues={{ query: "", status: "active", shop: "shibuya" }}
1218
+ onSubmit={(v) => onChange(v)}
1219
+ >
1220
+ <FormField name="query" label="\u30AD\u30FC\u30EF\u30FC\u30C9">
1221
+ <Input placeholder="\u540D\u524D / \u30E1\u30FC\u30EB / \u96FB\u8A71\u756A\u53F7" style={{ width: "16rem" }} />
1222
+ </FormField>
1223
+ <FormField name="status" label="\u30B9\u30C6\u30FC\u30BF\u30B9">
1224
+ <Select options={[
1225
+ { value: "active", label: "\u7A3C\u50CD\u4E2D" },
1226
+ { value: "paused", label: "\u4E00\u6642\u505C\u6B62" },
1227
+ ]} />
1228
+ </FormField>
1229
+ <FormField name="shop" label="\u5E97\u8217">
1230
+ <Select options={[
1231
+ { value: "shibuya", label: "\u6E0B\u8C37\u5E97" },
1232
+ { value: "shinjuku", label: "\u65B0\u5BBF\u5E97" },
1233
+ ]} />
1234
+ </FormField>
1235
+ <Button type="submit" variant="primary">\u691C\u7D22</Button>
1236
+ <Button type="reset" variant="ghost">\u30EA\u30BB\u30C3\u30C8</Button>
1237
+ </Form>
1238
+ )
1239
+ }`
1240
+ },
1241
+ {
1242
+ name: "loading-states",
1243
+ tagline: "Skeleton on init fetch + Spinner on save (UX nuance).",
1244
+ tags: ["loading", "skeleton", "spinner", "form"],
1245
+ code: `import { useState, useEffect } from "react"
1246
+ import {
1247
+ Card, Form, FormField, Input, Textarea, Button, Flex, Separator,
1248
+ } from "@godxjp/ui"
1249
+
1250
+ export function ProfileEditor() {
1251
+ const [submitting, setSubmitting] = useState(false)
1252
+ const [initialFetched, setInitialFetched] = useState(false)
1253
+ const [values, setValues] = useState({ name: "", bio: "" })
1254
+
1255
+ // Initial fetch
1256
+ useEffect(() => {
1257
+ fetch("/api/me").then(r => r.json()).then(data => {
1258
+ setValues(data)
1259
+ setInitialFetched(true)
1260
+ })
1261
+ }, [])
1262
+
1263
+ return (
1264
+ <Card title="\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB" style={{ maxWidth: 640 }}>
1265
+ <Form
1266
+ loading={!initialFetched ? { kind: "skeleton" } : submitting}
1267
+ defaultValues={values}
1268
+ onSubmit={async (v) => {
1269
+ setSubmitting(true)
1270
+ await fetch("/api/me", { method: "PUT", body: JSON.stringify(v) })
1271
+ setSubmitting(false)
1272
+ }}
1273
+ >
1274
+ <FormField name="name" label="\u6C0F\u540D" required><Input /></FormField>
1275
+ <FormField name="bio" label="\u81EA\u5DF1\u7D39\u4ECB"><Textarea rows={3} /></FormField>
1276
+ <Separator />
1277
+ <Flex gap="small" justify="end">
1278
+ <Button variant="ghost" type="button" disabled={submitting}>\u30AD\u30E3\u30F3\u30BB\u30EB</Button>
1279
+ <Button type="submit" variant="primary" loading={submitting}>\u4FDD\u5B58</Button>
1280
+ </Flex>
1281
+ </Form>
1282
+ </Card>
1283
+ )
1284
+ }`
1285
+ }
1286
+ ];
1287
+ function findPattern(name) {
1288
+ const slug = name.trim().toLowerCase();
1289
+ return PATTERNS.find((p) => p.name === slug);
1290
+ }
1291
+ function searchPatterns(query) {
1292
+ const q = query.trim().toLowerCase();
1293
+ if (q === "") return PATTERNS;
1294
+ return PATTERNS.filter(
1295
+ (p) => p.name.includes(q) || p.tagline.toLowerCase().includes(q) || p.tags.some((t) => t.includes(q))
1296
+ );
1297
+ }
1298
+
1299
+ // src/data/skills-index.ts
1300
+ var SKILLS = [
1301
+ // ── taste (foundational) ───────────────────────────────────────
1302
+ {
1303
+ id: "taste",
1304
+ name: "Taste baseline \u2014 Senior UI/UX engineering",
1305
+ whenToUse: "Default for any production app screen. Metric-based rules, strict component architecture, CSS hardware acceleration, balanced design engineering.",
1306
+ source: "Leonxlnx/taste-skill (root) + @godxjp/ui design-thinking.ts",
1307
+ sections: [
1308
+ {
1309
+ id: "mobile-first",
1310
+ title: "Mobile-first non-negotiable",
1311
+ tagline: "Defaults target xs (\u22650px); enhance via sm: / md: / lg: / xl: / 2xl:",
1312
+ body: `Cardinal rule 24. Touch targets \u2265 44\xD744 px. NEVER read
1313
+ window.innerWidth \u2014 use useBreakpoint(). Stories render at narrow
1314
+ viewport first. Multi-column layouts: grid grid-cols-1 sm:grid-cols-N.
1315
+ EXCEPTION: name pairs (\u59D3+\u540D) use grid-cols-2 always.`
1316
+ },
1317
+ {
1318
+ id: "one-intent-per-screen",
1319
+ title: "One intent per screen",
1320
+ tagline: "Pick the ONE primary question this page answers. 60-80% visual weight to it.",
1321
+ body: `Wall-of-cards dashboards are AI slop. Show 1-2 hero metrics
1322
+ + ONE primary list + contextual actions. Tertiary content lives in
1323
+ Sheet / DropdownMenu / next page. The 8-stat-card grid pattern is a
1324
+ RED FLAG \u2014 it means "I couldn't decide what mattered".`
1325
+ },
1326
+ {
1327
+ id: "type-hierarchy",
1328
+ title: "Type does the hierarchy work",
1329
+ tagline: "Weight + size + color, NOT colored background blocks.",
1330
+ body: `Typography.Title size={1..5} is the canonical scale. h2 \u2192 h3 \u2192 h4
1331
+ each ~75% of previous. Don't skip levels. Body = Typography.Paragraph.
1332
+ Metadata = Typography.Text color="secondary". Type contrast alone IS
1333
+ the hierarchy \u2014 colored background blocks for every section is AI
1334
+ slop. Reserve colored bg for genuinely different surfaces (Card vs
1335
+ page, Alert vs body).`
1336
+ },
1337
+ {
1338
+ id: "whitespace-is-content",
1339
+ title: "Whitespace IS content",
1340
+ tagline: "Use the smallest spacing step that visually separates concepts.",
1341
+ body: `Spacing ladder: --spacing-1 (4px) for tight groups, -2 (8) for
1342
+ control pairs, -3 (12) for related controls in form, -4 (16) for
1343
+ sections, -6 (24) for cards in grid, -8 (32) for page rhythm.
1344
+ "Premium via excess padding" (everything spacing-6 to feel premium)
1345
+ is wrong \u2014 undersized content lost in oceans of grey. Premium = VARIED
1346
+ spacing \u2014 tight where related, generous where not.`
1347
+ },
1348
+ {
1349
+ id: "two-accents",
1350
+ title: "Two accents do real work \u2014 not eight",
1351
+ tagline: "ONE brand color for action + ONE semantic color contextually. Not a rainbow.",
1352
+ body: `Use --primary for actions (Button, link, focus ring) + ONE
1353
+ semantic (destructive for delete confirm, warning for deadline alert,
1354
+ success for completed state). NEVER a rainbow tag wall. Tag variety
1355
+ via appearance (soft/solid/outline) of the SAME hue, not different
1356
+ hues.`
1357
+ },
1358
+ {
1359
+ id: "form-discipline",
1360
+ title: "Form discipline \u2014 label, help, error always",
1361
+ tagline: "Every input has explicit label + help + error wired via FormField.",
1362
+ body: `Never placeholder-as-label (disappears on focus). Use
1363
+ <FormField label description /> \u2014 it wires the Radix Label, the
1364
+ description text, and the error via aria-describedby + role="alert"
1365
+ automatically. Server errors as inline near the field, NOT as toasts
1366
+ (SR can't announce a disappearing toast).`
1367
+ },
1368
+ {
1369
+ id: "loading-states",
1370
+ title: "Skeleton for INIT, Spinner for ACTIVE work",
1371
+ tagline: "Different states for different moments \u2014 never mix.",
1372
+ body: `<Form loading={{ kind: "skeleton" }}> while fetching existing
1373
+ values (no data yet \u2014 maintain layout, prevent flash). <Form loading>
1374
+ (boolean true) while saving (data is there, you're transforming).
1375
+ Skeleton during save is wrong (user sees structure they already saw \u2014
1376
+ broken). Spinner during init is wrong (nothing to spin over).`
1377
+ }
1378
+ ]
1379
+ },
1380
+ // ── soft (Awwwards / premium agency) ───────────────────────────
1381
+ {
1382
+ id: "soft",
1383
+ name: "Awwwards-tier \u2014 $150k agency build",
1384
+ whenToUse: "Premium agency brief \u2014 marketing site, hero pages, product showcase. NOT every internal SaaS screen. Apply when the brief asks for 'Linear-tier', 'Apple-esque', 'Awwwards-style'.",
1385
+ source: "Leonxlnx/taste-skill/soft-skill",
1386
+ sections: [
1387
+ {
1388
+ id: "absolute-zero",
1389
+ title: "Absolute Zero \u2014 banned defaults",
1390
+ tagline: "Inter / Roboto / Lucide / shadow-md / 3-col Bootstrap / linear easing \u2014 banned.",
1391
+ body: `BANNED FONTS: Inter, Roboto, Arial, Open Sans, Helvetica \u2192 use Geist
1392
+ / Clash Display / PP Editorial New / Plus Jakarta Sans.
1393
+ BANNED ICONS: standard thick Lucide / Material \u2192 use Phosphor Light /
1394
+ Remix Line.
1395
+ BANNED BORDERS: generic 1px solid gray \u2192 hairline rings (ring-1
1396
+ ring-black/5), tinted borders, OR whitespace as separator.
1397
+ BANNED SHADOWS: shadow-md, rgba(0,0,0,0.3) \u2192 ultra-diffuse low-opacity
1398
+ (<0.05), TINTED to background.
1399
+ BANNED LAYOUTS: edge-to-edge sticky navbars, symmetric 3-col \u2192 floating
1400
+ glass nav pills, asymmetric bento grids.
1401
+ BANNED MOTION: linear / ease-in-out / instant \u2192 custom cubic-bezier
1402
+ (0.32, 0.72, 0, 1), spring physics, scroll interpolation.`
1403
+ },
1404
+ {
1405
+ id: "vibe-archetypes",
1406
+ title: "3 Vibe Archetypes (pick 1)",
1407
+ tagline: "Ethereal Glass (SaaS/AI) | Editorial Luxury (Lifestyle/Agency) | Soft Structuralism (Consumer/Health)",
1408
+ body: `1. ETHEREAL GLASS (SaaS / AI / Tech): OLED black #050505, radial
1409
+ mesh gradients (purple/emerald orbs), Vantablack cards with heavy
1410
+ backdrop-blur-2xl, white/10 hairlines. Wide geometric Grotesk.
1411
+ 2. EDITORIAL LUXURY (Lifestyle / Real Estate / Agency): Warm creams
1412
+ #FDFBF7, muted sage, deep espresso. High-contrast Variable Serif
1413
+ for massive headings. CSS noise overlay opacity-0.03 for paper.
1414
+ 3. SOFT STRUCTURALISM (Consumer / Health / Portfolio): Silver-grey
1415
+ or pure white. Massive bold Grotesk typography. Airy floating
1416
+ components, unbelievably soft diffused ambient shadows
1417
+ (shadow-[0_30px_60px_-30px_rgba(0,0,0,0.06)]).`
1418
+ },
1419
+ {
1420
+ id: "layout-archetypes",
1421
+ title: "3 Layout Archetypes (pick 1)",
1422
+ tagline: "Asymmetric Bento | Z-Axis Cascade | Editorial Split \u2014 ALL collapse to single-col on mobile.",
1423
+ body: `1. ASYMMETRIC BENTO: Masonry CSS Grid varying card sizes
1424
+ (col-span-8 row-span-2 next to stacked col-span-4). Mobile:
1425
+ grid-cols-1, gap-6, all col-span reset to 1.
1426
+ 2. Z-AXIS CASCADE: Elements stacked like physical cards, slightly
1427
+ overlapping with varying depth + -2deg/3deg rotations. Mobile:
1428
+ REMOVE rotations + negative-margin overlaps below 768px (touch
1429
+ conflicts), stack vertically.
1430
+ 3. EDITORIAL SPLIT: Massive typography w-1/2 left, interactive
1431
+ scrollable content right. Mobile: full-width vertical stack,
1432
+ typography on top, content below with horizontal scroll preserved.
1433
+
1434
+ UNIVERSAL MOBILE OVERRIDE: w-full, px-4, py-8 below 768px. NEVER
1435
+ h-screen \u2014 always min-h-[100dvh] (iOS Safari viewport jump fix).`
1436
+ },
1437
+ {
1438
+ id: "double-bezel",
1439
+ title: "Double-Bezel / Doppelrand architecture",
1440
+ tagline: "Cards nested like physical hardware \u2014 glass plate in aluminum tray.",
1441
+ body: `Never flat. Wrap every premium card in two nested enclosures:
1442
+
1443
+ OUTER SHELL: subtle bg (bg-black/5 or bg-white/5), hairline outer
1444
+ border (ring-1 ring-black/5 or border border-white/10), padding
1445
+ p-1.5 / p-2, large outer radius (rounded-[2rem]).
1446
+
1447
+ INNER CORE: distinct background, inner highlight
1448
+ (shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)]), mathematically
1449
+ smaller radius (rounded-[calc(2rem-0.375rem)]) for concentric curves.
1450
+
1451
+ The math gives "machined hardware" look. Concentric curves = human
1452
+ eye reads "precision".`
1453
+ },
1454
+ {
1455
+ id: "button-in-button",
1456
+ title: "Button-in-Button trailing icon",
1457
+ tagline: "Trailing arrow lives in its OWN nested pill \u2014 not naked next to text.",
1458
+ body: `Primary buttons: rounded-full, px-6 py-3 generous padding. Trailing
1459
+ arrow/icon NEVER sits naked next to text. Nests in its own circular
1460
+ wrapper: w-8 h-8 rounded-full bg-black/5 flex items-center justify-
1461
+ center, flush with main button's right inner padding. On hover, inner
1462
+ icon translates diagonally + scales up \u2014 internal kinetic tension.`
1463
+ },
1464
+ {
1465
+ id: "magnetic-hover",
1466
+ title: "Magnetic button hover physics",
1467
+ tagline: "Custom cubic-bezier, scale on press, internal translate on hover. NO linear easing.",
1468
+ body: `Use group utility. Hover \u2260 background color change. On hover:
1469
+ nested inner icon circle translates diagonally (group-hover:translate-
1470
+ x-1 group-hover:-translate-y-[1px]) AND scales up (scale-105). On
1471
+ press: scale entire button down slightly (active:scale-[0.98]) \u2014
1472
+ simulates physical click. Custom cubic-bezier on ALL transitions
1473
+ (NEVER linear / ease-in-out).`
1474
+ },
1475
+ {
1476
+ id: "scroll-entry",
1477
+ title: "Scroll-interpolation entry animations",
1478
+ tagline: "Elements never appear statically \u2014 gentle fade-up from below with blur.",
1479
+ body: `As elements enter viewport: translate-y-16 blur-md opacity-0 \u2192
1480
+ translate-y-0 blur-0 opacity-100 over 800ms+. Use IntersectionObserver
1481
+ or Framer Motion's whileInView. NEVER window.addEventListener("scroll")
1482
+ \u2014 continuous reflows kill mobile perf.`
1483
+ },
1484
+ {
1485
+ id: "performance-guardrails",
1486
+ title: "Performance guardrails",
1487
+ tagline: "GPU-safe transforms, blur only on fixed/sticky, noise on pointer-events-none.",
1488
+ body: `- Animate transform + opacity ONLY. NEVER top/left/width/height
1489
+ (layout reflow). will-change: transform sparingly.
1490
+ - backdrop-blur only on FIXED/STICKY elements. NEVER on scrolling
1491
+ containers \u2014 continuous GPU repaints, severe mobile frame drops.
1492
+ - grain/noise: FIXED pointer-events-none pseudo-element (position:
1493
+ fixed; inset: 0; z-index: 50). Never on scrolling containers.
1494
+ - Z-index discipline: no arbitrary z-50 or z-[9999]. Reserve for
1495
+ systemic layers (sticky nav, modals, overlays, tooltips).`
1496
+ }
1497
+ ]
1498
+ },
1499
+ // ── minimalist (editorial workspace) ───────────────────────────
1500
+ {
1501
+ id: "minimalist",
1502
+ name: "Minimalist \u2014 editorial workspace",
1503
+ whenToUse: "Document-style apps (Notion-clone, knowledge base, blog admin). Warm monochrome + spot pastels. Bento grids. Editorial serif headings + sans body + monospace for data.",
1504
+ source: "Leonxlnx/taste-skill/minimalist-skill",
1505
+ sections: [
1506
+ {
1507
+ id: "negative-constraints",
1508
+ title: "Banned defaults",
1509
+ tagline: "Inter / Roboto / Lucide / shadow-md / pill containers / emojis / Acme \u2014 banned.",
1510
+ body: `BANNED: Inter / Roboto / Open Sans fonts. Lucide / Feather / Heroicons
1511
+ default icons. Tailwind heavy shadows (md/lg/xl). Primary-colored hero
1512
+ backgrounds. Gradients, neon, full glassmorphism. rounded-full on
1513
+ large containers. Emojis anywhere in markup. Generic names (John Doe,
1514
+ Acme, Lorem Ipsum). AI clich\xE9s (Elevate, Seamless, Unleash, Next-Gen).`
1515
+ },
1516
+ {
1517
+ id: "typography",
1518
+ title: "Editorial typography",
1519
+ tagline: "Serif heading + character sans body + mono data. Off-black for body, never pure.",
1520
+ body: `Pair: editorial serif (Lyon Text / Newsreader / Playfair / Instrument
1521
+ Serif) for headings WITH character sans (SF Pro Display / Geist Sans /
1522
+ Switzer) body WITH monospace (Geist Mono / JetBrains Mono / SF Mono)
1523
+ for data + keystrokes.
1524
+
1525
+ Tight tracking on serif headings (-0.02em to -0.04em). Tight
1526
+ line-height (1.1). Body line-height 1.6. Body color: off-black
1527
+ #111111 or #2F3437 \u2014 NEVER pure #000. Secondary text: muted gray
1528
+ #787774.`
1529
+ },
1530
+ {
1531
+ id: "palette",
1532
+ title: "Warm monochrome + spot pastels",
1533
+ tagline: "Canvas warm bone #F7F6F3. Accents from 4 desaturated pastels only.",
1534
+ body: `Canvas: #FFFFFF or warm bone #F7F6F3 / #FBFBFA.
1535
+ Cards: #FFFFFF or #F9F9F8.
1536
+ Borders: ultra-light #EAEAEA or rgba(0,0,0,0.06).
1537
+
1538
+ Accents EXCLUSIVELY from 4 muted pastels:
1539
+ - Pale Red: bg #FDEBEC | text #9F2F2D
1540
+ - Pale Blue: bg #E1F3FE | text #1F6C9F
1541
+ - Pale Green: bg #EDF3EC | text #346538
1542
+ - Pale Yellow: bg #FBF3DB | text #956400`
1543
+ },
1544
+ {
1545
+ id: "bento-grids",
1546
+ title: "Asymmetric bento grids",
1547
+ tagline: "Cards: 1px solid #EAEAEA, 8-12px radius MAX, 24-40px padding, NO shadow.",
1548
+ body: `Asymmetric CSS Grid layouts (1x1, 1x2, 2x1, 2x2). Cards:
1549
+ border: 1px solid #EAEAEA, border-radius 8px or 12px MAX (never larger),
1550
+ generous internal padding (24-40px), no box-shadow. Use raw CSS Grid
1551
+ with gridColumn/gridRow span for the bento layout.`
1552
+ },
1553
+ {
1554
+ id: "components",
1555
+ title: "Component refinements",
1556
+ tagline: "Primary CTA: solid black bg, 4-6px radius. Tags: pill + uppercase + 0.05em tracking + pastel.",
1557
+ body: `PRIMARY CTA: solid #111 bg, white text, 4-6px radius (NOT full pill),
1558
+ no shadow. Hover: shift to #333 or active:scale(0.98).
1559
+ TAGS/BADGES: pill (border-radius 9999px), text-xs UPPERCASE,
1560
+ letter-spacing 0.05em. Background = muted pastel. Deep text color.
1561
+ ACCORDIONS (FAQ): strip ALL container chrome. Items separated by
1562
+ border-bottom: 1px solid #EAEAEA only. Toggle: sharp + / \u2212 icons.
1563
+ KBD: <kbd> as physical key \u2014 1px solid #EAEAEA, 4px radius, #F7F6F3
1564
+ bg, monospace.
1565
+ FAUX-OS chrome (for product previews): white top bar + 3 small light-
1566
+ gray circles (macOS replica).`
1567
+ },
1568
+ {
1569
+ id: "motion",
1570
+ title: "Subtle invisible motion",
1571
+ tagline: "Scroll-entry fade-up 600ms cubic-bezier(.16,1,.3,1). Card hover lift via shadow shift only.",
1572
+ body: `Scroll entry: translateY(12px) + opacity(0) \u2192 0/1 over 600ms with
1573
+ cubic-bezier(0.16, 1, 0.3, 1). IntersectionObserver, never raw scroll.
1574
+ Hover lift: box-shadow 0 \u2192 0 2px 8px rgba(0,0,0,0.04) over 200ms.
1575
+ Buttons: scale(0.98) on :active. Staggered list reveals: animation-
1576
+ delay calc(var(--index) * 80ms). Background ambient: optional slow
1577
+ radial gradient blob, 20s+ duration, opacity 0.02-0.04, on
1578
+ position:fixed pointer-events-none layer.`
1579
+ }
1580
+ ]
1581
+ },
1582
+ // ── brutalist ──────────────────────────────────────────────────
1583
+ {
1584
+ id: "brutalist",
1585
+ name: "Brutalist \u2014 Swiss print + military terminal",
1586
+ whenToUse: "Data-heavy dashboards, declassified-blueprint feel, portfolios needing raw mechanical aesthetic. Rigid grids, extreme type scale contrast, utilitarian color, analog degradation effects.",
1587
+ source: "Leonxlnx/taste-skill/brutalist-skill",
1588
+ sections: [
1589
+ {
1590
+ id: "principles",
1591
+ title: "Brutalist principles",
1592
+ tagline: "Raw mechanical interfaces \u2014 rigid grids, extreme type contrast, utilitarian color, analog degradation.",
1593
+ body: `Rejects ornament. Embraces structure as aesthetic. Grids are visible
1594
+ (via borders or rules). Type scale is dramatically contrasted (massive
1595
+ display heading next to small tabular body). Color is utilitarian \u2014
1596
+ black, off-white, single signal color (red, amber, terminal green).
1597
+ Analog effects (printer-bleed, halftone, screenprint registration
1598
+ errors) add character without becoming kitsch. Best for: dev tools,
1599
+ declassified-data presentations, raw-fact dashboards, technical
1600
+ portfolios.`
1601
+ }
1602
+ ]
1603
+ },
1604
+ // ── gpt-tasteskill ─────────────────────────────────────────────
1605
+ {
1606
+ id: "gpt-tasteskill",
1607
+ name: "GPT taste \u2014 editorial + advanced GSAP motion",
1608
+ whenToUse: "Long-scroll marketing pages with cinematic scroll choreography. Pins, stacks, scrubbed timelines. AIDA structure. Wide editorial typography. Bans 6-line wraps. Gapless bento grids.",
1609
+ source: "Leonxlnx/taste-skill/gpt-tasteskill",
1610
+ sections: [
1611
+ {
1612
+ id: "principles",
1613
+ title: "GSAP motion + AIDA structure",
1614
+ tagline: "Python-driven layout randomization, strict ScrollTrigger choreography, wide editorial typography.",
1615
+ body: `AIDA (Attention/Interest/Desire/Action) page spine. Wide editorial
1616
+ typography \u2014 bans 6-line wraps (line lengths cap at ~5). Gapless bento
1617
+ grids (cards flush against each other, no gutter \u2014 outline borders
1618
+ do the separation). Inline micro-images (small contextual photos
1619
+ within a section, not just hero). Massive section spacing (180-240px
1620
+ between sections, not 80). GSAP ScrollTriggers: pinning (section
1621
+ locks while sub-content scrolls), stacking (next section slides
1622
+ over current), scrubbing (animation tied to scroll progress).`
1623
+ }
1624
+ ]
1625
+ },
1626
+ // ── redesign ───────────────────────────────────────────────────
1627
+ {
1628
+ id: "redesign",
1629
+ name: "Redesign \u2014 audit + upgrade existing UI",
1630
+ whenToUse: "Working on an existing project (not greenfield). Find generic patterns, weak points, missing states. Apply fixes in priority order \u2014 font swap first, palette cleanup second, etc.",
1631
+ source: "Leonxlnx/taste-skill/redesign-skill + redesign-audit.ts",
1632
+ sections: [
1633
+ {
1634
+ id: "fix-priority",
1635
+ title: "Fix priority order",
1636
+ tagline: "Font \u2192 palette \u2192 states \u2192 layout \u2192 components \u2192 loading/empty/error \u2192 typography polish.",
1637
+ body: `Apply in THIS order for max visual impact at min risk:
1638
+
1639
+ 1. FONT SWAP \u2014 biggest instant improvement, lowest risk.
1640
+ 2. COLOR PALETTE CLEANUP \u2014 remove clashing / oversaturated colors.
1641
+ 3. HOVER + ACTIVE STATES \u2014 makes interface feel alive.
1642
+ 4. LAYOUT + SPACING \u2014 proper grid, max-width, consistent padding.
1643
+ 5. REPLACE GENERIC COMPONENTS \u2014 cliche \u2192 modern alternatives.
1644
+ 6. LOADING / EMPTY / ERROR STATES \u2014 makes it feel finished.
1645
+ 7. TYPOGRAPHY SCALE + SPACING POLISH \u2014 premium final touch.
1646
+
1647
+ Rules: work with existing stack, don't migrate frameworks, don't break
1648
+ functionality, test after every change. Small targeted improvements
1649
+ over big rewrites.`
1650
+ },
1651
+ {
1652
+ id: "audit-checklist",
1653
+ title: "Audit checklist (8 categories)",
1654
+ tagline: "Typography / color / layout / interactivity / content / components / iconography / code / omissions.",
1655
+ body: `See redesign-audit.ts (50+ checks). Common findings:
1656
+
1657
+ TYPOGRAPHY: Inter everywhere, weak headlines, full-width paragraphs,
1658
+ only 400/700 weights, proportional numbers in data, Title Case On
1659
+ Every Header.
1660
+ COLOR: pure #000, oversaturated accents, multiple competing accents,
1661
+ purple/blue AI gradient, generic black shadows, empty flat sections.
1662
+ LAYOUT: 3-equal-card columns (most generic AI pattern), height:100vh
1663
+ iOS jump, no max-width container, dashboard always left sidebar.
1664
+ INTERACTIVITY: no hover, no active feedback, no focus ring, generic
1665
+ spinners, no empty states, alert() for errors, dead links.
1666
+ CONTENT: John Doe / Acme / Lorem Ipsum, AI clich\xE9s, exclamation marks
1667
+ in success, passive voice errors.
1668
+ OMISSIONS: no legal links, no back nav, no 404, no form validation,
1669
+ no skip-to-content.`
1670
+ }
1671
+ ]
1672
+ },
1673
+ // ── output (full-output enforcement) ───────────────────────────
1674
+ {
1675
+ id: "output",
1676
+ name: "Full-output enforcement",
1677
+ whenToUse: "Always. Bans the // ... / // TODO / 'I'll leave this as an exercise' patterns. Treat every task as production-critical.",
1678
+ source: "Leonxlnx/taste-skill/output-skill + output-quality.ts",
1679
+ sections: [
1680
+ {
1681
+ id: "banned",
1682
+ title: "Banned patterns",
1683
+ tagline: "// ... / // TODO / 'for brevity' / 'rest follows pattern' \u2014 HARD FAILURES.",
1684
+ body: `In code: // ..., // rest of code, // implement here, // TODO,
1685
+ /* ... */, // similar to above, // continue pattern, // add more
1686
+ as needed, bare ... standing for omitted code.
1687
+
1688
+ In prose: "Let me know if you want me to continue", "for brevity",
1689
+ "the rest follows the same pattern", "similarly for the remaining",
1690
+ "and so on" (replacing actual content), "I'll leave that as an
1691
+ exercise".
1692
+
1693
+ Structural: skeleton when full implementation was requested, first +
1694
+ last section skipping middle, describing what code should do instead
1695
+ of writing it.`
1696
+ },
1697
+ {
1698
+ id: "long-output-protocol",
1699
+ title: "Long-output protocol",
1700
+ tagline: "Write at full quality to clean breakpoint, then [PAUSED] marker, never compress.",
1701
+ body: `When response approaches token limit:
1702
+ - Do NOT compress remaining sections.
1703
+ - Write at FULL QUALITY up to clean breakpoint (end of function /
1704
+ file / section).
1705
+ - End with: [PAUSED \u2014 X of Y complete. Send "continue" to resume
1706
+ from: <section name>]
1707
+ - On "continue": pick up EXACTLY where stopped. No recap, no
1708
+ repetition.`
1709
+ }
1710
+ ]
1711
+ },
1712
+ // ── brandkit ───────────────────────────────────────────────────
1713
+ {
1714
+ id: "brandkit",
1715
+ name: "Brandkit \u2014 identity guidelines boards",
1716
+ whenToUse: "Designing a brand identity board first (before screens). Logo system, color palette, typography lockup, icon system, photography direction, brand voice.",
1717
+ source: "Leonxlnx/taste-skill/brandkit",
1718
+ sections: [
1719
+ {
1720
+ id: "principles",
1721
+ title: "Brandkit principles",
1722
+ tagline: "Premium brand-guidelines boards \u2014 minimalist / cinematic / editorial / dark-tech / luxury / cultural variants.",
1723
+ body: `Compositions for brand identity decks. Minimalist (workspace),
1724
+ cinematic (entertainment), editorial (publishing), dark-tech (SaaS),
1725
+ luxury (lifestyle), cultural (heritage), security (defense / fintech),
1726
+ gaming, developer-tool, consumer-app. Logo concepts with intentional
1727
+ symbolic meaning. Refined composition (asymmetric grid, generous
1728
+ breathing). Sparse typography. Premium mockups. Art-directed
1729
+ imagery. Flexible grid layouts.`
1730
+ }
1731
+ ]
1732
+ },
1733
+ // ── stitch ─────────────────────────────────────────────────────
1734
+ {
1735
+ id: "stitch",
1736
+ name: "Stitch \u2014 semantic DESIGN.md for Google Stitch",
1737
+ whenToUse: "Pairing with Google Stitch (or similar AI UI generator). Generate DESIGN.md files that enforce premium standards \u2014 strict typography, calibrated color, asymmetric layouts, perpetual micro-motion.",
1738
+ source: "Leonxlnx/taste-skill/stitch-skill",
1739
+ sections: [
1740
+ {
1741
+ id: "principles",
1742
+ title: "Stitch DESIGN.md principles",
1743
+ tagline: "Agent-friendly design specs \u2014 strict type, calibrated color, asymmetric layout, micro-motion, hardware acceleration.",
1744
+ body: `DESIGN.md = instruction set for downstream AI UI generators.
1745
+ Enforces: strict typography (no Inter, specific fonts named),
1746
+ calibrated color (specific hex, not "blue"), asymmetric layouts
1747
+ (specific grid template strings), perpetual micro-motion (specific
1748
+ timing functions), hardware-accelerated performance (transform/
1749
+ opacity only). Output is consumable by AI agents \u2014 explicit beats
1750
+ expressive.`
1751
+ }
1752
+ ]
1753
+ },
1754
+ // ── imagegen-mobile ────────────────────────────────────────────
1755
+ {
1756
+ id: "imagegen-mobile",
1757
+ name: "Imagegen mobile \u2014 app screen reference images",
1758
+ whenToUse: "Pre-code phase. Generate mobile screen mockups before implementing. Onboarding flows, auth, home dashboards, profile, settings, chat, ecommerce, fintech, health, productivity.",
1759
+ source: "Leonxlnx/taste-skill/imagegen-frontend-mobile",
1760
+ sections: [
1761
+ {
1762
+ id: "principles",
1763
+ title: "Mobile image direction principles",
1764
+ tagline: "App-native, premium, readable, flow-aware, platform-aware. Wrap in subtle premium phone mockup. Multi-screen consistency.",
1765
+ body: `Generate premium app-native mobile screen images + flow images
1766
+ (NOT generic AI mockups, NOT phone-shaped websites). Default mockup
1767
+ presence: subtle premium iPhone frame with visible chrome, focus
1768
+ stays on app content. Generate 3-5 screens per flow (onboarding,
1769
+ auth, home, detail, settings). Logical flow (each screen continues
1770
+ the user's task). First-screen cleanliness (don't dump every feature
1771
+ on the entry screen). Safe-area awareness (status bar + home
1772
+ indicator preserved). Mobile anti-tells: no purple-blue fintech
1773
+ gradients, no random glass cards, no ambient blobs, no fake neon, no
1774
+ dribbble floating widgets, no oversized corner radii on everything,
1775
+ no rainbow chip walls, no fake chart dashboard spam, no cloned
1776
+ screens in flows.`
1777
+ }
1778
+ ]
1779
+ },
1780
+ // ── imagegen-web ───────────────────────────────────────────────
1781
+ {
1782
+ id: "imagegen-web",
1783
+ name: "Imagegen web \u2014 landing page section images",
1784
+ whenToUse: "Pre-code phase for landing / marketing sites. Generate ONE image per section (8 sections \u2192 8 images). Hero composition variety (NOT always left-text/right-image).",
1785
+ source: "Leonxlnx/taste-skill/imagegen-frontend-web",
1786
+ sections: [
1787
+ {
1788
+ id: "hard-output-rule",
1789
+ title: "Hard output rule \u2014 one image per section",
1790
+ tagline: "8 sections requested \u2192 8 separate images. NEVER combine sections.",
1791
+ body: `Each image = one section, own image call. NEVER combine multiple
1792
+ sections into one frame. NEVER return a single tall image with the
1793
+ whole page. Default to 6 sections if "landing page" with no count.
1794
+ 8 sections for "full website template". Announce each ("Section 1
1795
+ of 8: Hero", "Section 2 of 8: Trust bar").`
1796
+ },
1797
+ {
1798
+ id: "hero-composition-bias",
1799
+ title: "Hero composition variety",
1800
+ tagline: "Left-text / right-image hero is the most overused AI pattern. Pick from 10 alternatives first.",
1801
+ body: `Before reaching for left-text/right-image hero, consider:
1802
+ - centered over background image
1803
+ - bottom-left over image
1804
+ - bottom-right over image
1805
+ - top-left lead
1806
+ - stacked center
1807
+ - image-as-canvas
1808
+ - off-grid editorial
1809
+ - mini minimalist
1810
+ - right-text / left-image (inverted classic)
1811
+ Use left-text/right-image ONLY when genuinely the strongest choice
1812
+ for the brand.`
1813
+ }
1814
+ ]
1815
+ },
1816
+ // ── image-to-code ──────────────────────────────────────────────
1817
+ {
1818
+ id: "image-to-code",
1819
+ name: "Image-to-code \u2014 generate design first, then implement",
1820
+ whenToUse: "Visual-first brief in Codex. First generate the design image yourself, deeply analyze, THEN implement code matching it.",
1821
+ source: "Leonxlnx/taste-skill/image-to-code-skill",
1822
+ sections: [
1823
+ {
1824
+ id: "workflow",
1825
+ title: "Image-to-code workflow",
1826
+ tagline: "Generate design image \u2192 analyze \u2192 implement. Prefer large readable section-specific images.",
1827
+ body: `Workflow:
1828
+ 1. Generate the design image FIRST (one per section, large + readable).
1829
+ 2. Deeply analyze: composition, hierarchy, palette, typography, motion.
1830
+ 3. Implement React/HTML/CSS matching as closely as possible.
1831
+
1832
+ Prefer LARGE, readable, section-specific images over tiny compressed
1833
+ boards. Generate fresh standalone images for sections / detail views
1834
+ instead of cropping old. Avoid lazy under-generation. Avoid cards-
1835
+ inside-cards-inside-cards UI. Keep the hero clean, spacious, readable,
1836
+ visible on a small laptop.`
1837
+ }
1838
+ ]
1839
+ }
1840
+ ];
1841
+ function findSkill(id) {
1842
+ return SKILLS.find((s) => s.id === id);
1843
+ }
1844
+ function findSection(skillId, sectionId) {
1845
+ return findSkill(skillId)?.sections.find((s) => s.id === sectionId);
1846
+ }
1847
+ function routeTask(task) {
1848
+ const q = task.toLowerCase();
1849
+ const matches = [];
1850
+ const route = (kw, skill, section, why, alsoSee) => {
1851
+ if (kw.some((k) => q.includes(k))) matches.push({ skill, section, why, alsoSee });
1852
+ };
1853
+ route(
1854
+ ["premium", "awwwards", "agency", "linear", "apple", "high-end", "luxury"],
1855
+ "soft",
1856
+ "vibe-archetypes",
1857
+ "Premium tier \u2014 pick a Vibe + Layout archetype + apply Double-Bezel.",
1858
+ ["soft/double-bezel", "soft/magnetic-hover"]
1859
+ );
1860
+ route(
1861
+ ["landing page", "marketing", "hero", "long scroll"],
1862
+ "imagegen-web",
1863
+ "hero-composition-bias",
1864
+ "Landing pages benefit from hero composition variety + per-section image generation.",
1865
+ ["gpt-tasteskill/principles", "soft/layout-archetypes"]
1866
+ );
1867
+ route(
1868
+ ["mobile app", "ios", "android", "phone screen", "onboarding flow"],
1869
+ "imagegen-mobile",
1870
+ "principles",
1871
+ "Mobile app design \u2014 generate screens first, avoid phone-shaped-website.",
1872
+ ["taste/mobile-first"]
1873
+ );
1874
+ route(
1875
+ ["workspace", "notion", "document", "editorial", "knowledge base"],
1876
+ "minimalist",
1877
+ "palette",
1878
+ "Editorial workspace = warm monochrome + spot pastels + serif headings.",
1879
+ ["minimalist/typography", "minimalist/bento-grids"]
1880
+ );
1881
+ route(
1882
+ ["dashboard", "data heavy", "tabular", "ops table"],
1883
+ "brutalist",
1884
+ "principles",
1885
+ "Data-heavy dashboards work with Brutalist (rigid grids, utilitarian color).",
1886
+ ["taste/one-intent-per-screen"]
1887
+ );
1888
+ route(
1889
+ ["brand", "identity", "logo", "guidelines"],
1890
+ "brandkit",
1891
+ "principles",
1892
+ "Brand identity work \u2014 boards before screens."
1893
+ );
1894
+ route(
1895
+ ["refactor", "redesign", "upgrade existing", "audit"],
1896
+ "redesign",
1897
+ "fix-priority",
1898
+ "Existing project = run audit first, fix in priority order (font \u2192 palette \u2192 states \u2192 ...).",
1899
+ ["redesign/audit-checklist"]
1900
+ );
1901
+ route(
1902
+ ["form", "validation", "submit", "sign up", "registration"],
1903
+ "taste",
1904
+ "form-discipline",
1905
+ "Form must have explicit label + help + error wired via FormField (rule 34)."
1906
+ );
1907
+ route(
1908
+ ["loading", "saving", "skeleton", "spinner"],
1909
+ "taste",
1910
+ "loading-states",
1911
+ "Skeleton for INIT fetch, Spinner for active work. Never mix."
1912
+ );
1913
+ route(
1914
+ ["mobile first", "responsive", "breakpoint"],
1915
+ "taste",
1916
+ "mobile-first",
1917
+ "Default styles target xs. Touch targets \u2265 44px. Use useBreakpoint()."
1918
+ );
1919
+ route(
1920
+ ["complete code", "full implementation", "no placeholder"],
1921
+ "output",
1922
+ "banned",
1923
+ "Banned: // ..., // TODO, 'for brevity'. Ship complete runnable code."
1924
+ );
1925
+ route(
1926
+ ["gsap", "scrolltrigger", "scroll choreography", "pinning"],
1927
+ "gpt-tasteskill",
1928
+ "principles",
1929
+ "GSAP ScrollTrigger \u2014 pinning, stacking, scrubbing."
1930
+ );
1931
+ route(
1932
+ ["from image", "image to code", "design first"],
1933
+ "image-to-code",
1934
+ "workflow",
1935
+ "Generate design image first \u2192 analyze \u2192 implement."
1936
+ );
1937
+ if (matches.length === 0) {
1938
+ return [{
1939
+ skill: "taste",
1940
+ section: "<see whenToUse>",
1941
+ why: `No keyword match for "${task}". Default to the "taste" baseline \u2014 see whenToUse for sections.`
1942
+ }];
1943
+ }
1944
+ return matches;
1945
+ }
1946
+
1947
+ // src/data/anti-ai-tells.ts
1948
+ var ANTI_AI_TELLS = [
1949
+ // ── visual ─────────────────────────────────────────────────────
1950
+ {
1951
+ category: "visual",
1952
+ name: "Purple-blue gradient hero",
1953
+ body: `The default LLM color palette \u2014 purple \u2192 blue \u2192 cyan radial /
1954
+ linear gradient as hero background. Looks like every AI-generated
1955
+ SaaS landing page from 2023.`,
1956
+ fix: `Use the framework's accent palette (\`data-accent="blue"\` /
1957
+ "violet" / "cyan" / "green" / "orange" / "rose"). Solid surface
1958
+ colors with semantic meaning. If you want depth, use a SINGLE
1959
+ subtle gradient that supports brand (not decoration).`
1960
+ },
1961
+ {
1962
+ category: "visual",
1963
+ name: "Glassmorphism without purpose",
1964
+ body: `Frosted-glass cards stacked on a colorful blurry background.
1965
+ Looked novel in 2020 \u2014 now a tell that the designer reached for
1966
+ trend instead of solving a problem.`,
1967
+ fix: `Solid surface tiers (Card on background, Popover on Card,
1968
+ Dialog on backdrop). The framework's elevation system already
1969
+ encodes 3 surface tiers \u2014 use them.`
1970
+ },
1971
+ {
1972
+ category: "visual",
1973
+ name: "Ambient blobs / floating shapes",
1974
+ body: `Random gradient blobs floating behind content with no narrative
1975
+ purpose. The "creative space-filler" AI pattern. Reads as
1976
+ distracting noise.`,
1977
+ fix: `If the page needs visual interest, use a REAL image (product
1978
+ photo, founder photo, branded illustration). If you need
1979
+ "breathing room", use whitespace. Never use shapes as filler.`
1980
+ },
1981
+ {
1982
+ category: "visual",
1983
+ name: "Oversized border-radius on everything",
1984
+ body: `Every Card / Button / Input with \`border-radius: 24px\`. Reads
1985
+ as "I picked one radius and applied it globally". Premium design
1986
+ uses ROLES \u2014 small radius on inputs (4-6px), medium on cards
1987
+ (8-12px), pill on chips (full).`,
1988
+ fix: `Use the framework's radius scale (\`--radius-sm | -md | -lg | -full\`).
1989
+ Each primitive defaults to the right role; only override when the
1990
+ design canon specifically calls for it.`
1991
+ },
1992
+ {
1993
+ category: "visual",
1994
+ name: "Rainbow chip wall",
1995
+ body: `Row of Tags / Badges each in a different color (red, orange,
1996
+ yellow, green, blue, purple) \u2014 usually navigation or filter
1997
+ categories. Reads as chaos; eye can't anchor.`,
1998
+ fix: `Pick ONE accent for the tag row. Use \`appearance\` ("soft" vs
1999
+ "solid" vs "outline") for variety within the same hue. Reserve
2000
+ non-neutral colors (success / warning / destructive) for tags
2001
+ that genuinely carry that meaning.`
2002
+ },
2003
+ // ── layout ─────────────────────────────────────────────────────
2004
+ {
2005
+ category: "layout",
2006
+ name: "8-card stat dashboard",
2007
+ body: `Homepage with a 4x2 grid of "stat cards" \u2014 each with an icon, a
2008
+ number, a sparkline, a delta. None of them relate to a real
2009
+ business question; they were chosen because "more cards = more
2010
+ data". Classic AI dashboard slop.`,
2011
+ fix: `Show 1-2 hero metrics (the ones executives ASK about), then the
2012
+ top action list (orders waiting, tasks due, alerts). If the user
2013
+ needs more analytics, link to a dedicated Reports page.`
2014
+ },
2015
+ {
2016
+ category: "layout",
2017
+ name: "Phone-shaped website",
2018
+ body: `Mobile screen rendered as a vertical strip with the same density
2019
+ + same layout as desktop \u2014 just narrower. Cramped tap targets,
2020
+ horizontal scrolling for overflow, no system bar awareness.`,
2021
+ fix: `Mobile is its OWN design. Use full-width inputs (\`block\` Button),
2022
+ stacked layout, larger tap targets, Sheet/Drawer for secondary
2023
+ content, system-bar safe area. The framework's \`useBreakpoint\`
2024
+ + Tailwind \`sm:\` variants give you the canvas.`
2025
+ },
2026
+ {
2027
+ category: "layout",
2028
+ name: "Wall-of-tabs navigation",
2029
+ body: `10+ tabs at the top of a screen, no priority. User has to read all
2030
+ of them to find the right one. AI default: "more tabs = more
2031
+ features = better".`,
2032
+ fix: `2-4 tabs max. If you have more categories, use a sidebar (Menu),
2033
+ or a Cascader / Tree picker. Tabs are for switching between PEERS
2034
+ (2-4 mutually exclusive views of the same data).`
2035
+ },
2036
+ {
2037
+ category: "layout",
2038
+ name: "Identical clone screens",
2039
+ body: `5 onboarding steps where every screen has the same headline +
2040
+ illustration + 2 buttons layout. Reads as "I copy-pasted the
2041
+ template" \u2014 and devalues the user's time at each step.`,
2042
+ fix: `Each step has a distinct visual + interactive feel. Step 1 might
2043
+ be a centered question, step 2 a side-by-side comparison, step 3
2044
+ a multi-field form, step 4 a single yes/no card. Same palette +
2045
+ type system for coherence; different composition for engagement.`
2046
+ },
2047
+ // ── copy ───────────────────────────────────────────────────────
2048
+ {
2049
+ category: "copy",
2050
+ name: "Filler corporate phrases",
2051
+ body: `"Elevate your potential", "unlock seamless productivity",
2052
+ "transform your workflow", "next-generation experience". Reads as
2053
+ nothing because it MEANS nothing.`,
2054
+ fix: `Write what the feature DOES, specifically. "Sync 1,000 rows in 2
2055
+ seconds" beats "Lightning-fast performance". "Replaces 3 manual
2056
+ steps" beats "Streamline your workflow".`
2057
+ },
2058
+ {
2059
+ category: "copy",
2060
+ name: "Generic brand placeholders",
2061
+ body: `Acme, NovaCore, Flowbit, Quantix, VeloPay, Lumen, Apex \u2014 the
2062
+ go-to AI brand names that scream "I couldn't think of one".`,
2063
+ fix: `Use believable real-sounding names: \u682A\u5F0F\u4F1A\u793EABC\u5546\u4E8B, Tanaka
2064
+ Trading, Yokohama Coffee Roasters, Mountain View Bakery. Or use
2065
+ your actual project's brand if known.`
2066
+ },
2067
+ {
2068
+ category: "copy",
2069
+ name: "Vague empty-state copy",
2070
+ body: `"Get started", "Begin your journey", "No items yet" \u2014 without
2071
+ saying WHAT to do or WHY there's nothing.`,
2072
+ fix: `Be specific + actionable: "\u307E\u3060\u6CE8\u6587\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u5546\u54C1\u3092\u8FFD\u52A0\u3057\u3066
2073
+ \u6700\u521D\u306E\u6CE8\u6587\u3092\u4F5C\u6210\u3057\u307E\u3057\u3087\u3046\u3002" + a clear next-action Button.
2074
+ Empty states are TEACHING MOMENTS \u2014 use them to onboard.`
2075
+ },
2076
+ {
2077
+ category: "copy",
2078
+ name: "Apologetic / passive-voice error messages",
2079
+ body: `"Sorry, something went wrong" / "An error has occurred" \u2014 no
2080
+ information about WHAT, no recovery action.`,
2081
+ fix: `Specific + actionable: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u306E\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093
2082
+ (\u4F8B: name@example.com)". For server errors: "\u901A\u4FE1\u30A8\u30E9\u30FC
2083
+ (\u518D\u8A66\u884C \u30DC\u30BF\u30F3)". Never apologise if you can't say what failed
2084
+ or what to do.`
2085
+ },
2086
+ // ── interaction ────────────────────────────────────────────────
2087
+ {
2088
+ category: "interaction",
2089
+ name: "Hover-only affordances",
2090
+ body: `Action buttons that only appear on hover (table row actions
2091
+ hidden until mouseover). Breaks on mobile (no hover), inaccessible
2092
+ (keyboard users can't discover).`,
2093
+ fix: `Show actions inline or in a kebab menu (DropdownMenu) that's
2094
+ ALWAYS visible. If you must hide on desktop for density, ensure
2095
+ the same actions are reachable via keyboard (Tab to row, Enter to
2096
+ expand a row-actions DropdownMenu).`
2097
+ },
2098
+ {
2099
+ category: "interaction",
2100
+ name: "Auto-advancing carousel",
2101
+ body: `Hero carousel that rotates every 3 seconds. Users haven't
2102
+ finished reading slide 1; now slide 2 is gone. Accessibility
2103
+ nightmare (cognitive load, motion-sensitive).`,
2104
+ fix: `Carousel ONLY rotates on explicit user action (arrow click,
2105
+ dot click, swipe). \`<Carousel autoplay={false}>\` is the default
2106
+ in the framework for this reason.`
2107
+ },
2108
+ {
2109
+ category: "interaction",
2110
+ name: "Drag-without-handle",
2111
+ body: `Cards / list items reorderable by long-press anywhere \u2014 no
2112
+ visual indicator that they ARE draggable. Users discover it by
2113
+ accident or never.`,
2114
+ fix: `Show a drag handle icon (\`<GripVertical>\`) on the left of the
2115
+ row. Users see it, understand "this row is draggable", reach for
2116
+ it deliberately.`
2117
+ },
2118
+ {
2119
+ category: "interaction",
2120
+ name: "Disappearing focus ring",
2121
+ body: `\`outline: none\` on focus to "look cleaner". Keyboard users
2122
+ can't see where they are; total navigation failure.`,
2123
+ fix: `Use \`:focus-visible\` (Radix primitives do automatically) so the
2124
+ ring shows on keyboard focus, hides on mouse-click. Don't strip.`
2125
+ },
2126
+ // ── imagery ────────────────────────────────────────────────────
2127
+ {
2128
+ category: "imagery",
2129
+ name: "Stock photo of generic smiling team",
2130
+ body: `Empty state / About page with a photo of a "diverse team in an
2131
+ open office laughing at a laptop". Reads as 2010 corporate stock.
2132
+ No relationship to your product.`,
2133
+ fix: `Real photos of YOUR team / users (with consent), product
2134
+ screenshots, branded illustrations. Avatar's INITIALS fallback is
2135
+ better than a generic stock person.`
2136
+ },
2137
+ {
2138
+ category: "imagery",
2139
+ name: "Floating 3D crypto icon",
2140
+ body: `Empty state with a chrome / pastel 3D icon (coin, key, shield)
2141
+ floating in the center. Looks like every NFT marketplace from
2142
+ 2021.`,
2143
+ fix: `Simple lucide-react line icon (\`<Inbox size={48} />\`) +
2144
+ descriptive title. Or a flat illustration matching the brand
2145
+ palette. Skip the 3D entirely unless your brand IS 3D.`
2146
+ },
2147
+ {
2148
+ category: "imagery",
2149
+ name: "Decorative gradient mesh background",
2150
+ body: `Page sections with a colorful gradient mesh ("Stripe-style")
2151
+ behind text. Looks "premium" until you realize every AI design
2152
+ uses it. Often hurts text contrast.`,
2153
+ fix: `Solid background (\`--background\`). If you need depth, use a
2154
+ subtle 1px border + \`--card\` background tint. Reserve high-effort
2155
+ backgrounds for pages where they matter (marketing hero, product
2156
+ showcase) \u2014 not every internal screen.`
2157
+ },
2158
+ // ── structure ──────────────────────────────────────────────────
2159
+ {
2160
+ category: "structure",
2161
+ name: "Settings as one long form",
2162
+ body: `Settings page with 40 form fields in a single scroll. User
2163
+ loses their place, can't find the field they came for.`,
2164
+ fix: `Section the form with \`<Typography.Title size={5}>\` subheaders
2165
+ + \`<Separator />\`. Group by concern (\u57FA\u672C\u60C5\u5831 / \u516C\u958B\u7BC4\u56F2 / \u901A\u77E5 /
2166
+ \u30BB\u30AD\u30E5\u30EA\u30C6\u30A3). If 40 fields is still too many, split into Tabs
2167
+ or a Sidebar-driven multi-page settings flow.`
2168
+ },
2169
+ {
2170
+ category: "structure",
2171
+ name: "Modal-in-modal-in-modal",
2172
+ body: `Click a Button \u2192 Dialog opens \u2192 click "Edit" \u2192 another Dialog
2173
+ opens \u2192 click "Confirm" \u2192 AlertDialog opens. Triple stack;
2174
+ user loses context.`,
2175
+ fix: `Use Sheet for the FIRST level (side panel), Dialog for the
2176
+ confirm. Or, redesign the flow so the edit is INLINE in the
2177
+ first Dialog (no second Dialog needed). AlertDialog for confirm
2178
+ is correct \u2014 but ONE deep, not three.`
2179
+ },
2180
+ {
2181
+ category: "structure",
2182
+ name: "Spinner-only loading state",
2183
+ body: `Page-level spinner while data loads. User stares at an empty
2184
+ shell with a centered spinner. Layout shifts when content
2185
+ arrives.`,
2186
+ fix: `Use Skeleton placeholders matching the eventual content shape.
2187
+ The framework's \`<Form loading={{ kind: "skeleton" }}>\` cascades
2188
+ to every field; \`<Skeleton className="h-9 w-full rounded-md" />\`
2189
+ for individual blocks. Layout stays stable, perceived speed
2190
+ improves.`
2191
+ }
2192
+ ];
2193
+ function aiTellsByCategory(cat) {
2194
+ return ANTI_AI_TELLS.filter((t) => t.category === cat);
2195
+ }
2196
+
2197
+ // src/data/redesign-audit.ts
2198
+ var REDESIGN_CHECKS = [
2199
+ // ── typography ─────────────────────────────────────────────────
2200
+ {
2201
+ category: "typography",
2202
+ symptom: "Inter / Roboto / Open Sans everywhere \u2014 the AI default.",
2203
+ fix: "Pick a font with character: Geist, Outfit, Cabinet Grotesk, Satoshi for sans. For editorial / creative \u2014 pair a serif heading (Newsreader, Lyon, Playfair) with a sans body.",
2204
+ uiNote: "Override --font-sans + --font-serif at the consumer's root CSS. Framework reads from these tokens."
2205
+ },
2206
+ {
2207
+ category: "typography",
2208
+ symptom: "Headlines lack presence \u2014 small + thin + default tracking.",
2209
+ fix: "Increase display size, tighten letter-spacing (-0.02em to -0.04em), reduce line-height (1.1). Headlines should feel HEAVY and INTENTIONAL.",
2210
+ uiNote: "Typography.Title size={1} for hero; override fontFamily + letterSpacing inline."
2211
+ },
2212
+ {
2213
+ category: "typography",
2214
+ symptom: "Body paragraphs full-width \u2014 hard to read.",
2215
+ fix: "Limit paragraph max-width to ~65ch. Increase line-height to 1.6+.",
2216
+ uiNote: "Wrap Typography.Paragraph in `<div style={{ maxWidth: '65ch' }}>`."
2217
+ },
2218
+ {
2219
+ category: "typography",
2220
+ symptom: "Only Regular (400) + Bold (700) weights \u2014 flat hierarchy.",
2221
+ fix: "Introduce Medium (500) + SemiBold (600) for subtle weight contrasts."
2222
+ },
2223
+ {
2224
+ category: "typography",
2225
+ symptom: "Numbers in proportional font \u2014 columns jitter in tables.",
2226
+ fix: "`font-variant-numeric: tabular-nums` for data, or a monospace font like Geist Mono.",
2227
+ uiNote: "Table primitive already uses `tabular-nums` on `.num` cells. For ad-hoc numeric labels, add the CSS prop manually."
2228
+ },
2229
+ {
2230
+ category: "typography",
2231
+ symptom: "Orphaned words \u2014 single word on the last line of a heading.",
2232
+ fix: "`text-wrap: balance` (h1/h2/h3) or `text-wrap: pretty` (body)."
2233
+ },
2234
+ {
2235
+ category: "typography",
2236
+ symptom: "Title Case On Every Header.",
2237
+ fix: "Use sentence case instead. More modern, easier to read."
2238
+ },
2239
+ // ── color / surface ────────────────────────────────────────────
2240
+ {
2241
+ category: "color-surface",
2242
+ symptom: "Pure #000000 background.",
2243
+ fix: "Replace with off-black (#0A0A0A) / dark charcoal (#121212) / tinted dark (deep navy).",
2244
+ uiNote: "Framework dark theme already uses tinted dark values \u2014 verify the consumer's override didn't force pure black."
2245
+ },
2246
+ {
2247
+ category: "color-surface",
2248
+ symptom: "Oversaturated accent colors.",
2249
+ fix: "Keep saturation below 80%. Desaturate so accents BLEND with neutrals rather than scream."
2250
+ },
2251
+ {
2252
+ category: "color-surface",
2253
+ symptom: "More than one accent color competing.",
2254
+ fix: "Pick ONE. Remove the rest. Consistency beats variety in palette.",
2255
+ uiNote: "Set ONE `data-accent` at `<html>` root. Use semantic colors (success / warning / destructive) only for genuinely semantic content."
2256
+ },
2257
+ {
2258
+ category: "color-surface",
2259
+ symptom: "Purple/blue 'AI gradient' aesthetic \u2014 most common AI fingerprint.",
2260
+ fix: "Replace with neutral base + ONE considered accent. Drop the gradient entirely if it has no narrative purpose."
2261
+ },
2262
+ {
2263
+ category: "color-surface",
2264
+ symptom: "Generic black `box-shadow` everywhere.",
2265
+ fix: "Tint shadow to match background hue (e.g. cool gray bg \u2192 cool gray shadow). Colored shadows over pure black."
2266
+ },
2267
+ {
2268
+ category: "color-surface",
2269
+ symptom: "Random dark section breaking an otherwise light page.",
2270
+ fix: "Either commit to full dark mode OR keep light consistently. If contrast needed, use a SLIGHTLY darker shade of the same palette \u2014 not a sudden jump to #111."
2271
+ },
2272
+ {
2273
+ category: "color-surface",
2274
+ symptom: "Empty flat sections with no visual depth.",
2275
+ fix: "Add subtle background imagery at low opacity (`/picsum.photos/seed/{name}/1920/1080`) OR ambient gradient at 0.02-0.05 opacity. Empty flat = unfinished."
2276
+ },
2277
+ // ── layout ─────────────────────────────────────────────────────
2278
+ {
2279
+ category: "layout",
2280
+ symptom: "Everything centered + symmetric.",
2281
+ fix: "Break symmetry: offset margins, mixed aspect ratios, left-aligned header over centered body."
2282
+ },
2283
+ {
2284
+ category: "layout",
2285
+ symptom: "Three equal card columns as feature row \u2014 the most generic AI layout.",
2286
+ fix: "Replace with 2-column zig-zag, asymmetric grid, horizontal scroll, or masonry. The 3-equal-cols pattern is RED FLAG #1.",
2287
+ uiNote: "Use Bento Grid (custom CSS grid with `gridColumn: 'span N'`) instead of `<Grid cols={3}>` for hero sections."
2288
+ },
2289
+ {
2290
+ category: "layout",
2291
+ symptom: "`height: 100vh` causing iOS Safari jump.",
2292
+ fix: "Use `min-height: 100dvh` (dynamic viewport) instead."
2293
+ },
2294
+ {
2295
+ category: "layout",
2296
+ symptom: "No max-width container \u2014 content stretches edge-to-edge.",
2297
+ fix: "Add a container constraint (1200-1440px) with `margin: auto`. Or use `max-w-4xl / max-w-5xl` for content-heavy pages.",
2298
+ uiNote: "Framework's PageContent constrains via `var(--container-max-width)`. Consumer may override."
2299
+ },
2300
+ {
2301
+ category: "layout",
2302
+ symptom: "Cards forced to same height by flexbox.",
2303
+ fix: "Allow variable heights or use masonry when content varies.",
2304
+ uiNote: "Use Masonry primitive \u2014 handles variable heights without flexbox stretch."
2305
+ },
2306
+ {
2307
+ category: "layout",
2308
+ symptom: "Buttons at random vertical positions in card rows.",
2309
+ fix: "Pin CTAs to card bottom \u2014 same Y-position across the row regardless of content above.",
2310
+ uiNote: "Card's `actions` footer slot bottom-aligns automatically."
2311
+ },
2312
+ {
2313
+ category: "layout",
2314
+ symptom: "Feature lists starting at different vertical positions in pricing tables.",
2315
+ fix: "Fixed-height title/price block + consistent spacing above the feature list. Cards align across columns."
2316
+ },
2317
+ {
2318
+ category: "layout",
2319
+ symptom: "Dashboard ALWAYS has a left sidebar.",
2320
+ fix: "Consider top navigation, floating command menu, or collapsible panel. Sidebar isn't the only chrome.",
2321
+ uiNote: "Framework supports both \u2014 AppShell with sidebar slot is optional; can use Topbar-only for some flows."
2322
+ },
2323
+ // ── interactivity ──────────────────────────────────────────────
2324
+ {
2325
+ category: "interactivity",
2326
+ symptom: "No hover states on buttons.",
2327
+ fix: "Background shift, scale, or translate on hover \u2014 150-200ms ease.",
2328
+ uiNote: "Framework Button has built-in hover. If overridden \u2014 restore."
2329
+ },
2330
+ {
2331
+ category: "interactivity",
2332
+ symptom: "No active/pressed feedback.",
2333
+ fix: "`scale(0.98)` or `translateY(1px)` on `:active`. Simulates a physical click."
2334
+ },
2335
+ {
2336
+ category: "interactivity",
2337
+ symptom: "No focus ring (`outline: none`).",
2338
+ fix: "Restore visible `:focus-visible` ring. Accessibility requirement, not optional."
2339
+ },
2340
+ {
2341
+ category: "interactivity",
2342
+ symptom: "Generic circular spinner for page-level loading.",
2343
+ fix: "Replace with Skeleton placeholders matching the eventual content shape.",
2344
+ uiNote: "Framework Skeleton + Form `loading={{ kind: 'skeleton' }}` handles cascading initial-fetch state."
2345
+ },
2346
+ {
2347
+ category: "interactivity",
2348
+ symptom: "No empty states \u2014 empty dashboard shows nothing.",
2349
+ fix: "Design a composed 'getting started' view: Empty primitive with title + description + next-action button."
2350
+ },
2351
+ {
2352
+ category: "interactivity",
2353
+ symptom: "`window.alert()` for errors.",
2354
+ fix: "Inline error in the relevant Field, OR toast for non-form errors, OR Dialog for blocking errors."
2355
+ },
2356
+ {
2357
+ category: "interactivity",
2358
+ symptom: "Dead links (`href='#'`).",
2359
+ fix: "Either link to real destinations or visually disable the button."
2360
+ },
2361
+ {
2362
+ category: "interactivity",
2363
+ symptom: "No indication of current page in navigation.",
2364
+ fix: "Style the active nav link distinctly.",
2365
+ uiNote: "Sidebar handles via `activeId` \u2014 pass it."
2366
+ },
2367
+ // ── content ────────────────────────────────────────────────────
2368
+ {
2369
+ category: "content",
2370
+ symptom: "Generic names \u2014 'John Doe', 'Jane Smith'.",
2371
+ fix: "Diverse, realistic names. For Japanese apps: \u7530\u4E2D \u592A\u90CE, \u4F50\u85E4 \u7F8E\u54B2, Nguy\u1EC5n Lan, Maria Cruz."
2372
+ },
2373
+ {
2374
+ category: "content",
2375
+ symptom: "Fake round numbers \u2014 '99.99%', '50%', '$100.00'.",
2376
+ fix: "Organic data: '47.2%', '$99.00', '+1 (312) 847-1928'."
2377
+ },
2378
+ {
2379
+ category: "content",
2380
+ symptom: "Placeholder brand names \u2014 Acme, Nexus, SmartFlow.",
2381
+ fix: "Invent contextual believable brands or use the consumer's real brand."
2382
+ },
2383
+ {
2384
+ category: "content",
2385
+ symptom: "AI copy clich\xE9s \u2014 'elevate', 'seamless', 'unleash', 'next-gen', 'game-changer', 'delve', 'tapestry', 'in the world of'.",
2386
+ fix: "Plain specific language. Numbers, nouns, verbs.",
2387
+ uiNote: "Framework's cardinal rule 9 bans this in framework docs; same discipline applies to consumer copy."
2388
+ },
2389
+ {
2390
+ category: "content",
2391
+ symptom: "Exclamation marks in success messages.",
2392
+ fix: "Remove. Be confident, not loud."
2393
+ },
2394
+ {
2395
+ category: "content",
2396
+ symptom: "'Oops!' or apologetic error messages.",
2397
+ fix: "Direct + specific: 'Connection failed. Please try again.' / '\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u306E\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093'."
2398
+ },
2399
+ {
2400
+ category: "content",
2401
+ symptom: "Lorem Ipsum.",
2402
+ fix: "Real draft copy. Even rough placeholder beats Latin."
2403
+ },
2404
+ // ── components ─────────────────────────────────────────────────
2405
+ {
2406
+ category: "components",
2407
+ symptom: "Generic card look (border + shadow + white).",
2408
+ fix: "Remove border OR shadow OR background \u2014 keep ONE. Cards exist only when elevation communicates hierarchy."
2409
+ },
2410
+ {
2411
+ category: "components",
2412
+ symptom: "Always one filled + one ghost button.",
2413
+ fix: "Add text links / tertiary styles for variety.",
2414
+ uiNote: "Button has `variant='link'` for tertiary actions."
2415
+ },
2416
+ {
2417
+ category: "components",
2418
+ symptom: "3-card carousel testimonials with dots.",
2419
+ fix: "Replace with masonry wall of quotes, embedded social posts, or single rotating quote."
2420
+ },
2421
+ {
2422
+ category: "components",
2423
+ symptom: "Pricing table with 3 equal towers.",
2424
+ fix: "Highlight recommended tier with COLOR and emphasis, not just extra height."
2425
+ },
2426
+ {
2427
+ category: "components",
2428
+ symptom: "Modals for everything.",
2429
+ fix: "Use inline editing, Sheet (slide-over), or expandable Collapse for simple actions. Reserve Dialog for true blocking decisions."
2430
+ },
2431
+ {
2432
+ category: "components",
2433
+ symptom: "Footer link farm with 4 columns.",
2434
+ fix: "Simplify. Main nav paths + legally required links. No marketing kitchen sink."
2435
+ },
2436
+ // ── iconography ────────────────────────────────────────────────
2437
+ {
2438
+ category: "iconography",
2439
+ symptom: "Lucide or Feather icons exclusively.",
2440
+ fix: "Use Phosphor (Bold / Fill), Heroicons, or a custom set. AI default tell.",
2441
+ uiNote: "Framework ships with lucide as locked dependency (rule 14). For editorial differentiation, layer Phosphor on top."
2442
+ },
2443
+ {
2444
+ category: "iconography",
2445
+ symptom: "Cliche icon metaphors \u2014 rocketship 'launch', shield 'security'.",
2446
+ fix: "Less obvious: bolt, fingerprint, spark, vault, gem."
2447
+ },
2448
+ {
2449
+ category: "iconography",
2450
+ symptom: "Stock 'diverse team in office' photo.",
2451
+ fix: "Real team photos, candid shots, or a consistent illustration style. Avatar initials fallback > generic stock person."
2452
+ },
2453
+ // ── code quality ───────────────────────────────────────────────
2454
+ {
2455
+ category: "code-quality",
2456
+ symptom: "Div soup \u2014 no semantic HTML.",
2457
+ fix: "`<nav>`, `<main>`, `<article>`, `<aside>`, `<section>` for landmarks.",
2458
+ uiNote: "AppShell renders the canonical landmark structure automatically."
2459
+ },
2460
+ {
2461
+ category: "code-quality",
2462
+ symptom: "Inline styles mixed with CSS classes haphazardly.",
2463
+ fix: "Move styling into the project's system. Inline `style={{}}` only for layout / positioning (rule 29)."
2464
+ },
2465
+ {
2466
+ category: "code-quality",
2467
+ symptom: "Missing alt text on images.",
2468
+ fix: "Describe content for SR. Never leave `alt=''` or `alt='image'` on meaningful images."
2469
+ },
2470
+ {
2471
+ category: "code-quality",
2472
+ symptom: "Arbitrary z-index values like `9999`.",
2473
+ fix: "Establish a clean z-index scale in CSS variables."
2474
+ },
2475
+ // ── strategic omissions ────────────────────────────────────────
2476
+ {
2477
+ category: "omissions",
2478
+ symptom: "No legal links in footer.",
2479
+ fix: "Add Privacy Policy + Terms of Service."
2480
+ },
2481
+ {
2482
+ category: "omissions",
2483
+ symptom: "Dead ends in user flows \u2014 no 'back'.",
2484
+ fix: "Every page has a way back. Breadcrumb, back button, OR clear nav state."
2485
+ },
2486
+ {
2487
+ category: "omissions",
2488
+ symptom: "No custom 404 page.",
2489
+ fix: "Design a helpful branded 404 with a way home and search."
2490
+ },
2491
+ {
2492
+ category: "omissions",
2493
+ symptom: "No form validation.",
2494
+ fix: "Client-side validation via zod schema. Framework's Form + FormField handle field-level errors automatically."
2495
+ },
2496
+ {
2497
+ category: "omissions",
2498
+ symptom: "No 'skip to content' link.",
2499
+ fix: "Hidden skip-link, first focusable element. Essential for keyboard users.",
2500
+ uiNote: "AppShell renders one automatically."
2501
+ }
2502
+ ];
2503
+ var FIX_PRIORITY = [
2504
+ "1. Font swap \u2014 biggest instant improvement, lowest risk",
2505
+ "2. Color palette cleanup \u2014 remove clashing / oversaturated colors",
2506
+ "3. Hover + active states \u2014 makes the interface feel alive",
2507
+ "4. Layout + spacing \u2014 proper grid, max-width, consistent padding",
2508
+ "5. Replace generic components \u2014 swap cliche patterns for modern alternatives",
2509
+ "6. Add loading, empty, error states \u2014 makes it feel finished",
2510
+ "7. Polish typography scale + spacing \u2014 the premium final touch"
2511
+ ];
2512
+ var REDESIGN_RULES = [
2513
+ "Work with the existing tech stack. Do NOT migrate frameworks or styling libraries.",
2514
+ "Do NOT break existing functionality. Test after every change.",
2515
+ "Before importing any new library, check `package.json` first.",
2516
+ "Keep changes reviewable + focused. Small targeted improvements over big rewrites.",
2517
+ "Run the audit before fixing \u2014 listing issues first prevents accidental scope creep."
2518
+ ];
2519
+ function checksByCategory(cat) {
2520
+ return REDESIGN_CHECKS.filter((c) => c.category === cat);
2521
+ }
2522
+
2523
+ // src/tools/registry.ts
2524
+ var TOOL_DEFINITIONS = [
2525
+ // ── DISCOVERY (small responses) ────────────────────────────────
2526
+ {
2527
+ name: "list_skills",
2528
+ description: "List every design / taste skill bundled by this MCP (taste / soft / minimalist / brutalist / gpt-tasteskill / redesign / output / brandkit / stitch / imagegen-mobile / imagegen-web / image-to-code). Returns id + name + whenToUse + section ids. ~1KB. Use FIRST to discover what skills exist; then `get_skill_section` to drill in.",
2529
+ inputSchema: { type: "object", properties: {} }
2530
+ },
2531
+ {
2532
+ name: "list_primitives",
2533
+ description: "List every @godxjp/ui primitive / composite / shell. Returns group + tagline per entry. ~3KB. Optionally filter by group.",
2534
+ inputSchema: {
2535
+ type: "object",
2536
+ properties: {
2537
+ group: {
2538
+ type: "string",
2539
+ enum: ["general", "layout", "data-display", "data-entry", "feedback", "navigation", "composites", "shell", "providers"]
2540
+ }
2541
+ }
2542
+ }
2543
+ },
2544
+ {
2545
+ name: "list_patterns",
2546
+ description: "List every canonical code pattern (registration-form / settings-page / data-table / confirm-destructive / app-shell / filter-bar / loading-states). ~500 bytes. Use before `get_pattern`.",
2547
+ inputSchema: { type: "object", properties: {} }
2548
+ },
2549
+ {
2550
+ name: "list_anti_ai_tells",
2551
+ description: "List every AI-tell pattern to AVOID (organised by category: visual / layout / copy / interaction / imagery / structure). ~2KB. Use to self-audit a design before shipping.",
2552
+ inputSchema: {
2553
+ type: "object",
2554
+ properties: {
2555
+ category: { type: "string", enum: ["visual", "layout", "copy", "interaction", "imagery", "structure"] }
2556
+ }
2557
+ }
2558
+ },
2559
+ {
2560
+ name: "list_redesign_checks",
2561
+ description: "List the redesign audit checklist (50+ checks across 9 categories: typography / color-surface / layout / interactivity / content / components / iconography / code-quality / omissions). ~5KB. Use when auditing an existing project.",
2562
+ inputSchema: {
2563
+ type: "object",
2564
+ properties: {
2565
+ category: { type: "string", enum: ["typography", "color-surface", "layout", "interactivity", "content", "components", "iconography", "code-quality", "omissions"] }
2566
+ }
2567
+ }
2568
+ },
2569
+ // ── DRILL-DOWN (medium responses) ──────────────────────────────
2570
+ {
2571
+ name: "get_anti_ai_tell",
2572
+ description: "Fetch ONE anti-AI-tell \u2014 full body + concrete fix. Use after `list_anti_ai_tells`.",
2573
+ inputSchema: {
2574
+ type: "object",
2575
+ properties: { name: { type: "string", description: "Exact tell name from list_anti_ai_tells." } },
2576
+ required: ["name"]
2577
+ }
2578
+ },
2579
+ {
2580
+ name: "get_redesign_check",
2581
+ description: "Fetch redesign check(s) matching a symptom snippet. Returns full fix + UI note. Use after `list_redesign_checks`.",
2582
+ inputSchema: {
2583
+ type: "object",
2584
+ properties: { symptom: { type: "string", description: "Fragment of the symptom text (e.g. 'Inter everywhere' / '100vh')." } },
2585
+ required: ["symptom"]
2586
+ }
2587
+ },
2588
+ {
2589
+ name: "get_skill_section",
2590
+ description: "Fetch ONE section of ONE skill \u2014 token-efficient. E.g. `skill='soft', section='double-bezel'`. Use after `list_skills` narrowed the relevant skill + section.",
2591
+ inputSchema: {
2592
+ type: "object",
2593
+ properties: {
2594
+ skill: { type: "string", description: "Skill id (e.g. 'soft', 'minimalist', 'taste')." },
2595
+ section: { type: "string", description: "Section id within that skill." }
2596
+ },
2597
+ required: ["skill", "section"]
2598
+ }
2599
+ },
2600
+ {
2601
+ name: "get_component",
2602
+ description: "Full API for one @godxjp/ui component \u2014 props, types, default, example, story + doc paths, cardinal rules. ~2KB.",
2603
+ inputSchema: {
2604
+ type: "object",
2605
+ properties: { name: { type: "string", description: "Component name (e.g. 'Button', 'DataTable')." } },
2606
+ required: ["name"]
2607
+ }
2608
+ },
2609
+ {
2610
+ name: "get_pattern",
2611
+ description: "Full code snippet for one canonical pattern \u2014 copy-paste-ready.",
2612
+ inputSchema: {
2613
+ type: "object",
2614
+ properties: { name: { type: "string", description: "Pattern slug (use list_patterns first)." } },
2615
+ required: ["name"]
2616
+ }
2617
+ },
2618
+ {
2619
+ name: "get_rule",
2620
+ description: "Read one cardinal rule from CLAUDE.md (1-34) OR all if no number.",
2621
+ inputSchema: {
2622
+ type: "object",
2623
+ properties: { number: { type: "number", description: "Rule number 1-34." } }
2624
+ }
2625
+ },
2626
+ {
2627
+ name: "get_vocab",
2628
+ description: "Read shared prop-vocabulary type (`SizeProp`, `StatusProp`, `ColorProp`, `LoadingProp`, etc.) OR all if no name.",
2629
+ inputSchema: {
2630
+ type: "object",
2631
+ properties: { name: { type: "string", description: "Vocab type name." } }
2632
+ }
2633
+ },
2634
+ {
2635
+ name: "get_tokens",
2636
+ description: "Read design tokens, optionally filtered by category.",
2637
+ inputSchema: {
2638
+ type: "object",
2639
+ properties: {
2640
+ category: { type: "string", enum: ["color", "spacing", "typography", "radius", "shadow", "motion", "breakpoint", "density", "z-index"] }
2641
+ }
2642
+ }
2643
+ },
2644
+ // ── TASK ROUTING (smallest response — pointer) ─────────────────
2645
+ {
2646
+ name: "route_task",
2647
+ description: "Natural-language task \u2192 skill+section pointer. ~300 bytes. E.g. 'I want to design a premium agency hero' \u2192 `{skill:'soft', section:'vibe-archetypes', why:'...'}`. Use FIRST when you don't know which skill applies.",
2648
+ inputSchema: {
2649
+ type: "object",
2650
+ properties: { task: { type: "string", description: "Describe what you want to build." } },
2651
+ required: ["task"]
2652
+ }
2653
+ },
2654
+ {
2655
+ name: "suggest_primitive",
2656
+ description: "Use case \u2192 primitive recommendation. E.g. 'confirm a destructive delete' \u2192 DangerZone pattern + Dialog suggestion.",
2657
+ inputSchema: {
2658
+ type: "object",
2659
+ properties: { use_case: { type: "string" } },
2660
+ required: ["use_case"]
2661
+ }
2662
+ },
2663
+ {
2664
+ name: "search_components",
2665
+ description: "Fuzzy-search primitives by name / tagline / prop. Returns ranked matches.",
2666
+ inputSchema: {
2667
+ type: "object",
2668
+ properties: { query: { type: "string" } },
2669
+ required: ["query"]
2670
+ }
2671
+ },
2672
+ // ── LINT / AUDIT (one-shot critique) ───────────────────────────
2673
+ {
2674
+ name: "lint_jsx",
2675
+ description: "Heuristic check of a JSX snippet for common violations \u2014 raw `<button>` / `<input>`, `color='error'` on Tag/Badge, missing aria-label, missing source.code override on stories with cell renderers (rule 34), etc.",
2676
+ inputSchema: {
2677
+ type: "object",
2678
+ properties: { jsx: { type: "string" } },
2679
+ required: ["jsx"]
2680
+ }
2681
+ }
2682
+ ];
2683
+ async function dispatchTool(name, args) {
2684
+ switch (name) {
2685
+ // Discovery
2686
+ case "list_skills":
2687
+ return listSkills();
2688
+ case "list_primitives":
2689
+ return listPrimitives(args.group);
2690
+ case "list_patterns":
2691
+ return listPatterns();
2692
+ case "list_anti_ai_tells":
2693
+ return listAntiAiTells(args.category);
2694
+ case "list_redesign_checks":
2695
+ return listRedesignChecks(args.category);
2696
+ case "get_anti_ai_tell":
2697
+ return getAntiAiTell(String(args.name ?? ""));
2698
+ case "get_redesign_check":
2699
+ return getRedesignCheck(String(args.symptom ?? ""));
2700
+ // Drill-down
2701
+ case "get_skill_section":
2702
+ return getSkillSection(String(args.skill ?? ""), String(args.section ?? ""));
2703
+ case "get_component":
2704
+ return getComponent(String(args.name ?? ""));
2705
+ case "get_pattern":
2706
+ return getPattern(String(args.name ?? ""));
2707
+ case "get_rule":
2708
+ return getRule(typeof args.number === "number" ? args.number : void 0);
2709
+ case "get_vocab":
2710
+ return getVocab(args.name);
2711
+ case "get_tokens":
2712
+ return getTokens(args.category);
2713
+ // Task routing
2714
+ case "route_task":
2715
+ return routeTaskTool(String(args.task ?? ""));
2716
+ case "suggest_primitive":
2717
+ return suggestPrimitive(String(args.use_case ?? ""));
2718
+ case "search_components":
2719
+ return searchComponents(String(args.query ?? ""));
2720
+ // Lint
2721
+ case "lint_jsx":
2722
+ return lintJsx(String(args.jsx ?? ""));
2723
+ default:
2724
+ return `Unknown tool: ${name}`;
2725
+ }
2726
+ }
2727
+ function listSkills() {
2728
+ let out = `# Available skills (${SKILLS.length})
2729
+
2730
+ `;
2731
+ out += `Use \`get_skill_section skill="..." section="..."\` to drill in.
2732
+
2733
+ `;
2734
+ for (const s of SKILLS) {
2735
+ out += `## ${s.id} \u2014 ${s.name}
2736
+ `;
2737
+ out += `**When to use:** ${s.whenToUse}
2738
+
2739
+ `;
2740
+ out += `**Sections:** ${s.sections.map((sec) => `\`${sec.id}\``).join(", ")}
2741
+
2742
+ `;
2743
+ }
2744
+ out += `
2745
+ _Source: ${SKILLS.map((s) => s.source).filter((v, i, a) => a.indexOf(v) === i).slice(0, 3).join("; ")}, \u2026_`;
2746
+ return out;
2747
+ }
2748
+ function listPrimitives(group) {
2749
+ const list = group ? componentsByGroup(group) : COMPONENTS;
2750
+ if (list.length === 0) return `No components${group ? ` in group "${group}"` : ""}.`;
2751
+ const grouped = list.reduce((acc, c) => {
2752
+ (acc[c.group] ??= []).push(c);
2753
+ return acc;
2754
+ }, {});
2755
+ let out = `# @godxjp/ui primitives${group ? ` \u2014 ${group}` : ""}
2756
+
2757
+ ${list.length} components.
2758
+
2759
+ `;
2760
+ for (const [g, items] of Object.entries(grouped)) {
2761
+ out += `## ${g}
2762
+
2763
+ `;
2764
+ for (const c of items) out += `- **${c.name}** \u2014 ${c.tagline}
2765
+ `;
2766
+ out += "\n";
2767
+ }
2768
+ return out;
2769
+ }
2770
+ function listPatterns() {
2771
+ let out = `# Canonical patterns (${PATTERNS.length})
2772
+
2773
+ `;
2774
+ for (const p of PATTERNS) {
2775
+ out += `- **${p.name}** \u2014 ${p.tagline}
2776
+ _tags: ${p.tags.join(", ")}_
2777
+ `;
2778
+ }
2779
+ return out;
2780
+ }
2781
+ function listAntiAiTells(cat) {
2782
+ const list = cat ? aiTellsByCategory(cat) : ANTI_AI_TELLS;
2783
+ let out = `# AI tells to AVOID${cat ? ` \u2014 ${cat}` : ""} (${list.length})
2784
+
2785
+ `;
2786
+ out += `_Compact list. Use \`get_anti_ai_tell name="<name>"\` for the full body + fix._
2787
+
2788
+ `;
2789
+ const grouped = list.reduce((acc, t) => {
2790
+ (acc[t.category] ??= []).push(t);
2791
+ return acc;
2792
+ }, {});
2793
+ for (const [c, items] of Object.entries(grouped)) {
2794
+ out += `## ${c}
2795
+ `;
2796
+ for (const t of items) out += `- ${t.name}
2797
+ `;
2798
+ out += "\n";
2799
+ }
2800
+ return out;
2801
+ }
2802
+ function getAntiAiTell(name) {
2803
+ const t = ANTI_AI_TELLS.find((x) => x.name.toLowerCase() === name.trim().toLowerCase());
2804
+ if (!t) {
2805
+ let out = `Anti-AI-tell "${name}" not found. Use \`list_anti_ai_tells\` to discover. Closest:
2806
+
2807
+ `;
2808
+ for (const x of ANTI_AI_TELLS.slice(0, 8)) out += `- ${x.name} (${x.category})
2809
+ `;
2810
+ return out;
2811
+ }
2812
+ return `# ${t.name}
2813
+
2814
+ **Category:** ${t.category}
2815
+
2816
+ ## Symptom
2817
+
2818
+ ${t.body}
2819
+
2820
+ ## Fix
2821
+
2822
+ ${t.fix}
2823
+ `;
2824
+ }
2825
+ function listRedesignChecks(cat) {
2826
+ const list = cat ? checksByCategory(cat) : REDESIGN_CHECKS;
2827
+ let out = `# Redesign audit${cat ? ` \u2014 ${cat}` : ""} (${list.length} checks)
2828
+
2829
+ `;
2830
+ if (!cat) {
2831
+ out += `## Fix priority
2832
+ ${FIX_PRIORITY.map((p) => p).join("\n")}
2833
+
2834
+ `;
2835
+ out += `## Rules
2836
+ ${REDESIGN_RULES.map((r) => `- ${r}`).join("\n")}
2837
+
2838
+ `;
2839
+ }
2840
+ out += `_Compact list of symptoms. Use \`get_redesign_check symptom="<text snippet>"\` for the full fix + UI note._
2841
+
2842
+ `;
2843
+ const grouped = list.reduce((acc, c) => {
2844
+ (acc[c.category] ??= []).push(c);
2845
+ return acc;
2846
+ }, {});
2847
+ for (const [c, items] of Object.entries(grouped)) {
2848
+ out += `## ${c}
2849
+ `;
2850
+ for (const item of items) out += `- ${item.symptom}
2851
+ `;
2852
+ out += "\n";
2853
+ }
2854
+ return out;
2855
+ }
2856
+ function getRedesignCheck(snippet) {
2857
+ const q = snippet.trim().toLowerCase();
2858
+ if (!q) return "Pass `symptom` \u2014 a fragment matching the audit check symptom (e.g. 'Inter everywhere' / '100vh' / 'Acme').";
2859
+ const matches = REDESIGN_CHECKS.filter(
2860
+ (c) => c.symptom.toLowerCase().includes(q) || c.fix.toLowerCase().includes(q)
2861
+ );
2862
+ if (!matches.length) {
2863
+ return `No redesign check matches "${snippet}". Use \`list_redesign_checks\` to see all.`;
2864
+ }
2865
+ let out = `# Redesign checks matching "${snippet}" (${matches.length})
2866
+
2867
+ `;
2868
+ for (const c of matches) {
2869
+ out += `## ${c.category}
2870
+
2871
+ **Symptom:** ${c.symptom}
2872
+
2873
+ **Fix:** ${c.fix}
2874
+ ${c.uiNote ? `
2875
+ _UI note:_ ${c.uiNote}
2876
+ ` : ""}
2877
+ `;
2878
+ }
2879
+ return out;
2880
+ }
2881
+ function getSkillSection(skillId, sectionId) {
2882
+ const skill = findSkill(skillId);
2883
+ if (!skill) return `Skill "${skillId}" not found. Use \`list_skills\` for available ids.`;
2884
+ if (!sectionId) {
2885
+ let out = `# ${skill.name}
2886
+
2887
+ ${skill.whenToUse}
2888
+
2889
+ ## Sections
2890
+ `;
2891
+ for (const sec of skill.sections) out += `- \`${sec.id}\` \u2014 ${sec.tagline}
2892
+ `;
2893
+ return out;
2894
+ }
2895
+ const section = findSection(skillId, sectionId);
2896
+ if (!section) {
2897
+ let out = `Section "${sectionId}" not in skill "${skillId}". Available:
2898
+ `;
2899
+ for (const sec of skill.sections) out += `- \`${sec.id}\` \u2014 ${sec.tagline}
2900
+ `;
2901
+ return out;
2902
+ }
2903
+ return `# ${skill.name} \u2192 ${section.title}
2904
+
2905
+ ${section.tagline}
2906
+
2907
+ ${section.body}
2908
+
2909
+ _Source: ${skill.source}_`;
2910
+ }
2911
+ function getComponent(name) {
2912
+ const c = findComponent(name);
2913
+ if (!c) return `Component "${name}" not found. Use \`list_primitives\` to discover.`;
2914
+ let out = `# ${c.name}
2915
+
2916
+ **Group:** ${c.group}
2917
+
2918
+ ${c.tagline}
2919
+
2920
+ ## Props
2921
+
2922
+ `;
2923
+ out += `| Name | Type | Required | Default | Description |
2924
+ |---|---|---|---|---|
2925
+ `;
2926
+ for (const p of c.props) {
2927
+ out += `| \`${p.name}\` | \`${p.type}\` | ${p.required ? "\u2713" : ""} | ${p.defaultValue ? `\`${p.defaultValue}\`` : ""} | ${p.description} |
2928
+ `;
2929
+ }
2930
+ out += `
2931
+ ## Example
2932
+
2933
+ \`\`\`tsx
2934
+ ${c.example}
2935
+ \`\`\`
2936
+
2937
+ `;
2938
+ if (c.docPath) out += `**Reference doc:** \`docs/reference/${c.docPath}\`
2939
+
2940
+ `;
2941
+ out += `**Storybook:** \`src/stories/${c.storyPath}\`
2942
+
2943
+ `;
2944
+ out += `**Cardinal rules:** ${c.rules.map((n) => `#${n}`).join(", ")}
2945
+ `;
2946
+ return out;
2947
+ }
2948
+ function getPattern(name) {
2949
+ const p = findPattern(name);
2950
+ if (!p) {
2951
+ const candidates = searchPatterns(name);
2952
+ if (candidates.length === 0) return `Pattern "${name}" not found.`;
2953
+ let out = `Pattern "${name}" not found. Closest:
2954
+ `;
2955
+ for (const c of candidates) out += `- ${c.name} \u2014 ${c.tagline}
2956
+ `;
2957
+ return out;
2958
+ }
2959
+ return `# Pattern: ${p.name}
2960
+
2961
+ ${p.tagline}
2962
+
2963
+ **Tags:** ${p.tags.join(", ")}
2964
+
2965
+ \`\`\`tsx
2966
+ ${p.code}
2967
+ \`\`\`
2968
+ `;
2969
+ }
2970
+ function getRule(num) {
2971
+ if (num !== void 0) {
2972
+ const r = findRule(num);
2973
+ if (!r) return `Rule ${num} not found. Valid: 1-${CARDINAL_RULES.length}.`;
2974
+ return `# Rule ${r.number} \u2014 ${r.title}
2975
+
2976
+ ${r.body}
2977
+ `;
2978
+ }
2979
+ let out = `# Cardinal rules (${CARDINAL_RULES.length})
2980
+
2981
+ `;
2982
+ for (const r of CARDINAL_RULES) out += `## ${r.number}. ${r.title}
2983
+
2984
+ ${r.body}
2985
+
2986
+ `;
2987
+ return out;
2988
+ }
2989
+ function getVocab(name) {
2990
+ if (name) {
2991
+ const v = findVocab(name);
2992
+ if (!v) return `Vocab "${name}" not found.`;
2993
+ let out2 = `# ${v.name}
2994
+
2995
+ ${v.concept}
2996
+
2997
+ `;
2998
+ out2 += `**Values:** ${v.values.map((x) => `\`${x}\``).join(" | ")}
2999
+
3000
+ `;
3001
+ out2 += `**Used by:** ${v.usedBy.map((x) => `\`${x}\``).join(", ")}
3002
+
3003
+ `;
3004
+ if (v.notes) out2 += `**Notes:** ${v.notes}
3005
+ `;
3006
+ return out2;
3007
+ }
3008
+ let out = `# Prop vocabulary
3009
+
3010
+ ${PROP_VOCABULARY.length} shared types.
3011
+
3012
+ `;
3013
+ for (const v of PROP_VOCABULARY) {
3014
+ out += `## ${v.name}
3015
+ ${v.concept}
3016
+
3017
+ Values: ${v.values.map((x) => `\`${x}\``).join(" | ")}
3018
+
3019
+ `;
3020
+ }
3021
+ return out;
3022
+ }
3023
+ function getTokens(cat) {
3024
+ const list = cat ? tokensByCategory(cat) : TOKENS;
3025
+ if (list.length === 0) return `No tokens${cat ? ` in "${cat}"` : ""}.`;
3026
+ let out = `# Design tokens${cat ? ` \u2014 ${cat}` : ""}
3027
+
3028
+ `;
3029
+ const grouped = list.reduce((acc, t) => {
3030
+ (acc[t.category] ??= []).push(t);
3031
+ return acc;
3032
+ }, {});
3033
+ for (const [c, items] of Object.entries(grouped)) {
3034
+ out += `## ${c}
3035
+
3036
+ | Name | Role | Value | Axis |
3037
+ |---|---|---|---|
3038
+ `;
3039
+ for (const t of items) out += `| \`${t.name}\` | ${t.role} | ${t.value ?? "\u2014"} | ${t.axis ?? "\u2014"} |
3040
+ `;
3041
+ out += "\n";
3042
+ }
3043
+ return out;
3044
+ }
3045
+ function routeTaskTool(task) {
3046
+ if (!task.trim()) return "Describe the task (e.g. 'design a premium agency hero', 'audit existing settings page').";
3047
+ const results = routeTask(task);
3048
+ let out = `# Routing "${task}"
3049
+
3050
+ `;
3051
+ for (const r of results) {
3052
+ out += `- **skill:** \`${r.skill}\`, **section:** \`${r.section}\`
3053
+ ${r.why}
3054
+ `;
3055
+ if (r.alsoSee?.length) out += ` _Also see:_ ${r.alsoSee.map((s) => `\`${s}\``).join(", ")}
3056
+ `;
3057
+ }
3058
+ out += `
3059
+ Fetch with: \`get_skill_section skill="X" section="Y"\``;
3060
+ return out;
3061
+ }
3062
+ function suggestPrimitive(useCase) {
3063
+ const q = useCase.trim().toLowerCase();
3064
+ if (!q) return "Describe your use case.";
3065
+ const suggestions = [];
3066
+ const check = (kw, component, rationale, weight = 2) => {
3067
+ if (kw.some((k) => q.includes(k))) suggestions.push({ component, rationale, score: weight });
3068
+ };
3069
+ check(["form", "submit", "validation", "register", "sign up"], "Form + FormField", "RHF + zod composition.", 5);
3070
+ check(["table", "rows", "columns"], "DataTable / Table", "DataTable for chrome (toolbar+pagination+batch). Table for slim primitive.", 5);
3071
+ check(["modal", "dialog", "confirm"], "Dialog / AlertDialog", "Radix Dialog. AlertDialog for destructive.", 4);
3072
+ check(["drawer", "side panel", "sheet"], "Sheet", "Side panel for filters/settings.", 4);
3073
+ check(["toast", "notification"], "toast / Toaster", "Sonner-backed.", 4);
3074
+ check(["loading", "saving", "spinner"], "Spinner / Form loading prop", "Spinner=active work, Skeleton=init fetch.", 3);
3075
+ check(["alert", "banner"], "Alert", "5 semantic colors \xD7 outlined/banner.", 3);
3076
+ check(["select", "dropdown"], "Select / AutoComplete", "Select=discrete options, AutoComplete=free-text+suggestions.", 3);
3077
+ check(["filter"], "Form layout='inline' + pattern 'filter-bar'", "Inline form above table.", 4);
3078
+ check(["delete", "destructive"], "Pattern 'confirm-destructive'", "Card accent='destructive' + typed-name confirm.", 4);
3079
+ if (!suggestions.length) return `No direct match for "${useCase}". Try \`list_primitives\` or \`search_components\`.`;
3080
+ suggestions.sort((a, b) => b.score - a.score);
3081
+ let out = `# Suggestions for "${useCase}"
3082
+
3083
+ `;
3084
+ for (const s of suggestions) out += `- **${s.component}** \u2014 ${s.rationale}
3085
+ `;
3086
+ return out;
3087
+ }
3088
+ function searchComponents(query) {
3089
+ const q = query.trim().toLowerCase();
3090
+ if (!q) return listPrimitives();
3091
+ const matches = COMPONENTS.map((c) => {
3092
+ let score = 0;
3093
+ if (c.name.toLowerCase().includes(q)) score += 5;
3094
+ if (c.tagline.toLowerCase().includes(q)) score += 2;
3095
+ if (c.props.some((p) => p.name.toLowerCase().includes(q))) score += 1;
3096
+ return { c, score };
3097
+ }).filter((m) => m.score > 0).sort((a, b) => b.score - a.score).slice(0, 12);
3098
+ if (!matches.length) return `No matches for "${query}".`;
3099
+ let out = `# Search "${query}" \u2014 ${matches.length} matches
3100
+
3101
+ `;
3102
+ for (const { c, score } of matches) out += `- **${c.name}** (${c.group}, ${score}) \u2014 ${c.tagline}
3103
+ `;
3104
+ return out;
3105
+ }
3106
+ function lintJsx(jsx) {
3107
+ const issues = [];
3108
+ const check = (regex, msg) => {
3109
+ if (regex.test(jsx)) issues.push(msg);
3110
+ };
3111
+ check(/<button[\s>]/, "Use `<Button>` instead of raw `<button>` (rule 29).");
3112
+ check(/<input[\s>]/, "Use `<Input>` instead of raw `<input>` (rule 29).");
3113
+ check(/<select[\s>]/, "Use `<Select>` instead of raw `<select>` (rule 29).");
3114
+ check(/<textarea[\s>]/, "Use `<Textarea>` instead of raw `<textarea>` (rule 29).");
3115
+ check(/bg-(red|blue|green|yellow|gray|slate|zinc|neutral|stone|orange|amber|lime|emerald|teal|cyan|sky|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}\b/, "Use semantic token utilities (`bg-primary`/`bg-destructive`) not raw color scales (rule 2).");
3116
+ check(/<Tag[\s\S]*?color=["']error["']/i, 'Tag `color="error"` \u2192 `"destructive"` (v5.0, PR #60).');
3117
+ check(/<Badge[\s\S]*?variant=["']error["']/i, 'Badge `variant="error"` \u2192 `"destructive"` (v5.0, PR #63).');
3118
+ check(/(Flex|Space|Grid|Masonry)[\s\S]*?(gap|size)=["']middle["']/i, '`"middle"` \u2192 `"default"` for Flex/Space/Grid/Masonry (v5.0).');
3119
+ check(/<IconButton[\s\S]*?size=["']default["']/i, 'IconButton `size="default"` \u2192 `"md"` (v5.0).');
3120
+ check(/<SegmentedControl[\s\S]*?size=["']sm["']/i, 'SegmentedControl `size="sm"` \u2192 `"small"` (v5.0).');
3121
+ check(/<PageContent[\s\S]*?padding=["'](compact|comfortable)["']/i, 'PageContent `padding="compact"/"comfortable"` \u2192 `"tight"/"cozy"` (v5.0).');
3122
+ check(/<Pagination[\s\S]*?justify=["']between["']/i, 'Pagination `justify="between"` \u2192 `"space-between"` (v5.0).');
3123
+ if (/<IconButton(?![^>]*aria-label)/i.test(jsx) && !/asChild/i.test(jsx)) {
3124
+ issues.push("`<IconButton>` should have `aria-label` (rule 6 \u2014 WCAG).");
3125
+ }
3126
+ if (/cell:\s*\(\{?\s*row\s*\}?\)\s*=>/i.test(jsx) && /export\s+const\s+\w+\s*:\s*Story/i.test(jsx)) {
3127
+ if (!/parameters[\s\S]{0,200}source[\s\S]{0,100}code:/i.test(jsx)) {
3128
+ issues.push("Stories with function-valued cell renderers MUST override `parameters.docs.source.code` (rule 34).");
3129
+ }
3130
+ }
3131
+ if (/text-(red|blue|green|yellow)-\d{2,3}\b/.test(jsx)) issues.push("Hard-coded color scales \u2014 use semantic tokens. Tells AI-slop palette (rule 2 + anti-AI-tells.visual.rainbow-chip-wall).");
3132
+ if (/h-\[?100vh\]?/.test(jsx)) issues.push("`100vh` causes iOS Safari viewport jump \u2014 use `min-h-[100dvh]` (redesign.layout / soft.absolute-zero).");
3133
+ if (/className=["'][^"']*(?:shadow-md|shadow-lg|shadow-xl)["']/.test(jsx)) issues.push("Tailwind heavy shadows are an AI tell \u2014 use ultra-diffuse low-opacity (< 0.05) or tinted shadows (soft.absolute-zero, minimalist).");
3134
+ if (/Inter|Roboto|Helvetica|Open\s*Sans/i.test(jsx)) issues.push("Banned default fonts (Inter/Roboto/Helvetica/Open Sans). Use Geist/Clash Display/PP Editorial New (soft.absolute-zero, minimalist.negative-constraints).");
3135
+ if (/Acme|NovaCore|Flowbit|Quantix|VeloPay|John\s+Doe|Jane\s+Smith|Lorem\s+Ipsum/i.test(jsx)) issues.push("Generic placeholder content (Acme/NovaCore/John Doe/Lorem Ipsum). Use believable real-sounding names (anti-AI-tells.copy).");
3136
+ if (issues.length === 0) return "\u2705 No issues found against the heuristic checks.";
3137
+ let out = `# Lint findings \u2014 ${issues.length} issue${issues.length === 1 ? "" : "s"}
3138
+
3139
+ `;
3140
+ for (const i of issues) out += `- ${i}
3141
+ `;
3142
+ out += `
3143
+ Note: heuristic only \u2014 not a substitute for the full CI gate.
3144
+ `;
3145
+ return out;
3146
+ }
3147
+
3148
+ // src/resources/registry.ts
3149
+ var RESOURCE_DEFINITIONS = [
3150
+ {
3151
+ uri: "godx-ui://components",
3152
+ name: "All components",
3153
+ description: "Full component catalog as JSON \u2014 name, group, tagline, props, example, rules.",
3154
+ mimeType: "application/json"
3155
+ },
3156
+ {
3157
+ uri: "godx-ui://prop-vocabulary",
3158
+ name: "Shared prop vocabulary",
3159
+ description: "Cross-cutting prop types (SizeProp, StatusProp, ColorProp, LoadingProp, \u2026) as JSON.",
3160
+ mimeType: "application/json"
3161
+ },
3162
+ {
3163
+ uri: "godx-ui://tokens",
3164
+ name: "All design tokens",
3165
+ description: "Every CSS variable + role + value + axis as JSON.",
3166
+ mimeType: "application/json"
3167
+ },
3168
+ {
3169
+ uri: "godx-ui://rules",
3170
+ name: "Cardinal rules (34)",
3171
+ description: "The 34 binding rules from CLAUDE.md as Markdown.",
3172
+ mimeType: "text/markdown"
3173
+ },
3174
+ {
3175
+ uri: "godx-ui://patterns",
3176
+ name: "Code patterns",
3177
+ description: "Canonical pattern catalog (registration-form, settings-page, data-table, \u2026) as JSON.",
3178
+ mimeType: "application/json"
3179
+ }
3180
+ ];
3181
+ async function readResource(uri) {
3182
+ if (uri === "godx-ui://components") {
3183
+ return JSON.stringify(COMPONENTS, null, 2);
3184
+ }
3185
+ if (uri.startsWith("godx-ui://components/")) {
3186
+ const name = uri.slice("godx-ui://components/".length);
3187
+ const c = findComponent(name);
3188
+ if (!c) throw new Error(`Component not found: ${name}`);
3189
+ return formatComponentMarkdown(c);
3190
+ }
3191
+ if (uri === "godx-ui://prop-vocabulary") {
3192
+ return JSON.stringify(PROP_VOCABULARY, null, 2);
3193
+ }
3194
+ if (uri === "godx-ui://tokens") {
3195
+ return JSON.stringify(TOKENS, null, 2);
3196
+ }
3197
+ if (uri.startsWith("godx-ui://tokens/")) {
3198
+ const cat = uri.slice("godx-ui://tokens/".length);
3199
+ return JSON.stringify(tokensByCategory(cat), null, 2);
3200
+ }
3201
+ if (uri === "godx-ui://rules") {
3202
+ let out = `# Cardinal rules (${CARDINAL_RULES.length})
3203
+
3204
+ `;
3205
+ for (const r of CARDINAL_RULES) {
3206
+ out += `## ${r.number}. ${r.title}
3207
+
3208
+ ${r.body}
3209
+
3210
+ `;
3211
+ }
3212
+ return out;
3213
+ }
3214
+ if (uri.startsWith("godx-ui://rules/")) {
3215
+ const num = Number(uri.slice("godx-ui://rules/".length));
3216
+ const r = findRule(num);
3217
+ if (!r) throw new Error(`Rule not found: ${num}`);
3218
+ return `# Rule ${r.number} \u2014 ${r.title}
3219
+
3220
+ ${r.body}
3221
+ `;
3222
+ }
3223
+ if (uri === "godx-ui://patterns") {
3224
+ return JSON.stringify(PATTERNS.map(({ name, tagline, tags }) => ({ name, tagline, tags })), null, 2);
3225
+ }
3226
+ if (uri.startsWith("godx-ui://patterns/")) {
3227
+ const name = uri.slice("godx-ui://patterns/".length);
3228
+ const p = findPattern(name);
3229
+ if (!p) throw new Error(`Pattern not found: ${name}`);
3230
+ return `# ${p.name}
3231
+
3232
+ ${p.tagline}
3233
+
3234
+ **Tags:** ${p.tags.join(", ")}
3235
+
3236
+ \`\`\`tsx
3237
+ ${p.code}
3238
+ \`\`\`
3239
+ `;
3240
+ }
3241
+ throw new Error(`Unknown resource: ${uri}`);
3242
+ }
3243
+ function formatComponentMarkdown(c) {
3244
+ let out = `# ${c.name}
3245
+
3246
+ **Group:** ${c.group}
3247
+
3248
+ ${c.tagline}
3249
+
3250
+ `;
3251
+ out += `## Props
3252
+
3253
+ `;
3254
+ out += `| Name | Type | Required | Default | Description |
3255
+ |---|---|---|---|---|
3256
+ `;
3257
+ for (const p of c.props) {
3258
+ out += `| \`${p.name}\` | \`${p.type}\` | ${p.required ? "\u2713" : ""} | ${p.defaultValue ? `\`${p.defaultValue}\`` : ""} | ${p.description} |
3259
+ `;
3260
+ }
3261
+ out += `
3262
+ ## Example
3263
+
3264
+ \`\`\`tsx
3265
+ ${c.example}
3266
+ \`\`\`
3267
+ `;
3268
+ return out;
3269
+ }
3270
+
3271
+ // src/index.ts
3272
+ async function main() {
3273
+ const server = new Server(
3274
+ {
3275
+ name: "godx-ui-mcp",
3276
+ version: "0.1.0"
3277
+ },
3278
+ {
3279
+ capabilities: {
3280
+ tools: {},
3281
+ resources: {}
3282
+ }
3283
+ }
3284
+ );
3285
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3286
+ tools: TOOL_DEFINITIONS
3287
+ }));
3288
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3289
+ const { name, arguments: args } = request.params;
3290
+ const result = await dispatchTool(name, args ?? {});
3291
+ return { content: [{ type: "text", text: result }] };
3292
+ });
3293
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
3294
+ resources: RESOURCE_DEFINITIONS
3295
+ }));
3296
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3297
+ const { uri } = request.params;
3298
+ const text = await readResource(uri);
3299
+ return {
3300
+ contents: [
3301
+ {
3302
+ uri,
3303
+ mimeType: uri.endsWith(".json") ? "application/json" : "text/markdown",
3304
+ text
3305
+ }
3306
+ ]
3307
+ };
3308
+ });
3309
+ const transport = new StdioServerTransport();
3310
+ await server.connect(transport);
3311
+ console.error("[godx-ui-mcp] connected (stdio)");
3312
+ }
3313
+ main().catch((err) => {
3314
+ console.error("[godx-ui-mcp] fatal:", err);
3315
+ process.exit(1);
3316
+ });
3317
+ //# sourceMappingURL=index.js.map