@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/README.md +10 -8
- package/dist/index.js +1145 -189
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
494
|
+
name: "center",
|
|
507
495
|
type: "ReactNode",
|
|
508
496
|
defaultValue: "undefined",
|
|
509
|
-
description: "
|
|
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: "
|
|
500
|
+
name: "end",
|
|
555
501
|
type: "ReactNode",
|
|
556
502
|
defaultValue: "undefined",
|
|
557
|
-
description:
|
|
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: "
|
|
567
|
-
type: "
|
|
506
|
+
name: "children",
|
|
507
|
+
type: "ReactNode",
|
|
568
508
|
defaultValue: "undefined",
|
|
569
|
-
description: "
|
|
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: "
|
|
573
|
-
type: "
|
|
574
|
-
|
|
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
|
|
580
|
-
|
|
581
|
-
"DO
|
|
582
|
-
"
|
|
583
|
-
"DO
|
|
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
|
|
588
|
-
"
|
|
589
|
-
"
|
|
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
|
|
596
|
-
"
|
|
597
|
-
"
|
|
598
|
-
"
|
|
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 {
|
|
602
|
-
import {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
640
|
-
|
|
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: "
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
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: "
|
|
7244
|
-
|
|
7245
|
-
|
|
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
|
|
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: "
|
|
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",
|