@godxjp/ui-mcp 13.17.4 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -482,164 +482,91 @@ export default function Shell() {
482
482
  {
483
483
  name: "Topbar",
484
484
  group: "layout",
485
- tagline: "App-shell top bar with product/project chip switchers, search, notifications, and sidebar toggle \u2014 pass DropdownMenuContent to productMenu/projectMenu to turn any chip into a real dropdown switcher; the project chip is hidden entirely when neither project nor projectSidebar is set.",
485
+ tagline: "A PURE SLOT bar for the app shell \u2014 positions three clusters (start / center / end) and owns ONLY the bar layout. It bakes NO chrome: no product switcher, no search box, no notification bell, no language picker. The CONSUMER composes those from real primitives and drops them into a slot; whether a control is icon-only / labelled / bordered is that control's own config, never the shell's.",
486
486
  props: [
487
487
  {
488
- name: "product",
489
- type: "TopbarProductProp",
490
- required: true,
491
- description: "The active product identity shown in the left chip. Shape: `{ name: string; color?: string }`. `color` sets the chip icon background (defaults to `hsl(var(--attention))`); the first letter of `name` is used as the icon glyph."
492
- },
493
- {
494
- name: "project",
495
- type: "TopbarProjectProp | null",
496
- defaultValue: "undefined",
497
- description: "Optional active project shown after the product chip as `/ ProjectName`. Shape: `{ name: string }`. When both `project` and `projectMenu` are omitted the project chip is not rendered at all."
498
- },
499
- {
500
- name: "productMenu",
488
+ name: "start",
501
489
  type: "ReactNode",
502
490
  defaultValue: "undefined",
503
- description: "A `DropdownMenuContent` node. When provided, wraps the product chip in a `DropdownMenu` and renders this content as the dropdown body \u2014 turning the chip into an interactive switcher (e.g. an active-entity picker). When omitted, clicking the chip fires `onProductOpen` instead."
491
+ description: "Inline-start cluster \u2014 typically the sidebar toggle (a `Button` with a `PanelLeftClose` icon), the brand `Logo`, and primary nav."
504
492
  },
505
493
  {
506
- name: "projectMenu",
494
+ name: "center",
507
495
  type: "ReactNode",
508
496
  defaultValue: "undefined",
509
- description: "A `DropdownMenuContent` node. When provided, wraps the project chip in a `DropdownMenu`. Also causes the project chip to be rendered even when `project` is null \u2014 useful for a 'Pick project' state with a real dropdown. When omitted, clicking the chip fires `onProjectOpen`."
510
- },
511
- {
512
- name: "onProductOpen",
513
- type: "() => void",
514
- defaultValue: "undefined",
515
- description: "Called when the product chip is clicked and `productMenu` is NOT set. Use for custom modals / sheet-based switchers."
516
- },
517
- {
518
- name: "onProjectOpen",
519
- type: "() => void",
520
- defaultValue: "undefined",
521
- description: "Called when the project chip is clicked and `projectMenu` is NOT set."
522
- },
523
- {
524
- name: "onSearchOpen",
525
- type: "() => void",
526
- defaultValue: "undefined",
527
- description: "Called when the search bar button (\u2318K) is clicked. Wire this to your command-palette or search dialog."
528
- },
529
- {
530
- name: "searchPlaceholder",
531
- type: "string",
532
- defaultValue: 'i18n "layout.topbar.searchPlaceholder" ("\u691C\u7D22\u2026")',
533
- description: "Search-bar placeholder text \u2014 set a domain-specific hint (e.g. \u6848\u4EF6\u30FB\u53D7\u6CE8\u30FB\u9867\u5BA2\u3092\u691C\u7D22\u2026) instead of the generic default."
534
- },
535
- {
536
- name: "onTweaksOpen",
537
- type: "() => void",
538
- defaultValue: "undefined",
539
- description: "Called when the tweaks/settings icon button is clicked. The button is not rendered when this prop is omitted."
540
- },
541
- {
542
- name: "collapsed",
543
- type: "boolean",
544
- defaultValue: "false",
545
- description: "Whether the sidebar is currently collapsed. Controls which icon (`PanelLeftOpen` vs `PanelLeftClose`) is shown on the toggle button and sets its `aria-pressed` state."
546
- },
547
- {
548
- name: "onToggleCollapsed",
549
- type: "() => void",
550
- defaultValue: "undefined",
551
- description: "Called when the sidebar-toggle icon button is clicked. The button is not rendered when this prop is omitted."
497
+ description: "Center cluster (grows + centers) \u2014 optional. e.g. a search trigger (`Button` + `Search` icon opening your command palette) or a page/entity switcher (`DropdownMenu`)."
552
498
  },
553
499
  {
554
- name: "rightSlot",
500
+ name: "end",
555
501
  type: "ReactNode",
556
502
  defaultValue: "undefined",
557
- description: "Arbitrary content injected between the search bar and the notifications bell. Use for custom action buttons, locale switchers, or env badges."
558
- },
559
- {
560
- name: "unread",
561
- type: "boolean",
562
- defaultValue: "false",
563
- description: "When `true`, renders a red dot badge on the notifications bell to indicate unread notifications."
503
+ description: 'Inline-end cluster \u2014 settings pickers (`AppSettingPicker kind="locale"|"theme"`), a notifications `Button`, the user-menu `DropdownMenu`.'
564
504
  },
565
505
  {
566
- name: "onNotificationsOpen",
567
- type: "() => void",
506
+ name: "children",
507
+ type: "ReactNode",
568
508
  defaultValue: "undefined",
569
- description: "Called when the notifications bell button is clicked. The bell button is not rendered when this prop is omitted."
509
+ description: "Escape hatch \u2014 render fully custom bar content instead of the three slots. When set, `start`/`center`/`end` are ignored."
570
510
  },
571
511
  {
572
- name: "user",
573
- type: "ReactNode",
574
- defaultValue: "undefined",
575
- description: "User avatar / profile menu node rendered at the far right of the topbar, after the notifications bell and before the tweaks button."
512
+ name: "className",
513
+ type: "string",
514
+ description: "Merged onto the bar element; arbitrary props (aria-*, etc.) are forwarded."
576
515
  }
577
516
  ],
578
517
  usage: [
579
- "DO pass a `DropdownMenuContent` to `productMenu` or `projectMenu` to make a chip a real inline dropdown switcher (e.g. an entity/tenant picker). DO NOT combine both `productMenu` and `onProductOpen` \u2014 when `productMenu` is present, `onProductOpen` is ignored.",
580
- "DO omit both `project` and `projectMenu` when the app has no project concept \u2014 the project chip is hidden entirely. If you want a 'Pick project' placeholder with a real dropdown, pass only `projectMenu` (leave `project` null/undefined) so the chip renders in its empty state with the dropdown attached.",
581
- "DO render `Topbar` inside `AppShell`'s `topbar` prop \u2014 Topbar renders as a fragment of buttons/slots and relies on `AppShell`'s `app-topbar` CSS grid for layout. NEVER render it standalone outside of an `AppShell` or equivalent `<header>` container.",
582
- "DO wire `onSearchOpen` to your command-palette/dialog \u2014 the search bar button always renders (it is not conditional on this prop), so omitting the handler leaves users with a non-functional control.",
583
- "DO use `rightSlot` for extra topbar actions (e.g. locale switcher, environment badge, custom toolbar buttons) rather than adding children or extending the component.",
584
- "DON'T hand-roll a topbar from scratch \u2014 Topbar ships the sidebar toggle, product/project switcher, search, notifications bell, user slot, and tweaks button with correct `aria-label`/`aria-pressed` attributes already."
518
+ "DO compose the bar yourself: brand `Logo` + sidebar toggle in `start`, a search trigger in `center`, settings pickers + notifications + user menu in `end`. The shell only positions; it never decides WHICH controls exist.",
519
+ 'DO build the sidebar toggle as a `Button variant="ghost" size="icon-sm"` with a `PanelLeftClose`/`PanelLeftOpen` icon and your own `t()` aria-label, wired to AppShell\'s `sidebarCollapsed`. There is no baked toggle.',
520
+ "DO put a locale/theme switcher in `end` using `AppSettingPicker` (or your own control) \u2014 icon-only vs labelled, bordered vs not, is THAT component's prop, not Topbar's. Topbar does not ship or force a language picker.",
521
+ "DON'T look for `product`/`project`/`onSearchOpen`/`onNotificationsOpen`/`collapsed` props \u2014 they were removed. A chrome control only exists if YOU put it in a slot, so there is never a dead dropdown / empty search with nothing behind it.",
522
+ "DO render Topbar inside `AppShell`'s `topbar` slot (or any `<header>`). For a non-three-cluster layout, pass `children` and lay it out yourself."
585
523
  ],
586
524
  useCases: [
587
- "Admin / SaaS shell where the header shows the active legal entity (product chip) and users switch between entities via a `DropdownMenuContent` passed to `productMenu`.",
588
- "Multi-project app where the project chip shows the current project and `projectMenu` provides a `DropdownMenuContent` to switch projects inline without opening a modal.",
589
- "App-shell with a collapsible sidebar: pass `collapsed` + `onToggleCollapsed` to let users toggle the sidebar from the topbar without building a custom toggle button.",
590
- "Notification-aware shell: pass `onNotificationsOpen` + `unread={hasUnread}` to render a bell icon with a red-dot badge that opens a notifications panel.",
591
- "Apps needing a locale switcher or environment badge in the header: put it in `rightSlot` to slot it between the search bar and the notifications bell without modifying the component.",
592
- "Read-only / minimal shell where sidebar toggling, tweaks, and notifications are not needed \u2014 simply omit `onToggleCollapsed`, `onTweaksOpen`, and `onNotificationsOpen`; their buttons are not rendered."
525
+ "Admin shell: `start` = sidebar toggle + Logo + an entity switcher (`DropdownMenu` around a `Button`); `center` = a `Button` search trigger; `end` = `AppSettingPicker` (locale) + a notifications `Button` + a user `DropdownMenu`.",
526
+ "Minimal shell (no search, no notifications): pass only `start` (Logo) and `end` (user menu). Nothing else renders \u2014 no empty chrome.",
527
+ "Marketing / docs header: pass `children` with a fully custom flex layout when the three-cluster model doesn't fit."
593
528
  ],
594
529
  related: [
595
- "AppShell \u2014 the parent shell component that places Topbar inside its `app-topbar` header region via the `topbar` prop. Always use Topbar inside AppShell, not standalone.",
596
- "Sidebar \u2014 the companion left-rail nav; pair with Topbar's `collapsed`/`onToggleCollapsed` to keep sidebar and topbar toggle state in sync.",
597
- "DropdownMenu / DropdownMenuContent \u2014 pass a `DropdownMenuContent` as `productMenu` or `projectMenu` to turn a chip into an inline switcher. Topbar handles the `DropdownMenuTrigger` wrapping internally; only the Content node is needed.",
598
- "AppShell \u2014 a higher-level opinionated shell that already composes AppShell + Topbar with hardcoded product/project chips; use it for prototypes but use AppShell + Topbar directly for production apps that need real switcher props."
530
+ "AppShell \u2014 place Topbar in its `topbar` slot. AppShell also exposes its own `logo`/`topbarLeft`/`topbarRight` slots if you don't want a separate Topbar at all.",
531
+ "Logo \u2014 the brand mark for the `start` slot.",
532
+ "AppSettingPicker \u2014 locale/theme/timezone/currency picker; the consumer drops it into `end`. Its appearance (icon-only, labelled, bordered) is configured on IT, not on Topbar.",
533
+ "DropdownMenu \u2014 wrap a `Button` to build an entity switcher or user menu yourself, then place it in a slot."
599
534
  ],
600
- example: `import { Topbar } from "@godxjp/ui/layout";
601
- import { AppShell } from "@godxjp/ui/layout";
602
- import {
603
- DropdownMenuContent,
604
- DropdownMenuItem,
605
- } from "@godxjp/ui/navigation";
606
-
607
- // Entity-switcher example: product chip opens an entity dropdown,
608
- // project chip is hidden (no project concept in this app).
609
- function MyShell({ children }: { content: React.ReactNode }) {
610
- const [collapsed, setCollapsed] = React.useState(false);
611
- const [unread, setUnread] = React.useState(true);
612
-
613
- return (
614
- <AppShell
615
- sidebar={<MySidebar />}
616
- sidebarCollapsed={collapsed}
617
- topbar={
618
- <Topbar
619
- product={{ name: "CoreBooks", color: "hsl(220 70% 50%)" }}
620
- productMenu={
621
- <DropdownMenuContent align="start">
622
- <DropdownMenuItem onSelect={() => switchEntity("acme")}>
623
- Acme Corp
624
- </DropdownMenuItem>
625
- <DropdownMenuItem onSelect={() => switchEntity("globex")}>
626
- Globex Ltd
627
- </DropdownMenuItem>
628
- </DropdownMenuContent>
629
- }
630
- collapsed={collapsed}
631
- onToggleCollapsed={() => setCollapsed((c) => !c)}
632
- onSearchOpen={() => openCommandPalette()}
633
- unread={unread}
634
- onNotificationsOpen={() => openNotificationsPanel()}
635
- user={<UserAvatar />}
636
- />
535
+ example: `import { Topbar, AppShell } from "@godxjp/ui/layout";
536
+ import { Logo, Button } from "@godxjp/ui/general";
537
+ import { AppSettingPicker } from "@godxjp/ui/navigation";
538
+ import { PanelLeftClose, Search } from "lucide-react";
539
+
540
+ // The shell gives you slots; YOU decide what goes in them.
541
+ <AppShell
542
+ sidebar={<MySidebar />}
543
+ topbar={
544
+ <Topbar
545
+ start={
546
+ <>
547
+ <Button variant="ghost" size="icon-sm" aria-label={t("toggleSidebar")} onClick={toggle}>
548
+ <PanelLeftClose />
549
+ </Button>
550
+ <Logo label="CoreBooks" />
551
+ </>
637
552
  }
638
- >
639
- {children}
640
- </AppShell>
641
- );
642
- }`,
553
+ center={
554
+ <Button variant="outline" size="sm" onClick={openSearch}>
555
+ <Search />
556
+ {t("search")}
557
+ </Button>
558
+ }
559
+ end={
560
+ <>
561
+ <AppSettingPicker kind="locale" />
562
+ <UserMenu />
563
+ </>
564
+ }
565
+ />
566
+ }
567
+ >
568
+ {children}
569
+ </AppShell>`,
643
570
  storyPath: "layout/Topbar.stories.tsx",
644
571
  rules: [2, 3, 5, 6]
645
572
  },
