@godxjp/ui-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -890,7 +890,13 @@ var CARDINAL_RULES = [
890
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
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
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`.' }
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
+ { number: 35, title: "Status chips never wrap", body: "A `StatusBadge` / `Badge` reads as one atomic unit. Its label must never break across lines \u2014 pin `white-space: nowrap` on the chip (done in `badge-layout.css`), especially inside narrow `DataTable` cells (\u30B9\u30B3\u30FC\u30D7 / \u30B9\u30C6\u30FC\u30BF\u30B9 columns). If a cell is too tight, widen the column or shorten the label; never let the chip wrap." },
895
+ { number: 36, title: "StatusBadge tone/icon are the colour escape hatch", body: "`StatusBadge` auto-maps a fixed set of English lifecycle keys (active, draft, pending, scheduled, cancelled, failed, \u2026) to tone + icon. For ANY other value \u2014 localized labels (\u516C\u958B\u4E2D, \u30A2\u30AF\u30C6\u30A3\u30D6) or categorical tiers (\u4F1A\u54E1\u30E9\u30F3\u30AF, \u5951\u7D04\u30D7\u30E9\u30F3) \u2014 pass `tone` explicitly (success | warning | destructive | info | neutral) and, for non-lifecycle tiers, `icon={null}` to drop the misleading glyph. Don't let domain labels fall back to neutral grey + \u25CB. Map domain\u2192tone in the CONSUMER layer; the framework only provides the props." },
896
+ { number: 37, title: "DataTable is full-width \u2014 never inside a narrow grid column", body: "A multi-column `DataTable` occupies its OWN row at the page's full width: `<Card><CardContent flush><DataTable \u2026/></CardContent></Card>`. Never nest it in a `lg:col-span-2` of a `ResponsiveGrid columns={3}` beside a chart \u2014 the columns get squeezed until CJK text collapses to one character per line. Charts / KPI cards go in their own row ABOVE the table. (See the `inertia-list-page` pattern.)" },
897
+ { number: 38, title: "FilterBar stays OUT of CardContent flush", body: "`CardContent flush` strips horizontal padding for edge-to-edge tables. A `FilterBar` placed inside it loses all padding and sticks to the card edge. Render `FilterBar` as a STANDALONE block above the table card; wrap ONLY the `DataTable` / `EmptyState` in the `Card` + `CardContent flush`. Order on a list page: KPIs \u2192 FilterBar \u2192 table card." },
898
+ { number: 39, title: "Long text columns get an explicit width", body: "For columns whose value can be long (name / title / segment / address), set `col.width` to a Tailwind width class (e.g. `w-64`, `w-48`) so the column reserves space instead of shrinking and wrapping to many lines; leave numeric / status columns auto. Table cells default to `white-space: nowrap`, so an over-tight table scrolls horizontally rather than crushing \u2014 give the important columns real widths so the default layout reads well before any scroll." },
899
+ { number: 40, title: "Pages are mobile-first", body: "Author and verify every page at 320\u2013390px FIRST. Spacing comes only from `Stack` / `Inline` `gap` + `ResponsiveGrid columns={2|3|4}` (which collapse to a single column on narrow screens) \u2014 never raw `p-*` / `gap-*` / `space-*` utilities for page layout. Wide tables scroll horizontally on small screens (don't force-fit them); dialogs and sheets are full-height on mobile. Touch targets \u2265 44\xD744px." }
894
900
  ];
895
901
  function findRule(num) {
896
902
  return CARDINAL_RULES.find((r) => r.number === num);
@@ -1282,6 +1288,210 @@ export function ProfileEditor() {
1282
1288
  </Card>
1283
1289
  )
1284
1290
  }`
1291
+ },
1292
+ {
1293
+ name: "inertia-list-page",
1294
+ tagline: "Inertia + @godxjp/ui list page \u2014 PageContainer + FilterBar + DataTable + StatusBadge + Pagination (current primitive API).",
1295
+ tags: ["inertia", "list", "table", "page", "filter", "pagination", "datatable", "crm"],
1296
+ code: `import { Head, router } from "@inertiajs/react"
1297
+ import { useMemo, useState } from "react"
1298
+ import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
1299
+ import { Card, CardContent, CardStat, DataTable, EmptyState, StatusBadge, type ColumnDef } from "@godxjp/ui/data-display"
1300
+ import { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@godxjp/ui/data-entry"
1301
+ import { FilterBar, FilterGroup, Pagination } from "@godxjp/ui/navigation"
1302
+ import { formatDate } from "@godxjp/ui/datetime"
1303
+ import { withCrmLayout } from "@/layouts/crm-layout" // see "inertia-persistent-layout"
1304
+
1305
+ type Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }
1306
+ const PAGE_SIZE = 10
1307
+
1308
+ function Coupons({ coupons }: { coupons: Coupon[] }) {
1309
+ const [q, setQ] = useState("")
1310
+ const [status, setStatus] = useState("all")
1311
+ const [page, setPage] = useState(1)
1312
+
1313
+ const filtered = useMemo(() => coupons.filter((c) => {
1314
+ if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false
1315
+ if (status !== "all" && c.status !== status) return false
1316
+ return true
1317
+ }), [coupons, q, status])
1318
+ const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
1319
+
1320
+ // ColumnDef = { key, header, render?, align?: "left"|"center"|"right", sortable?, width? }
1321
+ const columns: ColumnDef<Coupon>[] = [
1322
+ { key: "name", header: "\u30AF\u30FC\u30DD\u30F3\u540D", render: (c) => <span className="font-medium">{c.name}</span> },
1323
+ { key: "scope", header: "\u30B9\u30B3\u30FC\u30D7", render: (c) => <StatusBadge status={c.scope} tone="info" icon={null} /> },
1324
+ { key: "status", header: "\u30B9\u30C6\u30FC\u30BF\u30B9", render: (c) => <StatusBadge status={c.status} /> },
1325
+ { key: "valid", header: "\u6709\u52B9\u671F\u9593", render: (c) => \`\${formatDate(c.validFrom)} \u301C \${formatDate(c.validTo)}\` },
1326
+ { key: "usage", header: "\u5229\u7528\u6570", align: "right", render: (c) => c.usage.toLocaleString() },
1327
+ ]
1328
+
1329
+ return (
1330
+ <>
1331
+ <Head title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" />
1332
+ {/* RULE: every page wraps in PageContainer; spacing via Stack/ResponsiveGrid, never p-*/gap-* */}
1333
+ <PageContainer title="\u30AF\u30FC\u30DD\u30F3\u7BA1\u7406" subtitle="\u914D\u4FE1\u4E2D\u306E\u30AF\u30FC\u30DD\u30F3\u4E00\u89A7">
1334
+ <Stack gap="lg">
1335
+ <ResponsiveGrid columns={3}>
1336
+ <CardStat label="\u516C\u958B\u4E2D" value={coupons.filter((c) => c.status === "\u516C\u958B\u4E2D").length} />
1337
+ <CardStat label="\u7DCF\u5229\u7528\u6570" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />
1338
+ <CardStat label="\u4EF6\u6570" value={coupons.length} />
1339
+ </ResponsiveGrid>
1340
+
1341
+ <FilterBar hasActiveFilters={q !== "" || status !== "all"} onClear={() => { setQ(""); setStatus("all"); setPage(1) }}>
1342
+ {/* SearchInput is value + onSearch(v) \u2014 NOT onChange */}
1343
+ <SearchInput placeholder="\u30AF\u30FC\u30DD\u30F3\u540D\u3067\u691C\u7D22" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />
1344
+ <FilterGroup label="\u30B9\u30C6\u30FC\u30BF\u30B9">
1345
+ <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>
1346
+ <SelectTrigger><SelectValue /></SelectTrigger>
1347
+ <SelectContent>
1348
+ <SelectItem value="all">\u5168\u30B9\u30C6\u30FC\u30BF\u30B9</SelectItem>
1349
+ <SelectItem value="\u516C\u958B\u4E2D">\u516C\u958B\u4E2D</SelectItem>
1350
+ <SelectItem value="\u4E0B\u66F8\u304D">\u4E0B\u66F8\u304D</SelectItem>
1351
+ </SelectContent>
1352
+ </Select>
1353
+ </FilterGroup>
1354
+ </FilterBar>
1355
+
1356
+ <Card>
1357
+ <CardContent flush>
1358
+ {filtered.length === 0
1359
+ ? <EmptyState title="\u8A72\u5F53\u3059\u308B\u30AF\u30FC\u30DD\u30F3\u304C\u3042\u308A\u307E\u305B\u3093" description="\u691C\u7D22\u6761\u4EF6\u3092\u5909\u66F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002" />
1360
+ : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(\`/coupons/\${c.id}\`)} />}
1361
+ </CardContent>
1362
+ </Card>
1363
+
1364
+ {filtered.length > PAGE_SIZE && (
1365
+ <Pagination current={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onChange={(p) => setPage(p)} />
1366
+ )}
1367
+ </Stack>
1368
+ </PageContainer>
1369
+ </>
1370
+ )
1371
+ }
1372
+
1373
+ Coupons.layout = withCrmLayout
1374
+ export default Coupons`
1375
+ },
1376
+ {
1377
+ name: "inertia-detail-page",
1378
+ tagline: "Inertia detail page \u2014 receives {id} prop, KeyValueGrid (compound) + CardStat + EmptyState fallback.",
1379
+ tags: ["inertia", "detail", "show", "page", "keyvaluegrid", "crm"],
1380
+ code: `import { Head, router } from "@inertiajs/react"
1381
+ import { PageContainer, ResponsiveGrid, Stack } from "@godxjp/ui/layout"
1382
+ import { Card, CardContent, CardStat, EmptyState, KeyValueGrid, StatusBadge } from "@godxjp/ui/data-display"
1383
+ import { Button } from "@godxjp/ui/general"
1384
+ import { formatDate } from "@godxjp/ui/datetime"
1385
+ import { ArrowLeft } from "lucide-react"
1386
+ import { withCrmLayout } from "@/layouts/crm-layout"
1387
+
1388
+ // Detail routes pass the param as an Inertia prop:
1389
+ // Route::get('/members/{id}', fn ($id) => Inertia::render('crm/members/show', ['id' => $id]))
1390
+ function MemberShow({ id }: { id: string }) {
1391
+ const member = MEMBERS.find((m) => m.id === id)
1392
+
1393
+ if (!member) {
1394
+ return (
1395
+ <>
1396
+ <Head title="\u4F1A\u54E1\u8A73\u7D30" />
1397
+ <PageContainer title="\u4F1A\u54E1\u8A73\u7D30" subtitle="\u4F1A\u54E1\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093">
1398
+ <EmptyState title="\u4F1A\u54E1\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" description={\`ID\u300C\${id}\u300D\u306F\u5B58\u5728\u3057\u307E\u305B\u3093\u3002\`} />
1399
+ <Button variant="outline" onClick={() => router.visit("/members")}><ArrowLeft className="size-4" />\u4E00\u89A7\u3078\u623B\u308B</Button>
1400
+ </PageContainer>
1401
+ </>
1402
+ )
1403
+ }
1404
+
1405
+ return (
1406
+ <>
1407
+ <Head title={member.name} />
1408
+ <PageContainer title={member.name} subtitle={\`\${member.id} / \${member.rank}\`}>
1409
+ <Stack gap="lg">
1410
+ <ResponsiveGrid columns={4}>
1411
+ <CardStat label="\u7D2F\u8A08\u8CFC\u5165\u984D" value={\`\xA5\${member.total.toLocaleString()}\`} />
1412
+ <CardStat label="\u6765\u5E97\u56DE\u6570" value={member.visits} />
1413
+ <CardStat label="\u30DD\u30A4\u30F3\u30C8" value={member.points.toLocaleString()} />
1414
+ <CardStat label="LTV" value={\`\xA5\${member.ltv.toLocaleString()}\`} />
1415
+ </ResponsiveGrid>
1416
+ <Card>
1417
+ <CardContent>
1418
+ {/* KeyValueGrid is COMPOUND \u2014 value goes in children, not a prop */}
1419
+ <KeyValueGrid columns={2}>
1420
+ <KeyValueGrid.Item label="\u6C0F\u540D">{member.name}</KeyValueGrid.Item>
1421
+ <KeyValueGrid.Item label="\u30E9\u30F3\u30AF"><StatusBadge status={member.rank} tone="info" icon={null} /></KeyValueGrid.Item>
1422
+ <KeyValueGrid.Item label="\u30B9\u30C6\u30FC\u30BF\u30B9"><StatusBadge status={member.status} /></KeyValueGrid.Item>
1423
+ <KeyValueGrid.Item label="\u767B\u9332\u65E5">{formatDate(member.registeredAt)}</KeyValueGrid.Item>
1424
+ </KeyValueGrid>
1425
+ </CardContent>
1426
+ </Card>
1427
+ </Stack>
1428
+ </PageContainer>
1429
+ </>
1430
+ )
1431
+ }
1432
+
1433
+ MemberShow.layout = withCrmLayout
1434
+ export default MemberShow`
1435
+ },
1436
+ {
1437
+ name: "inertia-persistent-layout",
1438
+ tagline: "Inertia persistent layout (AppShell+Sidebar) \u2014 the array-form gotcha + the SSR/Math.random gotcha.",
1439
+ tags: ["inertia", "layout", "appshell", "sidebar", "ssr", "hydration", "gotcha"],
1440
+ code: `// resources/js/layouts/crm-layout.tsx
1441
+ import { router, usePage } from "@inertiajs/react"
1442
+ import { AppShell, Sidebar } from "@godxjp/ui/layout"
1443
+ import { LayoutDashboard } from "lucide-react"
1444
+ import type { ReactNode } from "react"
1445
+
1446
+ export function CrmLayout({ children }: { children: ReactNode }) {
1447
+ const { url } = usePage()
1448
+ const sections = [{ label: "\u30E1\u30A4\u30F3", items: [{ id: "/dashboard", label: "\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9", icon: LayoutDashboard }] }]
1449
+ return (
1450
+ <AppShell sidebar={<Sidebar activeId={url} onSelect={(id) => router.visit(id)} sections={sections} product={{ name: "JOVY CRM" }} />}>
1451
+ {children}
1452
+ </AppShell>
1453
+ )
1454
+ }
1455
+
1456
+ // \u26A0\uFE0F GOTCHA 1 \u2014 persistent layout MUST be the ARRAY form.
1457
+ // A render fn \`(page) => <CrmLayout>{page}</CrmLayout>\` is indistinguishable from a
1458
+ // component; Inertia React calls it with the page-PROPS object and renders that
1459
+ // object as a child \u2192 "Objects are not valid as a React child {errors, auth, \u2026}".
1460
+ export const withCrmLayout = [CrmLayout] // \u2705 array \u2192 Inertia passes the page as children
1461
+ // page usage: Dashboard.layout = withCrmLayout
1462
+
1463
+ // \u26A0\uFE0F GOTCHA 2 \u2014 Inertia v3 SSRs even in \`npm run dev\`. NEVER call Math.random() or
1464
+ // argless new Date() during render (e.g. fabricating chart/demo numbers) \u2192 React
1465
+ // hydration mismatch ("server rendered text didn't match the client"). Seed
1466
+ // deterministically by index, or compute inside an event handler:
1467
+ const seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; return x - Math.floor(x) }`
1468
+ },
1469
+ {
1470
+ name: "status-badge-coloring",
1471
+ tagline: "Colour a StatusBadge for localized labels and tiers via tone + icon (escape-hatch props, @godxjp/ui \u2265 6.1).",
1472
+ tags: ["statusbadge", "badge", "tone", "color", "status", "tier", "table"],
1473
+ code: `import { StatusBadge } from "@godxjp/ui/data-display"
1474
+
1475
+ // StatusBadge auto-colours a fixed set of English LIFECYCLE keys:
1476
+ // active/completed (success \u2713) \xB7 draft (neutral \u25CB) \xB7 pending/temporary (warning \u23F1)
1477
+ // scheduled/sending (info) \xB7 cancelled (neutral) \xB7 failed/deleted/bounced (destructive \u2715)
1478
+ // Anything else (localized labels, tiers) falls back to neutral grey \u25CB unless you override.
1479
+
1480
+ // 1) Lifecycle with localized text \u2014 map to the key, keep JP via \`label\` (icon stays):
1481
+ <StatusBadge status="active" label="\u516C\u958B\u4E2D" /> // green \u2713 \u516C\u958B\u4E2D
1482
+
1483
+ // 2) Unknown label \u2014 set tone explicitly (no icon, since the key is unknown):
1484
+ <StatusBadge status="\u516C\u958B\u4E2D" tone="success" />
1485
+
1486
+ // 3) Tier / category \u2014 coloured pill, drop the misleading glyph with icon={null}:
1487
+ <StatusBadge status="\u30D7\u30EC\u30DF\u30A2\u30E0" tone="success" icon={null} />
1488
+ <StatusBadge status="\u30B4\u30FC\u30EB\u30C9" tone="warning" icon={null} />
1489
+ <StatusBadge status="\u6CD5\u4EBA\u5171\u901A" tone="info" icon={null} />
1490
+
1491
+ // tone: "success" | "warning" | "destructive" | "info" | "neutral" (import type StatusBadgeTone)
1492
+ // RULE: a chip never wraps \u2014 it is pinned white-space: nowrap, so it stays one line in
1493
+ // narrow table cells. Centralize the domain\u2192tone map in ONE small consumer wrapper and
1494
+ // import that instead of the raw StatusBadge across pages.`
1285
1495
  }
1286
1496
  ];
1287
1497
  function findPattern(name) {
@@ -2617,10 +2827,10 @@ var TOOL_DEFINITIONS = [
2617
2827
  },
2618
2828
  {
2619
2829
  name: "get_rule",
2620
- description: "Read one cardinal rule from CLAUDE.md (1-34) OR all if no number.",
2830
+ description: "Read one cardinal rule from CLAUDE.md (by number) OR all if no number.",
2621
2831
  inputSchema: {
2622
2832
  type: "object",
2623
- properties: { number: { type: "number", description: "Rule number 1-34." } }
2833
+ properties: { number: { type: "number", description: "Rule number (1-N)." } }
2624
2834
  }
2625
2835
  },
2626
2836
  {