@godxjp/ui 26.0.0 → 26.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,10 @@ export type SelectCompoundProp = FieldA11yProps & {
26
26
  form?: string;
27
27
  children?: React.ReactNode;
28
28
  };
29
- export type SelectProp = SelectDataProp | SelectCompoundProp;
29
+ export type SelectProp = SelectDataProp | (SelectCompoundProp & {
30
+ mode?: undefined;
31
+ labelInValue?: undefined;
32
+ });
30
33
  /**
31
34
  * Select — one component for every single-select. Use the compound API for full control
32
35
  * (`<Select><SelectTrigger/><SelectContent><SelectItem/></SelectContent></Select>`), OR the
@@ -33,6 +33,12 @@ function useFooterReveal(enabled) {
33
33
  function PageContainerInset({ className, children, ...props }) {
34
34
  return /* @__PURE__ */ jsx("div", { className: cn("ui-page-container-inset", className), ...props, children });
35
35
  }
36
+ function footerPadStyle(footerPad) {
37
+ const style = padStyle(footerPad, void 0);
38
+ const end = style?.paddingInlineEnd ?? style?.padding;
39
+ if (end === void 0) return style;
40
+ return { ...style, paddingInlineEnd: `calc(${end} + var(--page-footer-content-slack))` };
41
+ }
36
42
  function PageContainerRoot({
37
43
  title,
38
44
  subtitle,
@@ -150,7 +156,7 @@ function PageContainerRoot({
150
156
  ),
151
157
  toolbar != null && /* @__PURE__ */ jsx("div", { className: "ui-page-toolbar", style: padStyle(toolbarPad, void 0), children: toolbar }),
152
158
  children != null && /* @__PURE__ */ jsx("div", { className: "ui-page-body", children }),
153
- footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", style: padStyle(footerPad, void 0), children: footer })
159
+ footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", style: footerPadStyle(footerPad), children: footer })
154
160
  ]
155
161
  }
156
162
  );
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "26.0.0",
3
+ "version": "26.2.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -1119,7 +1119,7 @@
1119
1119
  }
1120
1120
 
1121
1121
  .ui-segmented-item:not([data-state="checked"]):not([data-disabled]):hover {
1122
- background: hsl(var(--segmented-item-hover-background));
1122
+ background: hsl(var(--segmented-item-hover-background, var(--accent)));
1123
1123
  color: hsl(var(--segmented-item-hover-color));
1124
1124
  }
1125
1125
  .ui-segmented-item:not([data-state="checked"]):not([data-disabled]):active {
@@ -2722,7 +2722,7 @@
2722
2722
  }
2723
2723
 
2724
2724
  .ui-control--filled:hover:not(:disabled):not([readonly]):not([data-disabled]) {
2725
- background-color: hsl(var(--control-variant-filled-hover-background));
2725
+ background-color: hsl(var(--control-variant-filled-hover-background, var(--accent)));
2726
2726
  }
2727
2727
 
2728
2728
  .ui-control--borderless {
@@ -2939,7 +2939,7 @@
2939
2939
  }
2940
2940
 
2941
2941
  .ui-slider-dot[data-active="true"] {
2942
- border-color: hsl(var(--slider-dot-active-border-color));
2942
+ border-color: hsl(var(--slider-dot-active-border-color, var(--primary)));
2943
2943
  }
2944
2944
 
2945
2945
  .ui-slider-tooltip {
@@ -3053,14 +3053,14 @@
3053
3053
  }
3054
3054
 
3055
3055
  .ui-radio-button[data-state="checked"] {
3056
- color: hsl(var(--choice-button-selected-color));
3057
- border-color: hsl(var(--choice-button-selected-border-color));
3056
+ color: hsl(var(--choice-button-selected-color, var(--primary)));
3057
+ border-color: hsl(var(--choice-button-selected-border-color, var(--primary)));
3058
3058
  }
3059
3059
 