@@ -759,6 +686,12 @@ function MyShell({ children }: { content: React.ReactNode }) {
759
686
  defaultValue: '"default"',
760
687
  description: "Corner radius from the tokens \u2014 `default` (control radius), `pill` (fully rounded, --radius-pill), `sharp` (square, --radius-sharp). Use the prop instead of a `rounded-*` className."
761
688
  },
689
+ {
690
+ name: "fullWidth",
691
+ type: "boolean",
692
+ defaultValue: "false",
693
+ description: 'Span the full container width (`width:100%`) instead of sizing to content. Use the prop instead of `className="w-full"` for stacked / auth-form / dialog-footer actions (rule #42).'
694
+ },
762
695
  {
763
696
  name: "asChild",
764
697
  type: "boolean",
@@ -931,6 +864,63 @@ import { Trash2 } from "lucide-react";
931
864
  <Heading level={2}>\u8ACB\u6C42\u66F8\u4E00\u89A7</Heading>
932
865
  <Heading level={3} tone="muted">\u88DC\u8DB3\u30BB\u30AF\u30B7\u30E7\u30F3</Heading>`
933
866
  },
867
+ {
868
+ name: "Logo",
869
+ group: "general",
870
+ tagline: 'Product brand-mark \u2014 the lettermark (or custom SVG) in a tokenized box. Use instead of a hand-rolled `<span className="flex size-8 rounded-md bg-primary font-bold">g</span>` (typography-on-span, literal size/radius).',
871
+ props: [
872
+ {
873
+ name: "glyph",
874
+ type: "ReactNode",
875
+ defaultValue: '"G"',
876
+ description: "The mark \u2014 a short lettermark string or a custom SVG/image node."
877
+ },
878
+ {
879
+ name: "label",
880
+ type: "string",
881
+ description: 'Accessible product name. When set the mark exposes `role="img"` + `aria-label` (use when the Logo IS the accessible name, e.g. a home link). Omitted \u2192 the mark is `aria-hidden` (decorative; an adjacent wordmark names it).'
882
+ },
883
+ {
884
+ name: "size",
885
+ type: '"xs" | "sm" | "md" | "lg"',
886
+ defaultValue: '"md"',
887
+ description: "Square box size (24 / 28 / 32 / 40). Size comes from the prop, never a literal."
888
+ },
889
+ {
890
+ name: "shape",
891
+ type: '"default" | "pill" | "sharp"',
892
+ defaultValue: '"default"',
893
+ description: "Corner shape \u2014 `default` (--logo-radius, a service knob), `pill` (fully rounded), `sharp` (square)."
894
+ }
895
+ ],
896
+ usage: [
897
+ "DO use Logo for the product glyph in a topbar, sidebar header, or auth shell instead of hand-rolling a styled `<span>`/`<div>` with a literal size + radius + bg (cardinal rules #42/#46).",
898
+ "DO pass `label` when the mark is the only branding AND acts as the accessible name (e.g. wrapped in a home link); omit it when a wordmark sits beside it (then the mark is decorative `aria-hidden`).",
899
+ "DO retheme via tokens \u2014 brand fill follows `--primary`; a service squares/rounds the corner globally via `--logo-radius`. DON'T override size/radius with a raw className.",
900
+ "DON'T put body copy in Logo \u2014 it is a brand mark (bold lettermark / SVG), not a Text/Heading substitute."
901
+ ],
902
+ useCases: [
903
+ 'Topbar / sidebar header brand mark next to a wordmark: `<Logo /> <Text weight="bold">GoDX</Text>`.',
904
+ 'Auth shell (sign-in card) centered product mark with an accessible name: `<Logo size="lg" label="GoDX" />`.',
905
+ 'A home link in an app shell where the logo IS the link label: `<a href="/"><Logo label="GoDX \u30DB\u30FC\u30E0" /></a>`.'
906
+ ],
907
+ related: [
908
+ "Avatar \u2014 for a USER/person image or initials; Logo is for the PRODUCT/brand mark.",
909
+ "Text / Heading \u2014 for the wordmark or any real copy beside the mark; Logo never holds prose."
910
+ ],
911
+ example: `import { Logo, Text } from "@godxjp/ui/general";
912
+
913
+ // Topbar lettermark + wordmark
914
+ <span className="inline-flex items-center gap-2">
915
+ <Logo />
916
+ <Text weight="bold">GoDX</Text>
917
+ </span>
918
+
919
+ // Auth shell, the mark is the accessible name
920
+ <Logo size="lg" label="GoDX" />`,
921
+ storyPath: "general/Logo.stories.tsx",
922
+ rules: [42, 46]
923
+ },
934
924
  // ─── data-display ───────────────────────────────────────────────────────
935
925
  {
936
926
  name: "DataTable",
@@ -1057,6 +1047,7 @@ import { Trash2 } from "lucide-react";
1057
1047
  "Table \u2014 raw primitive (TableHeader/TableBody/TableRow/TableCell). Use DataTable instead; only reach for Table directly when you need a non-standard layout that DataTable cannot express.",
1058
1048
  "SkeletonTable \u2014 standalone skeleton placeholder rendered before any DataTable mounts (e.g. in a Suspense fallback or deferred-prop skeleton slot). DataTable.loading covers in-table loading; SkeletonTable covers pre-mount skeletons.",
1059
1049
  "EmptyState \u2014 standalone empty state for non-table lists. DataTable already embeds EmptyState in its body; only use bare EmptyState for card content, non-tabular lists, or zero-state pages outside a DataTable.",
1050
+ "LineChart / BarChart / AreaChart / PieChart (@godxjp/ui/charts) \u2014 when the SHAPE or trend of aggregated data matters more than exact per-row figures, visualize it with a chart instead of (or alongside) the table; keep DataTable when users need to read, sort, or act on individual rows.",
1060
1051
  "DataState / InfiniteQueryState \u2014 TanStack Query lifecycle widgets from @godxjp/ui/query. Prefer these over DataTable when your list is driven by useQuery/useInfiniteQuery and you want automatic skeleton/empty/error handling at the query level rather than at the table level."
1061
1052
  ],
1062
1053
  example: `import { useState } from "react";
@@ -1400,7 +1391,8 @@ export function Grid({ rows }: { rows: Row[] }) {
1400
1391
  "ResponsiveGrid \u2014 required layout wrapper for StatCard grids; controls column count and responsive breakpoints. Always pair them together.",
1401
1392
  "SkeletonStat \u2014 exact loading placeholder shaped like a StatCard tile; swap in while KPI data is fetching, then replace with the real StatCard.",
1402
1393
  "Descriptions \u2014 use instead when displaying multiple label/value metadata pairs on a detail page (not headline KPIs); Descriptions is not card-bordered and does not show delta/hint slots.",
1403
- "Card + CardContent \u2014 use when you need a general-purpose content container with a header, footer, or arbitrary body; do NOT wrap StatCard inside these."
1394
+ "Card + CardContent \u2014 use when you need a general-purpose content container with a header, footer, or arbitrary body; do NOT wrap StatCard inside these.",
1395
+ "LineChart / BarChart / PieChart (@godxjp/ui/charts) \u2014 when the trend or composition BEHIND a KPI matters, pair the StatCard headline number with a chart in the same dashboard grid: StatCard states the figure, the chart shows its shape over time or its breakdown."
1404
1396
  ],
1405
1397
  example: `import { StatCard } from "@godxjp/ui/data-display";
1406
1398
  import { ResponsiveGrid } from "@godxjp/ui/layout";
@@ -1481,6 +1473,87 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
1481
1473
  storyPath: "data-display/Badge.stories.tsx",
1482
1474
  rules: [35]
1483
1475
  },
1476
+ {
1477
+ name: "ListRow",
1478
+ group: "data-display",
1479
+ tagline: "Single-line entity row (leading \xB7 title/description \xB7 trailing action) for SHORT lists inside a Card \u2014 sessions, API tokens, linked accounts, passkeys, MFA factors, invitations.",
1480
+ props: [
1481
+ {
1482
+ name: "title",
1483
+ type: "ReactNode",
1484
+ required: true,
1485
+ description: "Primary line \u2014 rendered in medium weight; truncates to one line."
1486
+ },
1487
+ {
1488
+ name: "description",
1489
+ type: "ReactNode",
1490
+ description: "Secondary line under the title (muted, xs); truncates to one line."
1491
+ },
1492
+ {
1493
+ name: "leading",
1494
+ type: "ReactNode",
1495
+ description: "Leading slot \u2014 a decorative icon or an Avatar. Mark a purely decorative icon `aria-hidden`."
1496
+ },
1497
+ {
1498
+ name: "trailing",
1499
+ type: "ReactNode",
1500
+ description: "Trailing slot \u2014 the row action(s): a Button / DropdownMenu trigger, a Badge, or a Switch."
1501
+ },
1502
+ {
1503
+ name: "align",
1504
+ type: '"center" | "start"',
1505
+ defaultValue: '"center"',
1506
+ description: "Cross-axis alignment of the columns; `start` for multi-line content."
1507
+ },
1508
+ {
1509
+ name: "as",
1510
+ type: '"div" | "li"',
1511
+ defaultValue: '"div"',
1512
+ description: "Render element \u2014 `li` when the parent is a semantic `<ul>`/`<ol>`."
1513
+ }
1514
+ ],
1515
+ usage: [
1516
+ "DO use ListRow for a SHORT (\u22482\u20138 item) list of entities inside a Card where each row is one line with an action \u2014 account sessions, API keys, linked identities, passkeys. Stack rows in a `<Card><CardContent flush>` so the rows draw their own quiet dividers edge-to-edge.",
1517
+ "DON'T reach for DataTable here \u2014 it carries sorting/selection/pagination chrome that a 3-item list doesn't need. DON'T nest a Card per row either (card-in-card). ListRow is the in-between surface.",
1518
+ 'DON\'T hand-roll `<div className="flex items-center justify-between border-b py-3">` \u2014 that is exactly the repeated pattern ListRow replaces (border/radius/padding are tokenized via `--list-row-*`).',
1519
+ 'DO put the row\'s action in `trailing` (a `ghost`/`outline` Button, a DropdownMenu trigger, a Switch, or a status Badge). DO pass `as="li"` when the rows live inside a semantic `<ul>`.'
1520
+ ],
1521
+ useCases: [
1522
+ "Account security page \u2014 a list of active sessions (device + last-seen as title/description, a destructive 'Revoke' Button in trailing).",
1523
+ "Developer settings \u2014 API tokens or passkeys, each row showing the name + created date and a DropdownMenu of actions.",
1524
+ "Linked accounts / SSO \u2014 an IdP icon in leading, the provider name + connected email, and a Switch or 'Disconnect' Button trailing."
1525
+ ],
1526
+ related: [
1527
+ "DataTable \u2014 use instead when the list is long or needs sorting/selection/pagination; ListRow is for short, chrome-light lists.",
1528
+ "Card \u2014 ListRow is designed to live inside `<CardContent flush>`; the Card supplies the outer surface and the closing border.",
1529
+ "Descriptions \u2014 for a key/value metadata grid on a detail page (no per-row action); ListRow is for actionable entity rows."
1530
+ ],
1531
+ example: `import { Card, CardContent, CardHeader, CardTitle, ListRow, Badge } from "@godxjp/ui/data-display";
1532
+ import { Button } from "@godxjp/ui/general";
1533
+ import { Smartphone } from "lucide-react";
1534
+
1535
+ <Card>
1536
+ <CardHeader>
1537
+ <CardTitle>\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30BB\u30C3\u30B7\u30E7\u30F3</CardTitle>
1538
+ </CardHeader>
1539
+ <CardContent flush>
1540
+ <ListRow
1541
+ leading={<Smartphone aria-hidden="true" className="size-4" />}
1542
+ title="iPhone 15 \xB7 Tokyo"
1543
+ description="\u6700\u7D42\u30A2\u30AF\u30BB\u30B9 2\u5206\u524D"
1544
+ trailing={<Badge status="active" />}
1545
+ />
1546
+ <ListRow
1547
+ leading={<Smartphone aria-hidden="true" className="size-4" />}
1548
+ title="MacBook Pro \xB7 Osaka"
1549
+ description="\u6700\u7D42\u30A2\u30AF\u30BB\u30B9 3\u65E5\u524D"
1550
+ trailing={<Button size="xs" variant="outline">\u30ED\u30B0\u30A2\u30A6\u30C8</Button>}
1551
+ />
1552
+ </CardContent>
1553
+ </Card>`,
1554
+ storyPath: "data-display/ListRow.stories.tsx",
1555
+ rules: [42, 44]
1556
+ },
1484
1557
  {
1485
1558
  name: "Descriptions",
1486
1559
  group: "data-display",
@@ -1613,7 +1686,8 @@ import { ResponsiveGrid } from "@godxjp/ui/layout";
1613
1686
  "Slider \u2014 use Slider when the user must drag or set a bounded numeric value (volume, priority, price range); use Progress when the value is read-only and must not be interacted with.",
1614
1687
  "Steps \u2014 use Steps for a discrete, named sequence of phases (onboarding wizard, checkout flow) where each step has a label and a clear current/done/pending state; use Progress for a continuous 0\u2013100 fill.",
1615
1688
  'Badge / Badge \u2014 use Badge or Badge to communicate a categorical status label (e.g. "Paid", "Overdue") without a fill metaphor; use Progress when the numeric proportion itself is the information.',
1616
- "StatCard \u2014 use StatCard to headline a single KPI metric with a title; compose Progress inside or alongside StatCard when a visual fill adds meaning to the number."
1689
+ "StatCard \u2014 use StatCard to headline a single KPI metric with a title; compose Progress inside or alongside StatCard when a visual fill adds meaning to the number.",
1690
+ "BarChart / PieChart / LineChart (@godxjp/ui/charts) \u2014 Progress shows ONE ratio against a target; the moment you have several series, categories, or a part-to-whole split (or a value changing over time), move up to a chart instead of stacking many Progress bars."
1617
1691
  ],
1618
1692
  example: `import { Progress } from "@godxjp/ui/data-display";
1619
1693
 
@@ -3263,6 +3337,7 @@ import { Button } from "@godxjp/ui/general";
3263
3337
  }
