@godxjp/ui 23.4.7 → 23.4.9

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.
@@ -142,7 +142,8 @@ declare const AlertDialogAction: typeof DialogAction;
142
142
  declare const AlertDialogCancel: typeof DialogCancel;
143
143
  /**
144
144
  * Preset: confirm / destructive / typed-challenge / step-up without compound markup. Both flows
145
- * force the destructive tone (button + soft header band).
145
+ * force the destructive shape: the confirm button plus antd's leading status glyph. The header
146
+ * surface stays untinted — see the note on `showDangerIcon` below.
146
147
  */
147
148
  declare function AlertDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, variant, confirmPhrase, challenge, onConfirm, stepUp, keepOpenOnConfirm, pending, }: AlertDialogProp): React.JSX.Element;
148
149
  export declare const Dialog: typeof DialogRoot & {
@@ -4,7 +4,7 @@ import { useOverlayPortalContainer } from "../../lib/overlay-portal.js";
4
4
  import * as React from "react";
5
5
  import { chain, mergeRefs } from "@react-aria/utils";
6
6
  import { Dialog as RacDialog, Modal, ModalOverlay } from "react-aria-components";
7
- import { X } from "lucide-react";
7
+ import { AlertCircle, X } from "lucide-react";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  import { Slot } from "../../lib/slot.js";
10
10
  import { overlayHeaderToneClass } from "./overlay-header-tone.js";
@@ -392,7 +392,7 @@ function AlertDialog({
392
392
  const needsPhrase = phrase != null && phrase.length > 0;
393
393
  const phraseMatches = !needsPhrase || typed === phrase;
394
394
  const effectiveVariant = needsPhrase ? "destructive" : variant;
395
- const headerTone = effectiveVariant === "destructive" ? "destructive" : "default";
395
+ const showDangerIcon = effectiveVariant === "destructive";
396
396
  const resolvedConfirm = confirmLabel ?? (needsPhrase ? t("common.delete") : t("common.continue"));
397
397
  const resolvedCancel = cancelLabel ?? t("common.cancel");
398
398
  const busy = pending || verifying;
@@ -429,10 +429,13 @@ function AlertDialog({
429
429
  })();
430
430
  };
431
431
  return /* @__PURE__ */ jsx(AlertDialogRoot, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [
432
- /* @__PURE__ */ jsxs(DialogHeader, { tone: headerTone, children: [
433
- /* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
434
- description ? /* @__PURE__ */ jsx(AlertDialogDescription, { children: description }) : null
435
- ] }),
432
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "ui-dialog-confirm-body", children: [
433
+ showDangerIcon && /* @__PURE__ */ jsx(AlertCircle, { className: "ui-dialog-confirm-icon", "aria-hidden": true }),
434
+ /* @__PURE__ */ jsxs("div", { className: "ui-dialog-confirm-text", children: [
435
+ /* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
436
+ description ? /* @__PURE__ */ jsx(AlertDialogDescription, { children: description }) : null
437
+ ] })
438
+ ] }) }),
436
439
  needsPhrase && /* @__PURE__ */ jsx(FormField, { id: inputId, label: t("common.typeToConfirm", { phrase }), children: /* @__PURE__ */ jsx(
437
440
  Input,
438
441
  {
@@ -0,0 +1,57 @@
1
+ {
2
+ "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
+ "version": "23.4.9",
4
+ "targetSize": {
5
+ "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
+ "min": 24,
7
+ "measures": "target",
8
+ "note": "The TARGET is the region that accepts the pointer action, which is not always the painted box. getBoundingClientRect() returns the border box and cannot see a pseudo-element, so it UNDER-REPORTS every selector listed under `expanders`. Measure with elementFromPoint — see `method` — or treat a listed selector as conforming at `targetMin`.",
9
+ "method": "docs/MEASUREMENT-CONTRACT.md#measuring-a-target",
10
+ "expanders": [
11
+ {
12
+ "selector": ".ui-control-inline-affix-action",
13
+ "via": "::after",
14
+ "source": "src/styles/control.css",
15
+ "targetMin": 24,
16
+ "components": [
17
+ "badge",
18
+ "date-picker",
19
+ "input",
20
+ "tag-input",
21
+ "textarea",
22
+ "time-picker"
23
+ ]
24
+ },
25
+ {
26
+ "selector": ".ui-data-table-sort-button",
27
+ "via": "::after",
28
+ "source": "src/styles/table-layout.css",
29
+ "targetMin": 24,
30
+ "components": [
31
+ "data-table"
32
+ ]
33
+ },
34
+ {
35
+ "selector": ".ui-number-input-step",
36
+ "via": "::after",
37
+ "source": "src/styles/control.css",
38
+ "targetMin": 24,
39
+ "components": [
40
+ "number-input"
41
+ ]
42
+ }
43
+ ]
44
+ },
45
+ "spacing": {
46
+ "scale": "--space-* measures the distance BETWEEN things: page sections, siblings, a Card and its content.",
47
+ "doesNotApplyTo": "The INTERIOR of a control. That geometry is derived from the control band, not from --space-*, so a control's own label sits closer to its edge than --space-2 and is not a finding.",
48
+ "derivation": "interior height = --control-height − (interior padding × 2)",
49
+ "predicate": "An element is a control when its painted block-size equals the resolved --control-height for its size. Segmented's track, Button, Input and Select all satisfy it — a gate that applies a --space-* floor inside a control flags all four, not just Segmented.",
50
+ "reference": "docs/SPACING.md#control-interiors-are-not-on-this-scale"
51
+ },
52
+ "issues": {
53
+ "503": "Segmented track padding read against a --space-2 floor that does not govern control interiors.",
54
+ "506": "NumberInput steppers read as 24×13 (paint); the target is 24×26.",
55
+ "507": "DatePicker calendar button read as 20×20 (paint); the target is 24×24."
56
+ }
57
+ }
@@ -470,6 +470,10 @@
470
470
  fill: currentColor;
471
471
  }
472
472
 
473
+ .ui-radio[data-state="unchecked"] .ui-radio-icon {
474
+ visibility: hidden;
475
+ }
476
+
473
477
  .ui-switch:has(input[aria-invalid="true"]) {
474
478
  --tw-ring-shadow: 0 0 0 var(--focus-ring-width) hsl(var(--destructive));
475
479
  box-shadow: 0 0 0 var(--focus-ring-width) hsl(var(--destructive));
@@ -123,7 +123,8 @@
123
123
  }
124
124
  }
125
125
 
126
- [data-slot="dialog-content"]:has([data-slot="dialog-body"]) [data-slot="dialog-header"] {
126
+ [data-slot="dialog-content"]:has([data-slot="dialog-body"]) [data-slot="dialog-header"],
127
+ [data-slot="dialog-header"]:not([data-tone="default"]) {
127
128
  margin-inline: calc(-1 * var(--dialog-space-x));
128
129
  margin-block-start: calc(-1 * var(--dialog-space-y));
129
130
  padding: var(--dialog-space-y) var(--dialog-space-x);
@@ -222,6 +223,26 @@
222
223
  [data-slot="dialog-header"]:not([data-tone="default"]) [data-slot="dialog-description"] {
223
224
  color: hsl(var(--foreground));
224
225
  }
226
+
227
+ .ui-dialog-confirm-body {
228
+ display: flex;
229
+ align-items: flex-start;
230
+ gap: var(--dialog-confirm-space-gap);
231
+ }
232
+
233
+ .ui-dialog-confirm-icon {
234
+ flex: none;
235
+ inline-size: var(--dialog-confirm-icon-size);
236
+ block-size: var(--dialog-confirm-icon-size);
237
+ color: hsl(var(--text-error));
238
+ }
239
+
240
+ .ui-dialog-confirm-text {
241
+ display: flex;
242
+ min-inline-size: 0;
243
+ flex-direction: column;
244
+ gap: var(--dialog-title-block-space-gap);
245
+ }
225
246
  }
226
247
 
227
248
  @media (prefers-reduced-motion: reduce) {
@@ -716,6 +716,11 @@
716
716
  width: 100%;
717
717
  }
718
718
 
719
+ .ui-page-header-extra > .ui-flex[data-direction="row"] {
720
+ flex-wrap: wrap;
721
+ max-inline-size: 100%;
722
+ }
723
+
719
724
  .ui-page-header-title-row {
720
725
  display: flex;
721
726
  flex-wrap: wrap;
@@ -762,11 +767,6 @@
762
767
  .ui-page-header .ui-page-subtitle {
763
768
  font-size: var(--page-subtitle-font-size-compact);
764
769
  }
765
-
766
- .ui-page-header-extra > .ui-flex[data-direction="row"] {
767
- flex-wrap: wrap;
768
- max-width: 100%;
769
- }
770
770
  }
771
771
 
772
772
  @media (max-width: 639.98px) {
@@ -36,6 +36,9 @@
36
36
  --dialog-title-block-space-gap: var(--space-1);
37
37
  --dialog-extra-space-gap: var(--space-2);
38
38
 
39
+ --dialog-confirm-icon-size: var(--icon-size-xl);
40
+ --dialog-confirm-space-gap: var(--space-3);
41
+
39
42
  --dialog-header-close-space-inline-end: var(--space-8);
40
43
 
41
44
  --dialog-close-rest-alpha: 0.7;
@@ -0,0 +1,109 @@
1
+ # Measurement contract
2
+
3
+ **What box this library means when it says a target is 24×24, and where the `--space-*` scale stops
4
+ applying.** The facts below also ship as data at
5
+ `node_modules/@godxjp/ui/dist/contracts/measurement.json`, so a gate can read them without reading
6
+ this page.
7
+
8
+ ## Why this page exists
9
+
10
+ Three issues — [#503], [#506], [#507] — were each closed and reopened four times between 23.0.0 and
11
+ 23.4.5. Every reopening carried a correct measurement. Every closing carried a correct fix. The two
12
+ sides were measuring different boxes:
13
+
14
+ | issue | the consumer's gate read | what the library ships |
15
+ | ----- | ------------------------ | ---------------------------- |
16
+ | #507 | `20×20` | a **24×24** target |
17
+ | #506 | `24×13` | a **24×26** target |
18
+ | #503 | text `5.1px` from edge | 5.1px **derived** from the band |
19
+
20
+ `getBoundingClientRect()` returns an element's **border box**, and a border box does not include an
21
+ absolutely-positioned pseudo-element. So a gate built on it reports the same number forever no
22
+ matter what the library ships — which means it cannot tell *fixed* from *ignored*, and reopening the
23
+ issue is the correct thing for it to do. Prose could not close that gap either: `docs/SPACING.md`
24
+ carried the #503 derivation from 23.4.0 onward and the issue was reopened twice afterwards, because
25
+ a gate cannot read prose.
26
+
27
+ ## Target size
28
+
29
+ **WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px.** The criterion measures the
30
+ **target**: the region that accepts the pointer action. That is not always the painted box, and
31
+ carrying the target on a pseudo-element is the technique [Understanding 2.5.8] names.
32
+
33
+ This library does that in three places, because in each one growing the **paint** would move
34
+ geometry that belongs to something else:
35
+
36
+ | selector | paint stays | why the paint cannot grow |
37
+ | ------------------------------- | ----------- | --------------------------------------------------------------------------- |
38
+ | `.ui-control-inline-affix-action` | 20×20 | it is an affix inside a 32px field; growing it moves the field's own geometry |
39
+ | `.ui-number-input-step` | 24×13 | two steppers stacked in a 32px band — 24×2 = 48 does not fit |
40
+ | `.ui-data-table-sort-button` | header size | a header cell's height is the table's row rhythm |
41
+
42
+ The authoritative list is `targetSize.expanders` in `dist/contracts/measurement.json`; it is
43
+ generated from the CSS, and `scripts/check-measurement-contract.mjs` proves every entry in a real
44
+ browser before release, so it cannot claim a target the library does not ship.
45
+
46
+ ### Measuring a target
47
+
48
+ Scan outward from the centre with `elementFromPoint` until the point stops belonging to the element.
49
+ This reads the real target, and it also catches the case `getBoundingClientRect` misses in the other
50
+ direction — a 44×44 button half-covered by something else still reports 44×44 from its rect.
51
+
52
+ ```js
53
+ function hitRegion(el) {
54
+ const r = el.getBoundingClientRect();
55
+ const cx = r.left + r.width / 2;
56
+ const cy = r.top + r.height / 2;
57
+ const edge = (dx, dy) => {
58
+ let reach = 0;
59
+ for (let i = 0; i <= 40; i += 0.25) {
60
+ const hit = document.elementFromPoint(cx + dx * i, cy + dy * i);
61
+ if (hit === el || el.contains(hit)) reach = i;
62
+ else break;
63
+ }
64
+ return reach;
65
+ };
66
+ return { width: edge(-1, 0) + edge(1, 0), height: edge(0, -1) + edge(0, 1) };
67
+ }
68
+ ```
69
+
70
+ If your gate cannot run a hit-test, read the contract and treat a listed selector as conforming at
71
+ its `targetMin` instead:
72
+
73
+ ```js
74
+ const contract = require("@godxjp/ui/contracts/measurement.json");
75
+ const expanded = contract.targetSize.expanders.map((e) => e.selector);
76
+
77
+ // inside the gate, per candidate element:
78
+ if (expanded.some((sel) => el.matches(sel) || el.closest(sel))) return; // target is on a ::after
79
+ ```
80
+
81
+ ## Spacing
82
+
83
+ `--space-*` measures the distance **between** things: page sections, siblings in a stack, a card's
84
+ shell against its content. It is **not** a floor for the **interior** of a control — that geometry
85
+ derives from the control band (`--control-height`, `--control-padding-x`).
86
+
87
+ ```
88
+ interior height = --control-height − (interior padding × 2)
89
+ ```
90
+
91
+ A gate can tell the two apart at runtime without a selector list:
92
+
93
+ > An element is a control when its painted block-size equals the resolved `--control-height` for its
94
+ > size.
95
+
96
+ `Segmented`'s track satisfies it, and so do `Button`, `Input` and `Select` — which is the useful part
97
+ of the test: a `--space-2` floor applied inside a control flags all four, not just the one that got
98
+ reported. `docs/SPACING.md` § *Control interiors are NOT on this scale* carries the full derivation
99
+ and the `--segmented-track-padding` knob for a service that wants a roomier control.
100
+
101
+ ## MCP
102
+
103
+ `get_rule` · `list_audit_rules` · `get_tokens` — and `draft_bug_report` if a measurement taken this
104
+ way still disagrees with what the library claims. Bring the number; that is what this page is for.
105
+
106
+ [#503]: https://github.com/godx-jp/godxjp-ui/issues/503
107
+ [#506]: https://github.com/godx-jp/godxjp-ui/issues/506
108
+ [#507]: https://github.com/godx-jp/godxjp-ui/issues/507
109
+ [understanding 2.5.8]: https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html
package/docs/SPACING.md CHANGED
@@ -89,6 +89,11 @@ this library holds — or the track grows to 44px (28 + 8 × 2) and stops lining
89
89
  control. The same derivation is why a `Button`'s own label sits ~5px inside its border. A control
90
90
  is sized by its band; the band is what a dense enterprise UI is for.
91
91
 
92
+ **For a gate, not a reader.** This derivation also ships as data at
93
+ `dist/contracts/measurement.json` (`spacing`), because gh#503 was reopened twice *after* this
94
+ section was written — prose cannot be read by the browser test that files the bug. See
95
+ `docs/MEASUREMENT-CONTRACT.md`.
96
+
92
97
  **The knob, if a service wants a roomier control.** `--segmented-track-padding` is a published
93
98
  component token (`src/tokens/components/segmented.css`); raising it re-derives the item height from
94
99
  the same formula, so the track stays exactly one control tall. Do not reach for a Tailwind `p-*` on
@@ -7,7 +7,8 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
7
7
  /**
8
8
  * DangerConfirm recipe (godxui#193) — high-stakes deletion built on the AlertDialog preset.
9
9
  * No new component: `challenge` gates the confirm button behind an exact typed slug, and `stepUp`
10
- * runs an async passkey / 2FA re-auth before onConfirm fires. Both force the destructive tone.
10
+ * runs an async passkey / 2FA re-auth before onConfirm fires. Both force the destructive shape
11
+ * (button + leading status glyph, untinted surface — antd `Modal.confirm` parity).
11
12
  * Mirrors DXS SCR-203 (org delete by slug) and SCR-209 (refund with step-up).
12
13
  */
13
14
  export default function Demo() {
@@ -25,8 +26,10 @@ export default function Demo() {
25
26
  <CardTitle level={2}>Organization delete · typed slug challenge</CardTitle>
26
27
  <CardDescription>
27
28
  The confirm button stays disabled until the operator types the exact organization slug
28
- (<code>acme-inc</code>). A typed challenge always forces the destructive tone and a
29
- soft danger header band.
29
+ (<code>acme-inc</code>). A typed challenge always forces the destructive shape: the
30
+ destructive confirm button and antd&rsquo;s leading status glyph. The header surface
31
+ stays untinted — the glyph is the signal, exactly as <code>Modal.confirm</code> does
32
+ it.
30
33
  </CardDescription>
31
34
  </CardHeader>
32
35
  <CardContent>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "23.4.7",
4
- "godxUiMcp": "23.4.7",
3
+ "version": "23.4.9",
4
+ "godxUiMcp": "23.4.9",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -52,6 +52,7 @@
52
52
  "./styles/*": "./dist/styles/*.css",
53
53
  "./theme/*": "./dist/theme/*",
54
54
  "./tokens": "./dist/tokens/base.css",
55
+ "./contracts/measurement.json": "./dist/contracts/measurement.json",
55
56
  "./admin": {
56
57
  "types": "./dist/components/admin/index.d.ts",
57
58
  "import": "./dist/components/admin/index.js"
@@ -335,7 +336,7 @@
335
336
  "verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
336
337
  "verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
337
338
  "verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
338
- "verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-catalog-completeness && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets",
339
+ "verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:measurement-contract && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-catalog-completeness && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets",
339
340
  "verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:font-fallback-metrics && pnpm check:visual-audit",
340
341
  "verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage",
341
342
  "verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
@@ -399,6 +400,8 @@
399
400
  "check:data-entry-frame-runtime": "node scripts/check-data-entry-frame-runtime.mjs",
400
401
  "check:data-table-empty-inset": "node scripts/check-data-table-empty-inset.mjs",
401
402
  "check:number-input-step-target": "node scripts/check-number-input-step-target.mjs",
403
+ "check:measurement-contract": "node scripts/gen-measurement-contract.mjs --check",
404
+ "check:measurement-contract-browser": "node scripts/check-measurement-contract.mjs",
402
405
  "check:mcp-token-sync": "node scripts/gen-component-tokens.mjs --check",
403
406
  "check:email-token-sync": "node scripts/gen-email-tokens.mjs --check",
404
407
  "check:focus-ring-paint": "node scripts/check-focus-ring-paint.mjs",