3060
3060
  .ui-radio-button-bar[data-button-style="solid"] .ui-radio-button[data-state="checked"] {
3061
- color: hsl(var(--choice-button-solid-color));
3062
- background: hsl(var(--choice-button-solid-background));
3063
- border-color: hsl(var(--choice-button-solid-background));
3061
+ color: hsl(var(--choice-button-solid-color, var(--primary-foreground)));
3062
+ background: hsl(var(--choice-button-solid-background, var(--primary)));
3063
+ border-color: hsl(var(--choice-button-solid-background, var(--primary)));
3064
3064
  }
3065
3065
 
3066
3066
  .ui-radio-button[data-disabled] {
@@ -143,7 +143,7 @@
143
143
  content: "";
144
144
  inset: 0;
145
145
  background: conic-gradient(
146
- var(--float-button-progress-color) var(--float-button-progress-offset),
146
+ var(--float-button-progress-color, hsl(var(--primary))) var(--float-button-progress-offset),
147
147
  var(--float-button-progress-track-color) var(--float-button-progress-offset)
148
148
  );
149
149
  mask: radial-gradient(
@@ -53,7 +53,7 @@
53
53
  .ui-otp-slot[data-active="true"],
54
54
  .ui-otp-group[data-appearance="grouped"]:has(.ui-otp-slot[data-active="true"]) {
55
55
  outline: var(--focus-ring-width) solid
56
- hsl(var(--focus-outline-color) / var(--focus-ring-opacity, 1));
56
+ hsl(var(--focus-outline-color, var(--focus-ring-color, var(--ring))) / var(--focus-ring-opacity, 1));
57
57
  outline-offset: var(--focus-ring-offset);
58
58
 
59
59
  transition:
@@ -1,5 +1,11 @@
1
1
 
2
2
 
3
+ @property --page-footer-content-slack {
4
+ syntax: "<length-percentage>";
5
+ inherits: false;
6
+ initial-value: 0px;
7
+ }
8
+
3
9
  @layer components {
4
10
 
5
11
  .ui-separator {
@@ -90,7 +96,14 @@
90
96
  }
91
97
 
92
98
  .ui-brand-glow {
93
- background-image: var(--brand-glow);
99
+ background-image: var(
100
+ --brand-glow,
101
+ radial-gradient(
102
+ ellipse var(--brand-glow-size) at var(--brand-glow-position),
103
+ hsl(var(--brand-glow-color, var(--primary)) / var(--brand-glow-alpha)),
104
+ transparent 70%
105
+ )
106
+ );
94
107
  pointer-events: none;
95
108
  }
96
109
 
@@ -616,6 +629,14 @@
616
629
  max-inline-size: var(--page-measure-medium);
617
630
  }
618
631
 
632
+ .ui-page-container[data-measure="narrow"] .ui-page-footer {
633
+ --page-footer-content-slack: max(0px, 100% - var(--page-measure-narrow));
634
+ }
635
+
636
+ .ui-page-container[data-measure="medium"] .ui-page-footer {
637
+ --page-footer-content-slack: max(0px, 100% - var(--page-measure-medium));
638
+ }
639
+
619
640
  .ui-page-container--flush .ui-page-toolbar,
620
641
  .ui-page-container--flush .ui-page-body {
621
642
  padding-inline-start: 0;
@@ -867,6 +888,7 @@
867
888
  align-items: stretch;
868
889
  gap: var(--space-inline-sm);
869
890
  padding-top: var(--space-stack-md);
891
+ padding-inline-end: calc(var(--space-page-active-x) + var(--page-footer-content-slack));
870
892
  border-top: var(--page-footer-divider, 1px solid hsl(var(--border)));
871
893
  }
872
894
 
@@ -291,6 +291,10 @@
291
291
  max-inline-size: var(--app-shell-page-max-width);
292
292
  }
293
293
 
294
+ .app-main :where(.ui-page-footer) {
295
+ --page-footer-content-slack: max(0px, 100% - var(--app-shell-page-max-width));
296
+ }
297
+
294
298
  .ui-auth-shell {
295
299
  --control-height: var(--auth-shell-control-height);
296
300
  --heading-h1: var(--auth-shell-heading-size);
@@ -1430,8 +1434,8 @@
1430
1434
  }
1431
1435
 
1432
1436
  .ui-app-launcher-tile:hover {
1433
- background: hsl(var(--app-launcher-tile-hover-background));
1434
- color: hsl(var(--app-launcher-tile-hover-color));
1437
+ background: hsl(var(--app-launcher-tile-hover-background, var(--accent)));
1438
+ color: hsl(var(--app-launcher-tile-hover-color, var(--accent-foreground)));
1435
1439
  }
1436
1440
 
1437
1441
  .ui-app-launcher-tile[aria-current="page"] {
@@ -2036,14 +2040,14 @@
2036
2040
  }
2037
2041
 
2038
2042
  .ui-topbar-item:hover {
2039
- background: hsl(var(--topbar-item-hover-background));
2040
- color: hsl(var(--topbar-item-hover-color));
2043
+ background: hsl(var(--topbar-item-hover-background, var(--accent)));
2044
+ color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
2041
2045
  }
2042
2046
 
2043
2047
  .ui-topbar-item:active,
2044
2048
  .ui-topbar-item[data-state="open"] {
2045
2049
  background: hsl(var(--topbar-item-active-background));
2046
- color: hsl(var(--topbar-item-hover-color));
2050
+ color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
2047
2051
  }
2048
2052
 
2049
2053
  .ui-topbar-item:disabled,
@@ -35,7 +35,7 @@
35
35
  --number-input-touch-height: var(--band-height-2xl);
36
36
 
37
37
  --control-variant-filled-background: var(--muted);
38
- --control-variant-filled-hover-background: var(--accent);
38
+ --control-variant-filled-hover-background: initial;
39
39
 
40
40
  --control-count-font-size: var(--font-size-sm);
41
41
  --control-count-color: var(--muted-foreground);
@@ -58,7 +58,7 @@
58
58
  --slider-dot-size: var(--slider-track-height);
59
59
  --slider-dot-background: var(--background);
60
60
  --slider-dot-border-color: var(--border);
61
- --slider-dot-active-border-color: var(--primary);
61
+ --slider-dot-active-border-color: initial;
62
62
  --slider-tooltip-background: var(--popover);
63
63
  --slider-tooltip-color: var(--popover-foreground);
64
64
  --slider-tooltip-font-size: var(--font-size-xs);
@@ -69,10 +69,11 @@
69
69
  --choice-button-background: var(--background);
70
70
  --choice-button-color: var(--foreground);
71
71
  --choice-button-border-color: var(--input);
72
- --choice-button-selected-color: var(--primary);
73
- --choice-button-selected-border-color: var(--primary);
74
- --choice-button-solid-background: var(--primary);
75
- --choice-button-solid-color: var(--primary-foreground);
72
+
73
+ --choice-button-selected-color: initial;
74
+ --choice-button-selected-border-color: initial;
75
+ --choice-button-solid-background: initial;
76
+ --choice-button-solid-color: initial;
76
77
 
77
78
  --toggle-focus-ring-width: var(--stroke-lg);
78
79
  --toggle-focus-ring-alpha: 1;
@@ -32,7 +32,7 @@
32
32
 
33
33
  --float-button-progress-offset: 0turn;
34
34
  --float-button-progress-width: var(--space-1);
35
- --float-button-progress-color: hsl(var(--primary));
35
+ --float-button-progress-color: initial;
36
36
  --float-button-progress-track-color: hsl(var(--border));
37
37
 
38
38
  --float-button-motion-translate: var(--float-button-size);
@@ -17,7 +17,7 @@
17
17
  --segmented-item-hover-color: var(--foreground);
18
18
  --segmented-item-selected-color: var(--foreground);
19
19
 
20
- --segmented-item-hover-background: var(--accent);
20
+ --segmented-item-hover-background: initial;
21
21
  --segmented-item-active-background: var(--secondary);
22
22
 
23
23
  --segmented-item-selected-background: var(--background);
@@ -110,8 +110,8 @@
110
110
  --topbar-item-radius: var(--radius-sharp);
111
111
  --topbar-item-color: var(--muted-foreground);
112
112
 
113
- --topbar-item-hover-background: var(--accent);
114
- --topbar-item-hover-color: var(--accent-foreground);
113
+ --topbar-item-hover-background: initial;
114
+ --topbar-item-hover-color: initial;
115
115
  --topbar-item-active-background: var(--secondary);
116
116
 
117
117
  --topbar-item-badge-offset-block: var(--space-1);
@@ -158,8 +158,8 @@
158
158
  --app-launcher-tile-min-height: var(--control-height-comfortable);
159
159
  --app-launcher-tile-radius: var(--radius);
160
160
  --app-launcher-tile-color: var(--foreground);
161
- --app-launcher-tile-hover-background: var(--accent);
162
- --app-launcher-tile-hover-color: var(--accent-foreground);
161
+ --app-launcher-tile-hover-background: initial;
162
+ --app-launcher-tile-hover-color: initial;
163
163
 
164
164
  --app-launcher-tile-current-background: initial;
165
165
  --app-launcher-mark-size: 2.5rem;
@@ -100,19 +100,16 @@
100
100
 
101
101
  --shadow-glow: 0 0 0 0 transparent;
102
102
 
103
- --brand-glow-color: var(--primary);
103
+ --brand-glow-color: initial;
104
104
  --brand-glow-alpha: 0.18;
105
105
  --brand-glow-size: 60% 50%;
106
106
  --brand-glow-position: 50% 0%;
107
- --brand-glow: radial-gradient(
108
- ellipse var(--brand-glow-size) at var(--brand-glow-position),
109
- hsl(var(--brand-glow-color) / var(--brand-glow-alpha)),
110
- transparent 70%
111
- );
107
+ --brand-glow: initial;
112
108
 
113
109
  --focus-outline: 1;
114
110
  --focus-outline-weight: var(--stroke-hairline);
115
- --focus-outline-color: var(--focus-ring-color, var(--ring));
111
+
112
+ --focus-outline-color: initial;
116
113
 
117
114
  --control-outline-width: calc(var(--stroke-md) * var(--focus-outline));
118
115
  --focus-outline-offset: 0px;
@@ -243,6 +243,9 @@ The interaction states are **derived from the `--primary` in scope, at the eleme
243
243
  | `--control-outline` | **yes** — the focused field's halo (`--control-outline-alpha` stays per theme) | light `h s calc(l - 5.3)`, dark `h calc(s * 0.99) calc(l - 25.1)` |
244
244
  | `--primary-border` | **yes** (painted by no package surface; kept for the measurement in DESIGN-AUTHORITY) | light `h calc(s * 0.68) calc(l + 16)`, dark `h calc(s * 0.467) calc(l * 0.27043)` |
245
245
  | `--sidebar-item-active-foreground` | **yes** — defaults to the live `--primary-active` | — |
246
+ | `--focus-outline-color` | **yes, through `--ring`** — every keyboard-focus outline (gh#687) | `var(--focus-ring-color, var(--ring))`, read at the focused element — so it follows the `--ring` you set in the scope |
247
+ | Radio button bar selected (`--choice-button-*`), Slider active dot, BackTop progress, `.ui-brand-glow` | **yes** (gh#687) | `var(--primary)` / `var(--primary-foreground)` at the call site |
248
+ | Topbar item / AppLauncher tile / Segmented / filled-control hover | **follow `--accent`** (gh#687) | `var(--accent)` / `var(--accent-foreground)` at the call site |
246
249
  | `--primary-foreground` | **no — set it** | the label on a filled primary; you choose it for your seed |
247
250
  | `--ring` | **only on the element that declares `--primary`** — set it in a nested scope | `var(--primary)` on `:root` / `.dark`. `--ring` is a public role read as `hsl(var(--ring))` in consumer CSS, so it cannot become a live default; a scope below `<html>` inherits the root's |
248
251
  | `--destructive-*`, `--control-outline-error` | no — not brand | literals |
@@ -251,6 +254,7 @@ On the package seed these produce exactly the identity kit values the tier used
251
254
 
252
255
  **Rules that come with it:**
253
256
 
257
+ - **No token tier binds to a scoped role at `:root`.** Every knob whose default is `--primary`, `--primary-foreground`, `--ring`, `--accent` or `--accent-foreground` is `initial`, with the role as the call-site fallback — `--ring` itself is the one exception (above). `tenant-scope-freeze-687.test.ts` holds it.
254
258
  - **Override a state only by setting its knob** (`--primary-hover: 221 90% 72%`). A set knob wins over the derived default, in its own scope and below.
255
259
  - **Read a state through its fallback, not bare.** The four knobs are `initial` so that the default can resolve at the painting element (docs/TOKENS.md, the freeze rule). `hsl(var(--primary-hover))` on its own therefore paints nothing — use the utility (`bg-primary-hover`) or `hsl(var(--primary-hover, from hsl(var(--primary)) var(--primary-hover-channels)))`.
256
260
  - **Label polarity.** The theme's pair steps AWAY from that theme's default label: `darken` under a light label, `lighten` under a dark one, which keeps a label that clears 4.5:1 at rest at 4.5:1 in hover and pressed for every seed (measured, `derived-seed-sweep.test.ts`). If your seed needs the OTHER polarity of label (a pale yellow with dark text in the light theme), point the pair at it too — `applyPrimaryColor()` does this automatically from the label it picks:
@@ -3,6 +3,7 @@ import { AppShell, Flex, PageContainer, Sidebar, Topbar } from "@godxjp/ui/layou
3
3
  import type { SidebarSectionProp } from "@godxjp/ui/layout";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
5
5
  import { Button, Text } from "@godxjp/ui/general";
6
+ import { Input } from "@godxjp/ui/data-entry";
6
7
  import {
7
8
  Bell,
8
9
  Building2,
@@ -24,11 +25,13 @@ import {
24
25
  * single page:
25
26
  *
26
27
  * ?topbarSpan=content|full ?navRail=start|end|top|bottom ?collapsed=1 ?sidebar=none
27
- * ?responsive=docked ?measure=narrow|medium ?footer=1
28
+ * ?responsive=docked ?measure=narrow|medium ?footer=1|row
28
29
  *
29
30
  * `measure` and `footer` feed `scripts/check-app-shell-page-width.mjs` (gh#672): the page is fluid
30
31
  * in the main column at every sidebar state, and a sticky footer band always spans `.app-main`,
31
32
  * even when a service caps content with `--app-shell-page-max-width` or the page sets `measure`.
33
+ * The footer's CONTENT sits in the same column as the body (gh#682), so the two shapes a footer
34
+ * takes are both rendered: `footer=1` an end-aligned action group, `footer=row` a full-row composer.
32
35
  *
33
36
  * With no query it is `topbarSpan="full"` without a rail — the state gh#474 found collapsing
34
37
  * `main` into a 170px implicit track below 900px, with the page actions laid out past the edge.
@@ -66,12 +69,12 @@ function readState() {
66
69
  responsiveNavigation:
67
70
  params.get("responsive") === "docked" ? ("docked" as const) : ("drawer" as const),
68
71
  measure: (["narrow", "medium"] as const).find((m) => m === params.get("measure")),
69
- hasFooter: params.get("footer") === "1",
72
+ footer: (["1", "row"] as const).find((f) => f === params.get("footer")),
70
73
  };
71
74
  }
72
75
 
73
76
  export default function Demo() {
74
- const [{ topbarSpan, navRail, collapsed, hasSidebar, responsiveNavigation, measure, hasFooter }] =
77
+ const [{ topbarSpan, navRail, collapsed, hasSidebar, responsiveNavigation, measure, footer }] =
75
78
  useState(readState);
76
79
  const [activeId, setActiveId] = useState("dashboard");
77
80
  const strip = navRail === "top" || navRail === "bottom";
@@ -121,16 +124,23 @@ export default function Demo() {
121
124
  subtitle="株式会社アクメ · 2026年5月"
122
125
  measure={measure}
123
126
  footer={
124
- hasFooter ? (
127
+ footer === "1" ? (
125
128
  <Flex gap="sm" align="center" justify="end">
126
129
  <Button size="sm" variant="outline">
127
130
  下書き保存
128
131
  </Button>
129
132
  <Button size="sm">コメントを送信する</Button>
130
133
  </Flex>
134
+ ) : footer === "row" ? (
135
+ <Flex grow gap="sm" align="center">
136
+ <Flex grow>
137
+ <Input aria-label="コメント" placeholder="コメントを入力" />
138
+ </Flex>
139
+ <Button size="sm">送信</Button>
140
+ </Flex>
131
141
  ) : undefined
132
142
  }
133
- stickyFooter={hasFooter}
143
+ stickyFooter={footer !== undefined}
134
144
  extra={
135
145
  <Flex gap="sm" wrap>
136
146
  <Button size="sm" variant="outline">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "26.0.0",
4
- "godxUiMcp": "26.0.0",
3
+ "version": "26.2.0",
4
+ "godxUiMcp": "26.2.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -45,6 +45,19 @@ function mcpEntryMatches(a, b) {
45
45
  return keysA.every((k) => envA[k] === envB[k]);
46
46
  }
47
47
 
48
+ /**
49
+ * True when `entry` is the shape THIS package writes (`npx @godxjp/ui-mcp@<pin>`, env at most
50
+ * GODX_UI_VERSION), whatever pin and version it carries. Such an entry is ours to move forward on
51
+ * upgrade: treating it as custom left consumers on the MCP of the release they first installed
52
+ * (a `@godxjp/ui-mcp@25.4.0` pin survived the upgrade to 26.x).
53
+ */
54
+ function isPackageWrittenMcpEntry(entry) {
55
+ if (entry?.command !== "npx") return false;
56
+ const args = entry.args ?? [];
57
+ if (args.length !== 1 || !/^@godxjp\/ui-mcp@[^\s]+$/.test(args[0])) return false;
58
+ return Object.keys(entry.env ?? {}).every((key) => key === "GODX_UI_VERSION");
59
+ }
60
+
48
61
  function mcpConfigMismatchMessage(root, existing, expected) {
49
62
  const ui = readConsumerUiMetadata(root);
50
63
  const pin = expected.args?.[0] ?? "@godxjp/ui-mcp";
@@ -347,7 +360,11 @@ export function refreshBlock(current, next, startMarker, endMarker) {
347
360
  if (!endMarker) return null;
348
361
  const j = current.indexOf(endMarker, first);
349
362
  if (j < 0) return null;
350
- return current.slice(0, first) + next + current.slice(j + endMarker.length);
363
+ // `next` ends in its own newline, and the old block's newline is still the first character after
364
+ // the end marker — keeping both grew the file by one blank line on every refresh.
365
+ let tail = current.slice(j + endMarker.length);
366
+ if (next.endsWith("\n") && tail.startsWith("\n")) tail = tail.slice(1);
367
+ return current.slice(0, first) + next + tail;
351
368
  }
352
369
 
353
370
  export function ensureMcpJson(root) {
@@ -369,10 +386,13 @@ export function ensureMcpJson(root) {
369
386
  }
370
387
  json.mcpServers = json.mcpServers ?? {};
371
388
  if (json.mcpServers[MCP_KEY]) {
372
- if (!mcpEntryMatches(json.mcpServers[MCP_KEY], expected)) {
389
+ if (mcpEntryMatches(json.mcpServers[MCP_KEY], expected)) return "present";
390
+ if (!isPackageWrittenMcpEntry(json.mcpServers[MCP_KEY])) {
373
391
  return mcpConfigMismatchMessage(root, json.mcpServers[MCP_KEY], expected);
374
392
  }
375
- return "present";
393
+ json.mcpServers[MCP_KEY] = expected;
394
+ writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
395
+ return "refreshed";
376
396
  }
377
397
  const created = read.state === "missing";
378
398
  json.mcpServers[MCP_KEY] = expected;
package/scripts/cli.mjs CHANGED
@@ -18,13 +18,74 @@ const MAP = {
18
18
  "visual-audit": "visual-audit.mjs",
19
19
  };
20
20
 
21
+ // `<command> --help` is answered HERE: the scripts take free positional arguments, so a `--help`
22
+ // forwarded to ui-audit used to be read as nothing and ran a full audit instead of printing help.
23
+ const HELP = {
24
+ "init-agent": `godxjp-ui init-agent
25
+
26
+ Install the agent forcing-kit into the current consumer app (run it from the app root):
27
+ .mcp.json the godx-ui MCP server
28
+ .claude/settings.json a PostToolUse hook that audits every Write/Edit of a .tsx file
29
+ .claude/godxjp-ui-workflow.md
30
+ CLAUDE.md the godxjp-ui mandate block
31
+ Idempotent. Refuses inside the @godxjp/ui repo itself. Restart the agent afterwards.`,
32
+ "sync-rules": `godxjp-ui sync-rules
33
+
34
+ Refresh the package-owned agent files for the installed @godxjp/ui version: the godx-ui entry in
35
+ .mcp.json, the managed CLAUDE.md block, .claude/godxjp-ui-workflow.md and .ai/rules/godxjp-ui.md.
36
+ It is the postinstall step, run by hand — use it when the app installs with ignore-scripts=true,
37
+ where postinstall never runs and the rules go stale. Silent no-op in CI and when opted out.`,
38
+ audit: `godxjp-ui audit [dir …] [--changed] [--format json] [--quiet] [--rules]
39
+
40
+ Static UI-standardization audit over source (regex, no browser, fast). Exits non-zero on errors.
41
+ dir … directories or files to scan
42
+ --changed scan what this branch changed vs origin/main (committed, staged, untracked);
43
+ fails rather than reporting clean when origin/main cannot be resolved
44
+ --format json machine-readable findings
45
+ --quiet print errors only (warnings hidden)
46
+ --rules print the rule catalog as JSON and exit
47
+ Pre-commit: npx godxjp-ui audit resources/js || exit 1`,
48
+ "visual-audit": `godxjp-ui visual-audit [--format json] [--strict] <baseUrl> [route …]
49
+
50
+ Runtime audit (Playwright + axe-core) against an app you are ALREADY running locally.
51
+ baseUrl the running app, e.g. http://localhost:5173
52
+ route … paths appended to baseUrl (default "/")
53
+ --strict exit non-zero on any finding, not only errors
54
+ --format json machine-readable findings
55
+ --rules print the visual rule catalog as JSON and exit
56
+ Needs the playwright peer installed.`,
57
+ };
58
+
59
+ const USAGE = `usage: godxjp-ui <command> [args]
60
+
61
+ commands:
62
+ init-agent install the agent forcing-kit (MCP + auto-audit hook + CLAUDE.md mandate)
63
+ sync-rules refresh package-owned agent rules (postinstall, by hand)
64
+ audit static UI audit over source
65
+ visual-audit runtime audit (Playwright + axe-core) against a running app
66
+
67
+ godxjp-ui <command> --help details and flags for one command`;
68
+
69
+ const isHelp = (arg) => arg === "--help" || arg === "-h" || arg === "help";
21
70
  const [cmd, ...rest] = process.argv.slice(2);
71
+
72
+ if (cmd === undefined || isHelp(cmd)) {
73
+ const topic = isHelp(cmd) ? rest[0] : undefined;
74
+ console.log(topic && HELP[topic] ? HELP[topic] : USAGE);
75
+ process.exit(cmd === undefined ? 1 : 0);
76
+ }
22
77
  const script = MAP[cmd];
23
78
  if (!script) {
24
- console.error("usage: godxjp-ui <init-agent | sync-rules | audit | visual-audit> [args]");
79
+ console.error(`unknown command: ${cmd}\n\n${USAGE}`);
25
80
  process.exit(1);
26
81
  }
82
+ if (rest.includes("--help") || rest.includes("-h")) {
83
+ console.log(HELP[cmd]);
84
+ process.exit(0);
85
+ }
27
86
  const env =
28
- cmd === "sync-rules" ? { ...process.env, INIT_CWD: process.env.INIT_CWD ?? process.cwd() } : process.env;
87
+ cmd === "sync-rules"
88
+ ? { ...process.env, INIT_CWD: process.env.INIT_CWD ?? process.cwd() }
89
+ : process.env;
29
90
  const r = spawnSync("node", [join(HERE, script), ...rest], { stdio: "inherit", env });
30
91
  process.exit(r.status ?? 0);