3264
3338
  ],
3265
3339
  usage: [
3340
+ "ANATOMY (positions are fixed \u2014 never re-lay-them-out): Alert is ONE horizontal row \u2014 a SINGLE leading tone icon at the inline-start (top-aligned to the first text line, auto-selected by `tone`, never two icons), then the text body (Title/Description), then `<Alert.Actions>` in a trailing-RIGHT column (\u2265sm), and the dismiss \xD7 pinned to the TOP-RIGHT corner (rendered by `onDismiss`). DON'T stack these vertically, DON'T make the action a full-width bar under the text, DON'T center the \xD7 at the bottom, DON'T add a second icon \u2014 that hand-rolled vertical banner is the #1 Alert mistake.",
3266
3341
  'DO compose text as `<Alert.Title>` + `<Alert.Description>` \u2014 they stack vertically inside the body (the Example below is canonical). When you add `<Alert.Actions>`, the body becomes a two-column grid (text | actions) at \u2265sm; group multi-part text in `<Alert.Content>` so it occupies the text column as one block: `<Alert tone="destructive"><Alert.Content><Alert.Title>Error</Alert.Title><Alert.Description>{msg}</Alert.Description></Alert.Content><Alert.Actions><Button \u2026/></Alert.Actions></Alert>`.',
3267
3342
  "DO use `Alert.QueryError` (alias `AlertQueryError`) for TanStack Query / API failure surfaces \u2014 it already renders humanError(error), an i18n title, and an optional Retry button. Never hand-roll that pattern.",
3268
3343
  'DON\'T pass raw action elements directly as top-level children of `<Alert>` without wrapping them in `<Alert.Actions>` \u2014 the layout slot only activates correctly via the `data-slot="alert-actions"` wrapper.',
@@ -7189,60 +7264,475 @@ export function NotifyRow() {
7189
7264
  }\`}`,
7190
7265
  storyPath: "data-entry/Field.stories.tsx",
7191
7266
  rules: [23]
7192
- }
7193
- ];
7194
- function findComponent(name) {
7195
- const normalized = name.trim().toLowerCase();
7196
- return COMPONENTS.find((c) => c.name.toLowerCase() === normalized);
7197
- }
7198
- function componentsByGroup(group) {
7199
- return COMPONENTS.filter((c) => c.group === group);
7200
- }
7201
-
7202
- // src/data/prop-vocabulary.ts
7203
- var PROP_VOCABULARY = [
7204
- {
7205
- name: "ValueProp<T = string>",
7206
- concept: "Abstract controlled value.",
7207
- values: ["generic"],
7208
- usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Tabs", "SearchSelect"]
7209
- },
7210
- {
7211
- name: "DefaultValueProp<T = string>",
7212
- concept: "Abstract uncontrolled initial value.",
7213
- values: ["generic"],
7214
- usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Tabs"]
7215
- },
7216
- {
7217
- name: "OnValueChangeProp<T = string>",
7218
- concept: "Callback for abstract value changes. DOM events continue to use onChange.",
7219
- values: ["(value: T) => void"],
7220
- usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Transfer", "settings pickers"]
7221
- },
7222
- {
7223
- name: "OpenProp / DefaultOpenProp / OnOpenChangeProp",
7224
- concept: "Disclosure state.",
7225
- values: ["boolean", "(open: boolean) => void"],
7226
- usedBy: ["Dialog", "Sheet", "Popover"]
7227
7267
  },
7268
+ // ─── charts (tree-shaken `@godxjp/ui/charts`; needs the `recharts` optional peer) ───
7228
7269
  {
7229
- name: "SizeProp",
7230
- concept: "Shared public size names.",
7231
- values: ["xs", "sm", "md", "lg"],
7232
- usedBy: ["Button", "Steps", "Switch"],
7233
- notes: "Component-specific subsets must be documented. Old alias small is sm."
7234
- },
7235
- {
7236
- name: "ToneProp",
7237
- concept: "Semantic status/color intent.",
7238
- values: ["default", "success", "warning", "destructive", "info", "muted", "neutral"],
7239
- usedBy: ["Badge", "Alert"],
7240
- notes: "Status values belong in tone, not variant."
7270
+ name: "LineChart",
7271
+ group: "data-display",
7272
+ importPath: "@godxjp/ui/charts",
7273
+ tagline: "Trends over an ordered category axis \u2014 one or more series, locale-formatted ticks/tooltips, screen-reader text alternative built in. Data-visualization graph / plot.",
7274
+ props: [
7275
+ {
7276
+ name: "data",
7277
+ type: "ChartDatum[]",
7278
+ required: true,
7279
+ description: "Row data: one category per row with a numeric value per series."
7280
+ },
7281
+ {
7282
+ name: "series",
7283
+ type: "ChartSeriesProp[]",
7284
+ required: true,
7285
+ description: "Plotted series: { dataKey, label?, color? }. Colour defaults to the --chart-1..6 palette."
7286
+ },
7287
+ {
7288
+ name: "categoryKey",
7289
+ type: "string",
7290
+ required: true,
7291
+ description: "Key into each datum holding the x-axis category label."
7292
+ },
7293
+ {
7294
+ name: "label",
7295
+ type: "string",
7296
+ required: true,
7297
+ description: "Accessible name + visible caption (role=img needs a name)."
7298
+ },
7299
+ {
7300
+ name: "description",
7301
+ type: "string",
7302
+ description: "Extra context appended to the screen-reader description."
7303
+ },
7304
+ {
7305
+ name: "size",
7306
+ type: '"xs" | "sm" | "md" | "lg"',
7307
+ defaultValue: '"md"',
7308
+ description: "Canvas height preset. Ignored when `height` is set."
7309
+ },
7310
+ {
7311
+ name: "height",
7312
+ type: "number",
7313
+ description: "Explicit canvas height in px (overrides `size`)."
7314
+ },
7315
+ {
7316
+ name: "showLegend",
7317
+ type: "boolean",
7318
+ defaultValue: "true",
7319
+ description: "Show the series legend."
7320
+ },
7321
+ {
7322
+ name: "showGrid",
7323
+ type: "boolean",
7324
+ defaultValue: "true",
7325
+ description: "Show the cartesian background grid."
7326
+ },
7327
+ {
7328
+ name: "numberFormat",
7329
+ type: "Intl.NumberFormatOptions",
7330
+ description: "Locale-aware formatting for axis ticks + tooltip values."
7331
+ },
7332
+ {
7333
+ name: "curved",
7334
+ type: "boolean",
7335
+ defaultValue: "false",
7336
+ description: "Render smooth (monotone) lines instead of straight segments."
7337
+ },
7338
+ {
7339
+ name: "emptyMessage",
7340
+ type: "string",
7341
+ description: "Message shown when `data` is empty (defaults to a localized 'no data')."
7342
+ }
7343
+ ],
7344
+ usage: [
7345
+ 'DO import from the tree-shaken charts entry: `import { LineChart } from "@godxjp/ui/charts";`. Importing any other subpath never pulls in recharts.',
7346
+ "DO install the `recharts` optional peer dependency in the consuming app \u2014 charts are the only part of @godxjp/ui that needs it, so apps without charts never pay for it.",
7347
+ "DO pass an i18n'd `label` \u2014 it is both the visible caption and the accessible name; the component also emits a screen-reader list of the plotted values (WCAG 1.1.1).",
7348
+ "DO pre-translate each series' `label`; pass `numberFormat` (e.g. { style: 'currency', currency: 'JPY' }) and the axis/tooltip numbers localize automatically via Intl.",
7349
+ "DON'T hand-roll an SVG/canvas chart or drop raw recharts into a page \u2014 LineChart owns the colour tokens, locale formatting, empty state, and accessibility wiring."
7350
+ ],
7351
+ useCases: [
7352
+ "Revenue / KPI trend over months in a dashboard.",
7353
+ "Multi-series comparison (e.g. plan vs actual) across a time axis.",
7354
+ "Any continuous metric where the shape of the trend is the message."
7355
+ ],
7356
+ related: [
7357
+ "AreaChart \u2014 use when the filled magnitude under the line matters (cumulative/stacked volume).",
7358
+ "BarChart \u2014 use for discrete category comparison rather than a continuous trend.",
7359
+ "DataTable \u2014 use when exact per-row figures matter more than the trend shape."
7360
+ ],
7361
+ example: `import { LineChart } from "@godxjp/ui/charts";
7362
+
7363
+ <LineChart
7364
+ label={t("dashboard.revenueTrend")}
7365
+ data={data}
7366
+ categoryKey="month"
7367
+ series={[
7368
+ { dataKey: "plan", label: t("metric.plan") },
7369
+ { dataKey: "actual", label: t("metric.actual") },
7370
+ ]}
7371
+ numberFormat={{ style: "currency", currency: "JPY" }}
7372
+ />`,
7373
+ storyPath: "charts/LineChart.stories.tsx",
7374
+ rules: []
7241
7375
  },
7242
7376
  {
7243
- name: "GapProp",
7244
- concept: "Shared layout gap scale.",
7245
- values: ["xs", "sm", "md", "lg", "xl"],
7377
+ name: "BarChart",
7378
+ group: "data-display",
7379
+ importPath: "@godxjp/ui/charts",
7380
+ tagline: "Compare a value across categories \u2014 grouped or `stacked`, vertical or `horizontal`, with localized ticks/tooltips and a built-in text alternative. Data-visualization graph / plot / diagram.",
7381
+ props: [
7382
+ {
7383
+ name: "data",
7384
+ type: "ChartDatum[]",
7385
+ required: true,
7386
+ description: "Row data: one category per row with a numeric value per series."
7387
+ },
7388
+ {
7389
+ name: "series",
7390
+ type: "ChartSeriesProp[]",
7391
+ required: true,
7392
+ description: "Plotted series: { dataKey, label?, color? }."
7393
+ },
7394
+ {
7395
+ name: "categoryKey",
7396
+ type: "string",
7397
+ required: true,
7398
+ description: "Key into each datum holding the category label."
7399
+ },
7400
+ {
7401
+ name: "label",
7402
+ type: "string",
7403
+ required: true,
7404
+ description: "Accessible name + visible caption."
7405
+ },
7406
+ {
7407
+ name: "description",
7408
+ type: "string",
7409
+ description: "Extra context appended to the screen-reader description."
7410
+ },
7411
+ {
7412
+ name: "size",
7413
+ type: '"xs" | "sm" | "md" | "lg"',
7414
+ defaultValue: '"md"',
7415
+ description: "Canvas height preset. Ignored when `height` is set."
7416
+ },
7417
+ {
7418
+ name: "height",
7419
+ type: "number",
7420
+ description: "Explicit canvas height in px (overrides `size`)."
7421
+ },
7422
+ {
7423
+ name: "showLegend",
7424
+ type: "boolean",
7425
+ defaultValue: "true",
7426
+ description: "Show the series legend."
7427
+ },
7428
+ {
7429
+ name: "showGrid",
7430
+ type: "boolean",
7431
+ defaultValue: "true",
7432
+ description: "Show the cartesian background grid."
7433
+ },
7434
+ {
7435
+ name: "numberFormat",
7436
+ type: "Intl.NumberFormatOptions",
7437
+ description: "Locale-aware formatting for ticks + tooltip values."
7438
+ },
7439
+ {
7440
+ name: "stacked",
7441
+ type: "boolean",
7442
+ defaultValue: "false",
7443
+ description: "Stack series into one bar instead of grouping side by side."
7444
+ },
7445
+ {
7446
+ name: "horizontal",
7447
+ type: "boolean",
7448
+ defaultValue: "false",
7449
+ description: "Lay bars out horizontally (category on the y-axis)."
7450
+ },
7451
+ { name: "emptyMessage", type: "string", description: "Message shown when `data` is empty." }
7452
+ ],
7453
+ usage: [
7454
+ 'DO import from the charts entry: `import { BarChart } from "@godxjp/ui/charts";` (recharts optional peer required).',
7455
+ "DO use `horizontal` when category labels are long (they read better on the y-axis).",
7456
+ "DO use `stacked` for part-to-whole-per-category; keep grouped (default) for direct side-by-side comparison.",
7457
+ "DON'T use BarChart for a single part-to-whole total \u2014 that is PieChart. DON'T fake bars with styled divs."
7458
+ ],
7459
+ useCases: [
7460
+ "Sales by region / category comparison.",
7461
+ "Stacked composition per period (e.g. expense breakdown by month).",
7462
+ "Ranking with long labels (horizontal)."
7463
+ ],
7464
+ related: [
7465
+ "LineChart \u2014 continuous trend rather than discrete comparison.",
7466
+ "PieChart \u2014 single part-to-whole composition.",
7467
+ "DataTable \u2014 exact tabular figures."
7468
+ ],
7469
+ example: `import { BarChart } from "@godxjp/ui/charts";
7470
+
7471
+ <BarChart
7472
+ label={t("report.salesByRegion")}
7473
+ data={data}
7474
+ categoryKey="region"
7475
+ series={[{ dataKey: "sales", label: t("metric.sales") }]}
7476
+ numberFormat={{ notation: "compact" }}
7477
+ />`,
7478
+ storyPath: "charts/BarChart.stories.tsx",
7479
+ rules: []
7480
+ },
7481
+ {
7482
+ name: "AreaChart",
7483
+ group: "data-display",
7484
+ importPath: "@godxjp/ui/charts",
7485
+ tagline: "Magnitude over an ordered category axis \u2014 overlay or `stacked` areas, optional `curved` smoothing, localized formatting + text alternative. Data-visualization graph / plot.",
7486
+ props: [
7487
+ {
7488
+ name: "data",
7489
+ type: "ChartDatum[]",
7490
+ required: true,
7491
+ description: "Row data: one category per row with a numeric value per series."
7492
+ },
7493
+ {
7494
+ name: "series",
7495
+ type: "ChartSeriesProp[]",
7496
+ required: true,
7497
+ description: "Plotted series: { dataKey, label?, color? }."
7498
+ },
7499
+ {
7500
+ name: "categoryKey",
7501
+ type: "string",
7502
+ required: true,
7503
+ description: "Key into each datum holding the x-axis category label."
7504
+ },
7505
+ {
7506
+ name: "label",
7507
+ type: "string",
7508
+ required: true,
7509
+ description: "Accessible name + visible caption."
7510
+ },
7511
+ {
7512
+ name: "description",
7513
+ type: "string",
7514
+ description: "Extra context appended to the screen-reader description."
7515
+ },
7516
+ {
7517
+ name: "size",
7518
+ type: '"xs" | "sm" | "md" | "lg"',
7519
+ defaultValue: '"md"',
7520
+ description: "Canvas height preset. Ignored when `height` is set."
7521
+ },
7522
+ {
7523
+ name: "height",
7524
+ type: "number",
7525
+ description: "Explicit canvas height in px (overrides `size`)."
7526
+ },
7527
+ {
7528
+ name: "showLegend",
7529
+ type: "boolean",
7530
+ defaultValue: "true",
7531
+ description: "Show the series legend."
7532
+ },
7533
+ {
7534
+ name: "showGrid",
7535
+ type: "boolean",
7536
+ defaultValue: "true",
7537
+ description: "Show the cartesian background grid."
7538
+ },
7539
+ {
7540
+ name: "numberFormat",
7541
+ type: "Intl.NumberFormatOptions",
7542
+ description: "Locale-aware formatting for ticks + tooltip values."
7543
+ },
7544
+ {
7545
+ name: "stacked",
7546
+ type: "boolean",
7547
+ defaultValue: "false",
7548
+ description: "Stack series areas instead of overlaying them."
7549
+ },
7550
+ {
7551
+ name: "curved",
7552
+ type: "boolean",
7553
+ defaultValue: "false",
7554
+ description: "Render smooth (monotone) areas instead of straight segments."
7555
+ },
7556
+ { name: "emptyMessage", type: "string", description: "Message shown when `data` is empty." }
7557
+ ],
7558
+ usage: [
7559
+ 'DO import from the charts entry: `import { AreaChart } from "@godxjp/ui/charts";` (recharts optional peer required).',
7560
+ "DO use `stacked` to show how parts accumulate into a total over time.",
7561
+ "DON'T overlay more than 2-3 unstacked areas \u2014 fill opacity makes dense overlays unreadable; switch to LineChart."
7562
+ ],
7563
+ useCases: [
7564
+ "Cumulative volume over time (e.g. total transactions per day).",
7565
+ "Stacked composition trend (traffic sources, revenue streams)."
7566
+ ],
7567
+ related: [
7568
+ "LineChart \u2014 when only the trend line matters, not the filled magnitude.",
7569
+ "BarChart \u2014 discrete category comparison."
7570
+ ],
7571
+ example: `import { AreaChart } from "@godxjp/ui/charts";
7572
+
7573
+ <AreaChart
7574
+ label={t("dashboard.trafficByChannel")}
7575
+ data={data}
7576
+ categoryKey="day"
7577
+ series={[
7578
+ { dataKey: "organic", label: t("channel.organic") },
7579
+ { dataKey: "paid", label: t("channel.paid") },
7580
+ ]}
7581
+ stacked
7582
+ />`,
7583
+ storyPath: "charts/AreaChart.stories.tsx",
7584
+ rules: []
7585
+ },
7586
+ {
7587
+ name: "PieChart",
7588
+ group: "data-display",
7589
+ importPath: "@godxjp/ui/charts",
7590
+ tagline: "Part-to-whole composition across a small set of slices \u2014 `donut` option, localized tooltips, and a screen-reader breakdown of every slice. Data-visualization graph / diagram.",
7591
+ props: [
7592
+ {
7593
+ name: "data",
7594
+ type: "ChartDatum[]",
7595
+ required: true,
7596
+ description: "Row data: one slice per row."
7597
+ },
7598
+ {
7599
+ name: "dataKey",
7600
+ type: "string",
7601
+ required: true,
7602
+ description: "Key into each datum holding the slice's numeric value."
7603
+ },
7604
+ {
7605
+ name: "nameKey",
7606
+ type: "string",
7607
+ required: true,
7608
+ description: "Key into each datum holding the slice's category label."
7609
+ },
7610
+ {
7611
+ name: "label",
7612
+ type: "string",
7613
+ required: true,
7614
+ description: "Accessible name + visible caption."
7615
+ },
7616
+ {
7617
+ name: "colors",
7618
+ type: "string[]",
7619
+ description: "Per-slice colours by index (defaults to the --chart-1..6 palette)."
7620
+ },
7621
+ {
7622
+ name: "description",
7623
+ type: "string",
7624
+ description: "Extra context appended to the screen-reader description."
7625
+ },
7626
+ {
7627
+ name: "size",
7628
+ type: '"xs" | "sm" | "md" | "lg"',
7629
+ defaultValue: '"md"',
7630
+ description: "Canvas height preset. Ignored when `height` is set."
7631
+ },
7632
+ {
7633
+ name: "height",
7634
+ type: "number",
7635
+ description: "Explicit canvas height in px (overrides `size`)."
7636
+ },
7637
+ {
7638
+ name: "showLegend",
7639
+ type: "boolean",
7640
+ defaultValue: "true",
7641
+ description: "Show the slice legend."
7642
+ },
7643
+ {
7644
+ name: "numberFormat",
7645
+ type: "Intl.NumberFormatOptions",
7646
+ description: "Locale-aware formatting for tooltip values."
7647
+ },
7648
+ {
7649
+ name: "donut",
7650
+ type: "boolean",
7651
+ defaultValue: "false",
7652
+ description: "Render a donut (hollow centre) instead of a full pie."
7653
+ },
7654
+ { name: "emptyMessage", type: "string", description: "Message shown when `data` is empty." }
7655
+ ],
7656
+ usage: [
7657
+ 'DO import from the charts entry: `import { PieChart } from "@godxjp/ui/charts";` (recharts optional peer required).',
7658
+ "DO keep slices few (\u22482\u20136) \u2014 pies are unreadable past a handful; use BarChart for many categories.",
7659
+ "DO pass `numberFormat` (e.g. percent or currency) so tooltip values localize.",
7660
+ "DON'T use a pie for trends over time (LineChart/AreaChart) or precise comparison (BarChart)."
7661
+ ],
7662
+ useCases: [
7663
+ "Budget / expense split across a few categories.",
7664
+ "Market or status share (e.g. paid vs overdue vs draft)."
7665
+ ],
7666
+ related: [
7667
+ "BarChart \u2014 when there are many categories or precise comparison matters.",
7668
+ "Progress \u2014 single ratio against a target rather than a multi-slice split."
7669
+ ],
7670
+ example: `import { PieChart } from "@godxjp/ui/charts";
7671
+
7672
+ <PieChart
7673
+ label={t("dashboard.expenseSplit")}
7674
+ data={data}
7675
+ dataKey="amount"
7676
+ nameKey="category"
7677
+ numberFormat={{ style: "currency", currency: "JPY" }}
7678
+ donut
7679
+ />`,
7680
+ storyPath: "charts/PieChart.stories.tsx",
7681
+ rules: []
7682
+ }
7683
+ ];
7684
+ function findComponent(name) {
7685
+ const normalized = name.trim().toLowerCase();
7686
+ return COMPONENTS.find((c) => c.name.toLowerCase() === normalized);
7687
+ }
7688
+ function componentsByGroup(group) {
7689
+ return COMPONENTS.filter((c) => c.group === group);
7690
+ }
7691
+
7692
+ // src/data/prop-vocabulary.ts
7693
+ var PROP_VOCABULARY = [
7694
+ {
7695
+ name: "ValueProp<T = string>",
7696
+ concept: "Abstract controlled value.",
7697
+ values: ["generic"],
7698
+ usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Tabs", "SearchSelect"]
7699
+ },
7700
+ {
7701
+ name: "DefaultValueProp<T = string>",
7702
+ concept: "Abstract uncontrolled initial value.",
7703
+ values: ["generic"],
7704
+ usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Tabs"]
7705
+ },
7706
+ {
7707
+ name: "OnValueChangeProp<T = string>",
7708
+ concept: "Callback for abstract value changes. DOM events continue to use onChange.",
7709
+ values: ["(value: T) => void"],
7710
+ usedBy: ["CheckboxGroup", "Upload", "Cascader", "TreeSelect", "Transfer", "settings pickers"]
7711
+ },
7712
+ {
7713
+ name: "OpenProp / DefaultOpenProp / OnOpenChangeProp",
7714
+ concept: "Disclosure state.",
7715
+ values: ["boolean", "(open: boolean) => void"],
7716
+ usedBy: ["Dialog", "Sheet", "Popover"]
7717
+ },
7718
+ {
7719
+ name: "SizeProp",
7720
+ concept: "Shared public size names.",
7721
+ values: ["xs", "sm", "md", "lg"],
7722
+ usedBy: ["Button", "Steps", "Switch"],
7723
+ notes: "Component-specific subsets must be documented. Old alias small is sm."
7724
+ },
7725
+ {
7726
+ name: "ToneProp",
7727
+ concept: "Semantic status/color intent.",
7728
+ values: ["default", "success", "warning", "destructive", "info", "muted", "neutral"],
7729
+ usedBy: ["Badge", "Alert"],
7730
+ notes: "Status values belong in tone, not variant."
7731
+ },
7732
+ {
7733
+ name: "GapProp",
7734
+ concept: "Shared layout gap scale.",
7735
+ values: ["xs", "sm", "md", "lg", "xl"],
7246
7736
  usedBy: ["Stack", "Inline"],
7247
7737
  notes: "Inline uses an Exclude<GapProp, 'xl'> subset."
7248
7738
  },
@@ -7278,7 +7768,7 @@ var TOKENS = [
7278
7768
  name: "--chart-1..6",
7279
7769
  category: "primitive",
7280
7770
  tier: "primitive",
7281
- role: "Neutral decorative chart primitives; @theme chart colors reference these tokens."
7771
+ role: "Neutral decorative chart series palette. The @godxjp/ui/charts components (LineChart/BarChart/AreaChart/PieChart) read these by series index automatically \u2014 a service rethemes every chart at once by overriding --chart-1..6; per-series/per-slice overrides go through the component's series.color / colors props."
7282
7772
  },
7283
7773
  { name: "--space-0..12", category: "primitive", tier: "primitive", role: "Raw spacing scale." },
7284
7774
  {
@@ -7287,6 +7777,24 @@ var TOKENS = [
7287
7777
  tier: "primitive",
7288
7778
  role: "Raw typography scale."
7289
7779
  },
7780
+ {
7781
+ name: "--duration-{fast,base,slow}",
7782
+ category: "primitive",
7783
+ tier: "primitive",
7784
+ role: "Motion durations (150 / 250 / 500ms). Read these instead of a literal `0.5s` for enter/transition timing (rule #2). dxs-kintai keeps motion short; honour `prefers-reduced-motion` at the call site."
7785
+ },
7786
+ {
7787
+ name: "--ease-{standard,emphasized,decelerate,accelerate}",
7788
+ category: "primitive",
7789
+ tier: "primitive",
7790
+ role: "Motion easing curves. `standard` for most transitions, `emphasized` for entrances/overlays (the vaul drawer curve), `decelerate` for settling in, `accelerate` for exits. Read instead of a literal `cubic-bezier(\u2026)`."
7791
+ },
7792
+ {
7793
+ name: "--reveal-distance",
7794
+ category: "primitive",
7795
+ tier: "primitive",
7796
+ role: "Distance (10px) a revealed element travels on enter (translateY/-X). Read instead of a literal `translateY(10px)` for staggered reveals."
7797
+ },
7290
7798
  { name: "--primary", category: "semantic", tier: "semantic", role: "Brand/action color role." },
7291
7799
  { name: "--success", category: "semantic", tier: "semantic", role: "Success status role." },
7292
7800
  { name: "--warning", category: "semantic", tier: "semantic", role: "Warning status role." },
@@ -7914,6 +8422,51 @@ var COMPONENT_TOKENS = [
7914
8422
  "value": "var(--space-4)",
7915
8423
  "description": "Column gap between the label and its control in horizontal/inline layout."
7916
8424
  },
8425
+ {
8426
+ "name": "--list-row-padding-y",
8427
+ "value": "var(--space-3)",
8428
+ "description": "ListRow component tokens \u2014 a single-line entity row for short lists inside a Card * (sessions / API tokens / linked accounts / passkeys \u2026). Sits in a flush CardContent; * rows separate with a quiet divider (#44 \u2014 chrome defaults to the calm semantic border)."
8429
+ },
8430
+ {
8431
+ "name": "--list-row-padding-x",
8432
+ "value": "var(--space-4)",
8433
+ "description": "ListRow component tokens \u2014 a single-line entity row for short lists inside a Card * (sessions / API tokens / linked accounts / passkeys \u2026). Sits in a flush CardContent; * rows separate with a quiet divider (#44 \u2014 chrome defaults to the calm semantic border)."
8434
+ },
8435
+ {
8436
+ "name": "--list-row-gap",
8437
+ "value": "var(--space-3)",
8438
+ "description": "ListRow component tokens \u2014 a single-line entity row for short lists inside a Card * (sessions / API tokens / linked accounts / passkeys \u2026). Sits in a flush CardContent; * rows separate with a quiet divider (#44 \u2014 chrome defaults to the calm semantic border)."
8439
+ },
8440
+ {
8441
+ "name": "--list-row-border",
8442
+ "value": "1px solid hsl(var(--border))",
8443
+ "description": "ListRow component tokens \u2014 a single-line entity row for short lists inside a Card * (sessions / API tokens / linked accounts / passkeys \u2026). Sits in a flush CardContent; * rows separate with a quiet divider (#44 \u2014 chrome defaults to the calm semantic border)."
8444
+ },
8445
+ {
8446
+ "name": "--logo-size",
8447
+ "value": "2rem",
8448
+ "description": "Logo (brand mark) component tokens. Box size and corner radius are knobs (#45) so a service can * match its own grid without forking the mark; brand fill follows --primary. The `size` prop picks * the step; a service retunes the steps globally here. (Not a --control-height tier \u2014 the mark is * decorative, not a density-aware interactive control.)"
8449
+ },
8450
+ {
8451
+ "name": "--logo-size-xs",
8452
+ "value": "1.5rem",
8453
+ "description": "md"
8454
+ },
8455
+ {
8456
+ "name": "--logo-size-sm",
8457
+ "value": "1.75rem",
8458
+ "description": "md"
8459
+ },
8460
+ {
8461
+ "name": "--logo-size-lg",
8462
+ "value": "2.5rem",
8463
+ "description": "md"
8464
+ },
8465
+ {
8466
+ "name": "--logo-radius",
8467
+ "value": "var(--radius)",
8468
+ "description": "md"
8469
+ },
7917
8470
  {
7918
8471
  "name": "--pagination-gap",
7919
8472
  "value": "var(--space-inline-sm)",
@@ -10018,6 +10571,21 @@ categories. Reads as chaos; eye can't anchor.`,
10018
10571
  "solid" vs "outline") for variety within the same hue. Reserve
10019
10572
  non-neutral colors (success / warning / destructive) for tags
10020
10573
  that genuinely carry that meaning.`
10574
+ },
10575
+ {
10576
+ category: "visual",
10577
+ name: "Oversaturated brand accent",
10578
+ body: `Full-bleed, fully-saturated brand color on buttons, banners and
10579
+ notification bars (the classic loud Slack/Linear/Notion blue) \u2014 the
10580
+ accent SCREAMS instead of signalling. A bright primary CTA bar across
10581
+ the whole width, vivid send buttons, neon success. It reads as a
10582
+ verbatim copy of a SaaS chrome, not a restrained product surface.`,
10583
+ fix: `\u6E0B\u307F (restraint): keep --primary chroma \u2264 0.18 \u2014 desaturate so the
10584
+ accent BLENDS with the warm-neutral spine and is used sparingly for
10585
+ the ONE primary action. Don't paint a full-width bar in raw blue;
10586
+ prefer a quiet Alert (icon + text + a normal-width action). Never
10587
+ hard-code a vivid hex \u2014 read \`bg-primary\`/\`text-primary\` tokens, and
10588
+ let a service retheme via --primary only.`
10021
10589
  },
10022
10590
  // ── layout ─────────────────────────────────────────────────────
10023
10591
  {
@@ -10062,8 +10630,39 @@ template" \u2014 and devalues the user's time at each step.`,
10062
10630
  be a centered question, step 2 a side-by-side comparison, step 3
10063
10631
  a multi-field form, step 4 a single yes/no card. Same palette +
10064
10632
  type system for coherence; different composition for engagement.`
10633
+ },
10634
+ {
10635
+ category: "layout",
10636
+ name: "Stacked notification banner (misplaced alert controls)",
10637
+ body: `A notification / "enable X" bar where the pieces are stacked
10638
+ VERTICALLY and mis-placed: the icon floats centered ABOVE the text
10639
+ (often a SECOND redundant icon on the far left too), the primary
10640
+ action is a FULL-WIDTH colored bar UNDER the text, and the dismiss \xD7
10641
+ sits centered at the BOTTOM on its own line. It's a hand-rolled
10642
+ banner that ignores the framework's Alert anatomy.`,
10643
+ fix: `Use <Alert> and respect its fixed anatomy \u2014 ONE leading tone icon
10644
+ at the inline-start (top-aligned, auto by \`tone\`, never two), the
10645
+ text body, an <Alert.Actions> with a NORMAL-WIDTH Button in the
10646
+ trailing-right column, and \`onDismiss\` to render the \xD7 in the
10647
+ TOP-RIGHT corner. It is ONE horizontal row, never a vertical stack;
10648
+ never hand-roll the \u2715 or a full-bleed action bar.`
10065
10649
  },
10066
10650
  // ── copy ───────────────────────────────────────────────────────
10651
+ {
10652
+ category: "copy",
10653
+ name: "Emoji in product UI",
10654
+ body: `Emoji sprinkled through the product surface \u2014 \u2705 / \u{1F389} / \u{1F525} in chat
10655
+ messages, status lines, toasts, buttons, empty states or success
10656
+ banners ("All tests green \u{1F389}", "done \u2705"). It reads as casual
10657
+ consumer-app slop, breaks on Windows/Linux, and pollutes the
10658
+ accessible name. Celebrating with confetti/\u{1F389} is the same tell.`,
10659
+ fix: `No emoji anywhere in product UI. State the fact quietly in
10660
+ i18n-keyed copy ("\u627F\u8A8D\u3057\u307E\u3057\u305F" / "All checks passed"). Use a Lucide
10661
+ icon (1.5px) for an affordance and a semantic Badge \`tone\`
10662
+ (success/warning/destructive) for status \u2014 color + label carry the
10663
+ meaning, not a glyph. Country names come from \`Intl.DisplayNames\`,
10664
+ never emoji flags.`
10665
+ },
10067
10666
  {
10068
10667
  category: "copy",
10069
10668
  name: "Filler corporate phrases",
@@ -10539,6 +11138,288 @@ function checksByCategory(cat) {
10539
11138
  return REDESIGN_CHECKS.filter((c) => c.category === cat);
10540
11139
  }
10541
11140
 
11141
+ // src/data/audit-rules.ts
11142
+ var AUDIT_COMMAND = "node node_modules/@godxjp/ui/scripts/ui-audit.mjs (add --format json for machine output, --rules to print this catalog)";
11143
+ var AUDIT_RULES = [
11144
+ // ── tokens (house design-system: semantic tokens only) ──────────────────
11145
+ {
11146
+ id: "no-raw-palette-color",
11147
+ severity: "error",
11148
+ category: "tokens",
11149
+ standard: null,
11150
+ fix: "Use semantic tokens (bg-primary, text-muted-foreground), never raw palette (bg-blue-500)."
11151
+ },
11152
+ {
11153
+ id: "no-arbitrary-hex",
11154
+ severity: "error",
11155
+ category: "tokens",
11156
+ standard: null,
11157
+ fix: "No hardcoded hex in className; read design-system color tokens."
11158
+ },
11159
+ {
11160
+ id: "no-arbitrary-spacing",
11161
+ severity: "error",
11162
+ category: "tokens",
11163
+ standard: null,
11164
+ fix: "No p-[13px]/gap-[7px]; use the token scale / <Flex gap> / <PageContainer>."
11165
+ },
11166
+ {
11167
+ id: "no-arbitrary-size",
11168
+ severity: "error",
11169
+ category: "tokens",
11170
+ standard: null,
11171
+ fix: "No w-[37px]/h-[260px]; use token sizes or a sizing prop (min-w-[\u2026] allowed)."
11172
+ },
11173
+ {
11174
+ id: "no-arbitrary-typography",
11175
+ severity: "error",
11176
+ category: "tokens",
11177
+ standard: null,
11178
+ fix: "No text-[20px]/leading-[1.7]; use the golden-ratio type-scale tokens."
11179
+ },
11180
+ {
11181
+ id: "no-arbitrary-radius",
11182
+ severity: "error",
11183
+ category: "tokens",
11184
+ standard: null,
11185
+ fix: "No rounded-[6px]; use rounded-sm/md/lg radius tokens."
11186
+ },
11187
+ {
11188
+ id: "no-dark-color-override",
11189
+ severity: "warn",
11190
+ category: "tokens",
11191
+ standard: null,
11192
+ fix: "Drop dark: color overrides \u2014 semantic tokens already adapt."
11193
+ },
11194
+ {
11195
+ id: "raw-white-black",
11196
+ severity: "warn",
11197
+ category: "tokens",
11198
+ standard: null,
11199
+ fix: "Prefer semantic tokens (text-primary-foreground, bg-background) over raw white/black."
11200
+ },
11201
+ {
11202
+ id: "no-domain-tracking-token",
11203
+ severity: "error",
11204
+ category: "tokens",
11205
+ standard: null,
11206
+ fix: "No package-tracking/domain tokens; use semantic tokens or app theme overrides."
11207
+ },
11208
+ {
11209
+ id: "no-space-xy",
11210
+ severity: "error",
11211
+ category: "tokens",
11212
+ standard: null,
11213
+ fix: "Use <Flex gap> instead of space-x/y-*."
11214
+ },
11215
+ // ── composition (real primitives, no raw HTML / hand-rolled) ─────────────
11216
+ {
11217
+ id: "no-raw-select",
11218
+ severity: "error",
11219
+ category: "composition",
11220
+ standard: "HTML Living Standard (WHATWG)",
11221
+ fix: "Use <Select> from @godxjp/ui, not a raw <select>."
11222
+ },
11223
+ {
11224
+ id: "no-raw-table",
11225
+ severity: "error",
11226
+ category: "composition",
11227
+ standard: "HTML Living Standard (WHATWG)",
11228
+ fix: "Use the <Table>/<DataTable> family, not a raw <table>."
11229
+ },
11230
+ {
11231
+ id: "no-raw-input",
11232
+ severity: "error",
11233
+ category: "composition",
11234
+ standard: "HTML Living Standard (WHATWG)",
11235
+ fix: "Use <Input> from @godxjp/ui, not a raw <input>."
11236
+ },
11237
+ {
11238
+ id: "no-raw-textarea",
11239
+ severity: "warn",
11240
+ category: "composition",
11241
+ standard: "HTML Living Standard (WHATWG)",
11242
+ fix: "Use <Textarea> from @godxjp/ui, not a raw <textarea>."
11243
+ },
11244
+ {
11245
+ id: "no-raw-button",
11246
+ severity: "error",
11247
+ category: "composition",
11248
+ standard: "HTML Living Standard (WHATWG)",
11249
+ fix: "Use <Button> from @godxjp/ui, not a raw <button>."
11250
+ },
11251
+ {
11252
+ id: "card-manual-padding",
11253
+ severity: "error",
11254
+ category: "composition",
11255
+ standard: null,
11256
+ fix: "Wrap the body in <CardContent>; don't hand-roll padding on <Card>."
11257
+ },
11258
+ {
11259
+ id: "card-needs-content",
11260
+ severity: "error",
11261
+ category: "composition",
11262
+ standard: null,
11263
+ fix: "<Card> body must be in <CardContent> (no padding otherwise); flush only for a full-bleed table."
11264
+ },
11265
+ {
11266
+ id: "bare-control-needs-formfield",
11267
+ severity: "warn",
11268
+ category: "composition",
11269
+ standard: "WCAG 2.2 SC 1.3.1 \xB7 3.3.2 \xB7 @godxjp/ui FormField (cardinal rule 227)",
11270
+ fix: "Wrap a labelled control in <FormField label=\u2026> \u2014 it owns label\u2194control id wiring, aria/error, AND the field rhythm; never pair a bare <Label> with an <Input>."
11271
+ },
11272
+ {
11273
+ id: "manual-field-error",
11274
+ severity: "warn",
11275
+ category: "composition",
11276
+ standard: "WCAG 2.2 SC 3.3.1",
11277
+ fix: "Use <FormField error=\u2026>, not a hand-rolled <p class='text-destructive'>."
11278
+ },
11279
+ {
11280
+ id: "manual-field-helper",
11281
+ severity: "warn",
11282
+ category: "composition",
11283
+ standard: null,
11284
+ fix: "Use <FormField helper=\u2026>, not a hand-rolled helper <p>."
11285
+ },
11286
+ // ── api (controlled-vocabulary props) ────────────────────────────────────
11287
+ {
11288
+ id: "status-tone-not-variant",
11289
+ severity: "error",
11290
+ category: "api",
11291
+ standard: null,
11292
+ fix: "Badge/Tag/StatCard status uses tone, not variant (variant is structural)."
11293
+ },
11294
+ {
11295
+ id: "value-callback-on-value-change",
11296
+ severity: "error",
11297
+ category: "api",
11298
+ standard: null,
11299
+ fix: "Abstract value components use onValueChange, not onChange."
11300
+ },
11301
+ // ── a11y (WCAG 2.2 + WAI-ARIA APG) ───────────────────────────────────────
11302
+ {
11303
+ id: "icon-button-needs-name",
11304
+ severity: "warn",
11305
+ category: "a11y",
11306
+ standard: "WCAG 2.2 SC 4.1.2 \xB7 1.1.1 \xB7 WAI-ARIA 1.2",
11307
+ fix: "Add aria-label={t('\u2026')} to <Button size='icon'>; the glyph is aria-hidden."
11308
+ },
11309
+ {
11310
+ id: "img-needs-alt",
11311
+ severity: "warn",
11312
+ category: "a11y",
11313
+ standard: "WCAG 2.2 SC 1.1.1 \xB7 HTML Living Standard",
11314
+ fix: "Add alt to every <img> (alt='' if decorative); prefer <Avatar>/<AspectRatio>."
11315
+ },
11316
+ {
11317
+ id: "no-positive-tabindex",
11318
+ severity: "warn",
11319
+ category: "a11y",
11320
+ standard: "WCAG 2.2 SC 2.4.3 \xB7 WAI-ARIA APG",
11321
+ fix: "Use tabIndex 0 or -1 only; never positive \u2014 it breaks focus order."
11322
+ },
11323
+ {
11324
+ id: "hand-rolled-close-glyph",
11325
+ severity: "warn",
11326
+ category: "a11y",
11327
+ standard: "WAI-ARIA 1.2 (dialog) \xB7 WCAG 2.2 SC 4.1.2",
11328
+ fix: "Pass onDismiss to <Alert>, or use <Dialog>/<Sheet>'s built-in labelled close \u2014 not a bare \u2715."
11329
+ },
11330
+ // ── i18n (ECMA-402 Intl + ISO + IANA) ────────────────────────────────────
11331
+ {
11332
+ id: "no-emoji-in-ui",
11333
+ severity: "warn",
11334
+ category: "i18n",
11335
+ standard: "Unicode UTS #51 \xB7 WCAG 2.2 SC 1.1.1",
11336
+ fix: "No emoji in product UI; quiet i18n copy + Lucide icon + Badge tone."
11337
+ },
11338
+ {
11339
+ id: "no-emoji-flag",
11340
+ severity: "warn",
11341
+ category: "i18n",
11342
+ standard: "ISO 3166-1 \xB7 ECMA-402 Intl.DisplayNames \xB7 Unicode UTS #51",
11343
+ fix: "Derive country names from Intl.DisplayNames; no emoji flags."
11344
+ },
11345
+ {
11346
+ id: "hardcoded-currency",
11347
+ severity: "warn",
11348
+ category: "i18n",
11349
+ standard: "ISO 4217 \xB7 ECMA-402 Intl.NumberFormat",
11350
+ fix: "Format money with Intl.NumberFormat({ style: 'currency', currency }), not \xA5{amount}."
11351
+ },
11352
+ {
11353
+ id: "raw-intl-date",
11354
+ severity: "warn",
11355
+ category: "i18n",
11356
+ standard: "ISO 8601 \xB7 IANA tz \xB7 ECMA-402 Intl.DateTimeFormat",
11357
+ fix: "Use formatDate from @godxjp/ui/datetime, not hand-built or locale-default dates."
11358
+ },
11359
+ // ── rtl (CSS Logical Properties) ─────────────────────────────────────────
11360
+ {
11361
+ id: "no-physical-direction",
11362
+ severity: "warn",
11363
+ category: "rtl",
11364
+ standard: "W3C CSS Logical Properties L1 \xB7 WCAG 2.2 (1.3.2)",
11365
+ fix: "Use logical utilities (ms-/me-/ps-/pe-, start-/end-, text-start/end, border-s/e, rounded-s/e)."
11366
+ },
11367
+ // ── copy (dxs-kintai DNA) ────────────────────────────────────────────────
11368
+ {
11369
+ id: "no-em-dash-in-copy",
11370
+ severity: "warn",
11371
+ category: "copy",
11372
+ standard: "@godxjp/ui dxs-kintai typography",
11373
+ fix: "No em-dash (\u2014) in copy; use a middot \xB7 or two calm sentences."
11374
+ }
11375
+ ];
11376
+ function auditRulesByCategory(category) {
11377
+ return category ? AUDIT_RULES.filter((r) => r.category === category) : AUDIT_RULES;
11378
+ }
11379
+
11380
+ // src/data/visual-rules.ts
11381
+ var VISUAL_AUDIT_COMMAND = "node node_modules/@godxjp/ui/scripts/visual-audit.mjs <baseUrl> [route \u2026] (needs optional peers: playwright + @axe-core/playwright + a chromium; --strict for a CI gate, --format json, --rules to print this catalog)";
11382
+ var VISUAL_RULES = [
11383
+ {
11384
+ id: "axe-violations",
11385
+ severity: "warn",
11386
+ category: "a11y",
11387
+ standard: "WCAG 2.2 A/AA \xB7 WAI-ARIA 1.2 (axe-core engine)",
11388
+ fix: "Fix each axe node \u2014 contrast (1.4.3), name/role/value (4.1.2), ARIA, landmarks. Runs on the REAL DOM, catching what static analysis cannot."
11389
+ },
11390
+ {
11391
+ id: "target-size-min",
11392
+ severity: "warn",
11393
+ category: "a11y",
11394
+ standard: "WCAG 2.2 SC 2.5.8 (24\xD724 AA) \xB7 2.5.5 (44\xD744 AAA)",
11395
+ fix: "Interactive targets must be \u226524\xD724 CSS px; size from the --control-height tier."
11396
+ },
11397
+ {
11398
+ id: "oversaturated-accent",
11399
+ severity: "warn",
11400
+ category: "color",
11401
+ standard: "@godxjp/ui dxs-kintai \u6E0B\u307F (OKLCH chroma \u2264 0.18)",
11402
+ fix: "Desaturate brand/primary surfaces (OKLCH chroma \u2264 0.18); read --primary tokens, no raw vivid bars."
11403
+ },
11404
+ {
11405
+ id: "emoji-rendered",
11406
+ severity: "warn",
11407
+ category: "i18n",
11408
+ standard: "Unicode UTS #51 \xB7 WCAG 2.2 SC 1.1.1",
11409
+ fix: "Remove emoji from rendered product text; quiet i18n copy + Lucide icon + Badge tone."
11410
+ },
11411
+ {
11412
+ id: "alert-controls-misplaced",
11413
+ severity: "warn",
11414
+ category: "layout",
11415
+ standard: "@godxjp/ui Alert anatomy \xB7 WAI-ARIA 1.2 \xB7 WCAG 2.2 SC 4.1.2",
11416
+ fix: "Use <Alert>: one leading tone icon, <Alert.Actions> trailing-right normal width, onDismiss \xD7 top-right, one horizontal row."
11417
+ }
11418
+ ];
11419
+ function visualRulesByCategory(category) {
11420
+ return category ? VISUAL_RULES.filter((r) => r.category === category) : VISUAL_RULES;
11421
+ }
11422
+
10542
11423
  // src/tools/registry.ts
10543
11424
  var TOOL_DEFINITIONS = [
10544
11425
  // ── DISCOVERY (small responses) ────────────────────────────────
@@ -10611,6 +11492,29 @@ var TOOL_DEFINITIONS = [
10611
11492
  }
10612
11493
  }
10613
11494
  },
11495
+ {
11496
+ name: "list_audit_rules",
11497
+ description: "List the LOCAL ui-audit rules (scripts/ui-audit.mjs) an agent should run BEFORE any visual review. Each rule cites the international standard it enforces (WCAG 2.2 / WAI-ARIA / ECMA-402 Intl / ISO 4217\xB73166\xB78601 / IANA / CSS Logical Properties / HTML LS) + a concrete fix. Optionally filter by category. Includes the run command. ~3KB.",
11498
+ inputSchema: {
11499
+ type: "object",
11500
+ properties: {
11501
+ category: {
11502
+ type: "string",
11503
+ enum: ["tokens", "composition", "api", "a11y", "i18n", "rtl", "copy"]
11504
+ }
11505
+ }
11506
+ }
11507
+ },
11508
+ {
11509
+ name: "list_visual_checks",
11510
+ description: "List the RUNTIME visual-audit checks (scripts/visual-audit.mjs \u2014 Playwright + axe-core) to run against the RUNNING app BEFORE a visual review. Catches what source/static checks can't: colour contrast + ARIA (axe), target size (WCAG 2.5.8), OKLCH chroma of rendered accents (\u6E0B\u307F), emoji that reached the DOM, and a mis-laid-out notification banner. Separate from list_audit_rules (static, zero-dep) because it needs a browser. ~2KB.",
11511
+ inputSchema: {
11512
+ type: "object",
11513
+ properties: {
11514
+ category: { type: "string", enum: ["a11y", "color", "i18n", "layout"] }
11515
+ }
11516
+ }
11517
+ },
10614
11518
  // ── DRILL-DOWN (medium responses) ──────────────────────────────
10615
11519
  {
10616
11520
  name: "get_anti_ai_tell",
@@ -10806,6 +11710,10 @@ async function dispatchTool(name, args) {
10806
11710
  return listAntiAiTells(args.category);
10807
11711
  case "list_redesign_checks":
10808
11712
  return listRedesignChecks(args.category);
11713
+ case "list_audit_rules":
11714
+ return listAuditRules(args.category);
11715
+ case "list_visual_checks":
11716
+ return listVisualChecks(args.category);
10809
11717
  case "get_anti_ai_tell":
10810
11718
  return getAntiAiTell(String(args.name ?? ""));
10811
11719
  case "get_redesign_check":
@@ -11021,6 +11929,56 @@ function listPatterns() {
11021
11929
  }
11022
11930
  return out;
11023
11931
  }
11932
+ function listVisualChecks(cat) {
11933
+ const list = visualRulesByCategory(cat);
11934
+ let out = `# Runtime visual-audit checks${cat ? ` \u2014 ${cat}` : ""} (${list.length})
11935
+
11936
+ `;
11937
+ out += `_Run against the RUNNING app BEFORE a visual review (warnings, non-blocking). Needs a browser._
11938
+ `;
11939
+ out += `\`\`\`
11940
+ ${VISUAL_AUDIT_COMMAND}
11941
+ \`\`\`
11942
+
11943
+ `;
11944
+ out += `_Static \`list_audit_rules\` first (cheap, every save) \u2192 THIS (before review) \u2192 human eyes for taste._
11945
+
11946
+ `;
11947
+ for (const r of list) {
11948
+ out += `- **${r.id}** (${r.severity}, ${r.category}) \u2014 _${r.standard}_
11949
+ ${r.fix}
11950
+ `;
11951
+ }
11952
+ return out;
11953
+ }
11954
+ function listAuditRules(cat) {
11955
+ const list = auditRulesByCategory(cat);
11956
+ let out = `# Local UI-audit rules${cat ? ` \u2014 ${cat}` : ""} (${list.length})
11957
+
11958
+ `;
11959
+ out += `_Run BEFORE any visual review (warnings are agent guidance, non-blocking):_
11960
+ `;
11961
+ out += `\`\`\`
11962
+ ${AUDIT_COMMAND}
11963
+ \`\`\`
11964
+
11965
+ `;
11966
+ const grouped = list.reduce((acc, r) => {
11967
+ (acc[r.category] ??= []).push(r);
11968
+ return acc;
11969
+ }, {});
11970
+ for (const [c, items] of Object.entries(grouped)) {
11971
+ out += `## ${c}
11972
+ `;
11973
+ for (const r of items) {
11974
+ out += `- **${r.id}** (${r.severity})${r.standard ? ` \u2014 _${r.standard}_` : ""}
11975
+ ${r.fix}
11976
+ `;
11977
+ }
11978
+ out += "\n";
11979
+ }
11980
+ return out;
11981
+ }
11024
11982
  function listAntiAiTells(cat) {
11025
11983
  const list = cat ? aiTellsByCategory(cat) : ANTI_AI_TELLS;
11026
11984
  let out = `# AI tells to AVOID${cat ? ` \u2014 ${cat}` : ""} (${list.length})
@@ -11204,9 +12162,7 @@ function componentTokensFor(name) {
11204
12162
  const prefixes = TOKEN_PREFIXES[name] ?? [
11205
12163
  name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
11206
12164
  ];
11207
- return COMPONENT_TOKENS.filter(
11208
- (t) => prefixes.some((p) => t.name.startsWith(`--${p}-`))
11209
- );
12165
+ return COMPONENT_TOKENS.filter((t) => prefixes.some((p) => t.name.startsWith(`--${p}-`)));
11210
12166
  }
11211
12167
  function getComponent(name) {
11212
12168
  const c = findComponent(name);
@@ -11718,7 +12674,7 @@ ${c.example}
11718
12674
  // package.json
11719
12675
  var package_default = {
11720
12676
  name: "@godxjp/ui-mcp",
11721
- version: "13.17.4",
12677
+ version: "14.0.1",
11722
12678
  description: "Model Context Protocol server for @godxjp/ui \u2014 gives Claude Code / Codex CLI / Cursor / any MCP-aware agent live access to the component catalog, prop vocabulary, design tokens, 45 cardinal rules, copy-paste-ready patterns, 12 design / taste skills synthesised from Leonxlnx/taste-skill, 20+ anti-AI-tell patterns, and a 50-check redesign audit \u2014 token-efficient (list \u2192 drill-down).",
11723
12679
  type: "module",
11724
12680
  main: "./dist/index.js",