@godxjp/ui 26.1.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.
@@ -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.1.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:
@@ -96,7 +96,14 @@
96
96
  }
97
97
 
98
98
  .ui-brand-glow {
99
- 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
+ );
100
107
  pointer-events: none;
101
108
  }
102
109
 
@@ -1434,8 +1434,8 @@
1434
1434
  }
1435
1435
 
1436
1436
  .ui-app-launcher-tile:hover {
1437
- background: hsl(var(--app-launcher-tile-hover-background));
1438
- 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)));
1439
1439
  }
1440
1440
 
1441
1441
  .ui-app-launcher-tile[aria-current="page"] {
@@ -2040,14 +2040,14 @@
2040
2040
  }
2041
2041
 
2042
2042
  .ui-topbar-item:hover {
2043
- background: hsl(var(--topbar-item-hover-background));
2044
- 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)));
2045
2045
  }
2046
2046
 
2047
2047
  .ui-topbar-item:active,
2048
2048
  .ui-topbar-item[data-state="open"] {
2049
2049
  background: hsl(var(--topbar-item-active-background));
2050
- color: hsl(var(--topbar-item-hover-color));
2050
+ color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
2051
2051
  }
2052
2052
 
2053
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:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "26.1.0",
4
- "godxUiMcp": "26.1.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);