@godxjp/ui 19.4.0 → 19.4.2

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.
Files changed (65) hide show
  1. package/dist/components/data-display/badge.d.ts +1 -1
  2. package/dist/components/data-display/list-row.d.ts +2 -0
  3. package/dist/components/data-display/list-row.js +16 -12
  4. package/dist/components/data-display/popover.d.ts +1 -1
  5. package/dist/components/data-display/table.d.ts +1 -1
  6. package/dist/components/data-display/timeline-grid.js +5 -3
  7. package/dist/components/data-entry/checkbox.js +1 -1
  8. package/dist/components/data-entry/form-field.js +5 -5
  9. package/dist/components/data-entry/input.js +1 -1
  10. package/dist/components/data-entry/radio.js +1 -1
  11. package/dist/components/data-entry/search-input.d.ts +2 -18
  12. package/dist/components/data-entry/search-input.js +3 -0
  13. package/dist/components/data-entry/select.js +1 -1
  14. package/dist/components/general/button.d.ts +2 -0
  15. package/dist/components/general/button.js +5 -1
  16. package/dist/components/general/inert-background.js +2 -1
  17. package/dist/components/layout/app-shell.js +1 -1
  18. package/dist/components/layout/master-detail.d.ts +1 -1
  19. package/dist/components/layout/master-detail.js +47 -3
  20. package/dist/components/layout/mobile-shell.d.ts +1 -1
  21. package/dist/components/layout/responsive-grid.js +7 -4
  22. package/dist/components/navigation/app-setting-picker.js +10 -1
  23. package/dist/email/tokens.generated.d.ts +1 -1
  24. package/dist/email/tokens.generated.js +1 -1
  25. package/dist/props/components/data-display.prop.d.ts +1 -1
  26. package/dist/props/components/data-entry.prop.d.ts +10 -5
  27. package/dist/props/components/general.prop.d.ts +4 -0
  28. package/dist/props/components/layout.prop.d.ts +8 -2
  29. package/dist/props/registry.d.ts +2 -2
  30. package/dist/props/registry.js +2 -1
  31. package/dist/props/vocabulary/layout.prop.d.ts +1 -1
  32. package/dist/styles/control.css +29 -0
  33. package/dist/styles/data-display-layout.css +22 -1
  34. package/dist/styles/focus-ring.css +9 -0
  35. package/dist/styles/form-layout.css +6 -0
  36. package/dist/styles/layout.css +36 -1
  37. package/dist/styles/navigation-layout.css +2 -1
  38. package/dist/tokens/components/data-display.css +2 -1
  39. package/dist/tokens/foundation.css +2 -1
  40. package/docs/FRAME-A11Y-CI.md +99 -17
  41. package/docs/FRAME-COVERAGE-REPORT.md +4 -2
  42. package/docs/data-display/list-row.tsx +21 -0
  43. package/docs/data-display/popover.tsx +4 -1
  44. package/docs/data-entry/date-picker.tsx +2 -1
  45. package/docs/data-entry/form-field/index.tsx +2 -2
  46. package/docs/data-entry/search-input.tsx +1 -0
  47. package/docs/data-entry/select-matrix.tsx +2 -1
  48. package/docs/data-entry/select.tsx +3 -1
  49. package/docs/feedback/dialog.tsx +2 -1
  50. package/docs/feedback/sheet.tsx +2 -1
  51. package/docs/general/button/index.md +4 -0
  52. package/docs/layout/app-shell.tsx +4 -4
  53. package/docs/layout/master-detail.tsx +3 -0
  54. package/docs/layout/responsive-grid.tsx +21 -4
  55. package/docs/layout/sidebar.tsx +3 -3
  56. package/docs/layout/split-pane.tsx +4 -5
  57. package/docs/navigation/context-menu.tsx +6 -1
  58. package/docs/navigation/dropdown-menu.tsx +2 -1
  59. package/docs/navigation/tabs.tsx +2 -2
  60. package/docs/showcase/case6-agency-handy.tsx +2 -2
  61. package/docs/showcase/org-switcher.tsx +1 -1
  62. package/docs/showcase/settings-account-sections.tsx +2 -2
  63. package/docs/showcase/table-master-detail.tsx +1 -1
  64. package/docs/showcase/table-view-tabs.tsx +1 -1
  65. package/package.json +2 -2
@@ -2063,3 +2063,32 @@
2063
2063
  color: var(--button-count-secondary-color, hsl(var(--secondary)));
2064
2064
  }
2065
2065
  }
2066
+
2067
+ @layer components {
2068
+ .ui-button[data-wrap] {
2069
+ --button-wrap-min-height: var(--control-height);
2070
+ height: auto;
2071
+ min-height: var(--button-wrap-min-height);
2072
+ max-inline-size: 100%;
2073
+ padding-block: var(--space-1);
2074
+ white-space: normal;
2075
+ overflow-wrap: anywhere;
2076
+ }
2077
+ .ui-button[data-wrap][data-size="xs"] {
2078
+ --button-wrap-min-height: var(--control-height-xs);
2079
+ }
2080
+ .ui-button[data-wrap][data-size="sm"] {
2081
+ --button-wrap-min-height: var(--control-height-sm);
2082
+ }
2083
+ .ui-button[data-wrap][data-size="lg"] {
2084
+ --button-wrap-min-height: var(--control-height-lg);
2085
+ }
2086
+ .ui-button[data-align="start"] {
2087
+ justify-content: flex-start;
2088
+ text-align: start;
2089
+ }
2090
+ .ui-button[data-align="end"] {
2091
+ justify-content: flex-end;
2092
+ text-align: end;
2093
+ }
2094
+ }
@@ -983,7 +983,9 @@
983
983
  box-sizing: border-box;
984
984
  inline-size: calc(100% / var(--timeline-grid-event-lanes, 1));
985
985
  block-size: calc(var(--timeline-grid-hour-height) * var(--timeline-grid-event-span, 0));
986
- min-block-size: var(--timeline-grid-event-min-height);
986
+ min-block-size: calc(
987
+ var(--timeline-grid-hour-height) * var(--timeline-grid-event-min-height-minutes) / 60
988
+ );
987
989
  padding-inline-end: var(--timeline-grid-event-gap);
988
990
  padding-block-end: var(--timeline-grid-event-gap);
989
991
  }
@@ -1064,3 +1066,22 @@
1064
1066
  pointer-events: none;
1065
1067
  }
1066
1068
  }
1069
+
1070
+ @layer components {
1071
+ a.ui-list-row {
1072
+ color: inherit;
1073
+ text-decoration: none;
1074
+ cursor: pointer;
1075
+ }
1076
+ a.ui-list-row:hover {
1077
+ background: hsl(var(--muted));
1078
+ }
1079
+ a.ui-list-row[aria-current="page"] {
1080
+
1081
+ background: hsl(var(--accent));
1082
+ box-shadow: inset var(--space-1) 0 0 hsl(var(--primary));
1083
+ }
1084
+ [dir="rtl"] a.ui-list-row[aria-current="page"] {
1085
+ box-shadow: inset calc(-1 * var(--space-1)) 0 0 hsl(var(--primary));
1086
+ }
1087
+ }
@@ -34,6 +34,7 @@
34
34
 
35
35
  :is(
36
36
  .ui-focus-ring-outline,
37
+ a.ui-list-row,
37
38
  .ui-accordion-trigger,
38
39
  .ui-carousel-dot,
39
40
  .ui-rating-star,
@@ -57,6 +58,10 @@
57
58
  --focus-ring-opacity: var(--topbar-icon-focus-ring-alpha);
58
59
  }
59
60
 
61
+ a.ui-list-row {
62
+ --focus-ring-offset: calc(-1 * var(--focus-ring-width));
63
+ }
64
+
60
65
  .ui-accordion-trigger {
61
66
  --focus-ring-offset: var(--accordion-focus-ring-offset);
62
67
  }
@@ -78,4 +83,8 @@
78
83
  outline: none;
79
84
  box-shadow: none;
80
85
  }
86
+
87
+ [aria-invalid="true"] {
88
+ --focus-ring-color: var(--destructive);
89
+ }
81
90
  }
@@ -1,6 +1,12 @@
1
1
 
2
2
 
3
3
  @layer components {
4
+
5
+ .ui-form-field-required {
6
+ margin-inline-start: var(--space-inline-xs);
7
+ color: hsl(var(--destructive));
8
+ }
9
+
4
10
  .ui-form {
5
11
  display: flex;
6
12
  flex-direction: column;
@@ -239,7 +239,7 @@
239
239
  .ui-responsive-grid {
240
240
  display: grid;
241
241
  gap: var(--space-stack-md);
242
- grid-template-columns: minmax(0, 1fr);
242
+ grid-template-columns: repeat(var(--responsive-grid-base, 1), minmax(0, 1fr));
243
243
  }
244
244
 
245
245
  .ui-responsive-grid[data-gap="none"] {
@@ -1348,3 +1348,38 @@
1348
1348
  }
1349
1349
  }
1350
1350
  }
1351
+
1352
+ @layer components {
1353
+ .ui-master-detail-back {
1354
+ display: none;
1355
+ }
1356
+ .ui-master-detail[data-mobile-pane] {
1357
+ position: relative;
1358
+ }
1359
+ .ui-master-detail-measure {
1360
+ position: absolute;
1361
+ inset-inline: 0;
1362
+ inset-block-start: 0;
1363
+ block-size: 0;
1364
+ overflow: hidden;
1365
+ visibility: hidden;
1366
+ pointer-events: none;
1367
+ }
1368
+ .ui-master-detail-measure > span {
1369
+ display: block;
1370
+ inline-size: var(--master-detail-collapse-below);
1371
+ block-size: 0;
1372
+ }
1373
+ .ui-master-detail[data-mobile-compact][data-mobile-pane="master"] > .ui-master-detail-detail,
1374
+ .ui-master-detail[data-mobile-compact][data-mobile-pane="detail"] > .ui-master-detail-master {
1375
+ display: none;
1376
+ }
1377
+ .ui-master-detail[data-mobile-compact] > .ui-master-detail-master {
1378
+ max-block-size: none;
1379
+ overflow: visible;
1380
+ }
1381
+ .ui-master-detail[data-mobile-compact] .ui-master-detail-back {
1382
+ display: block;
1383
+ margin-block-end: var(--space-3);
1384
+ }
1385
+ }
@@ -82,7 +82,8 @@
82
82
  }
83
83
  }
84
84
 
85
- .ui-pagination .ui-button svg {
85
+ .ui-pagination .ui-button svg,
86
+ .ui-pagination .ui-pagination-link svg {
86
87
  inline-size: var(--pagination-icon-size);
87
88
  block-size: var(--pagination-icon-size);
88
89
  }
@@ -86,7 +86,8 @@
86
86
  --timeline-grid-hour-height: 2.5rem;
87
87
  --timeline-grid-axis-width: 3.25rem;
88
88
  --timeline-grid-column-min-width: 6rem;
89
- --timeline-grid-event-min-height: 1.5rem;
89
+
90
+ --timeline-grid-event-min-height-minutes: 36;
90
91
  --timeline-grid-axis-font-size: var(--font-size-2xs);
91
92
  --timeline-grid-head-font-size: var(--font-size-xs);
92
93
  --timeline-grid-event-font-size: var(--font-size-2xs);
@@ -14,7 +14,8 @@
14
14
  --secondary: 45 15% 95%;
15
15
  --secondary-foreground: 48 8% 13%;
16
16
  --muted: 45 15% 95%;
17
- --muted-foreground: 44 5% 42%;
17
+
18
+ --muted-foreground: 44 5% 39%;
18
19
  --accent: 40 13% 91%;
19
20
  --accent-foreground: 48 8% 13%;
20
21
  --destructive: 357 64% 44%;
@@ -20,11 +20,56 @@ A control the **user cannot bring into the frame** — not merely one whose box
20
20
 
21
21
  - `check:contrast` (`scripts/check-contrast.mjs`) — a hand-picked set of ~6 pages checked for WCAG-AA text contrast. It never saw the other ~100 component frames. - `audit:examples` (`pnpm audit docs`) — a static source-regex linter. It can catch a hard-coded hex value but cannot see what actually renders: computed contrast, DOM landmark structure, focus order, or anything a Radix portal produces at runtime.
22
22
 
23
- ## Chrome vs component: two independently-tracked scopes
23
+ ## Chrome vs component vs overlay: three independently-tracked scopes
24
24
 
25
- `check:frame-axe` runs axe **twice** per frame/viewport:
25
+ `check:frame-axe` runs axe **twice** per frame/viewport, and a **third** time on the frames that declare an open step:
26
26
 
27
27
  - **Preview chrome** = the zoom / dimension **toolbar** (`.demo-block-toolbar`, `preview/src/demo-block.tsx`). We own this. It **must be 0** — a chrome violation fails the build.
28
+ - **Component/demo** = everything else, in the frame's **default** state (the rendered frame _and_ its Radix portals). Allowlisted in `baseline.component`.
29
+ - **Overlay** = the same scope re-measured **after opening an overlay** — see below. Allowlisted separately in `baseline.overlay`.
30
+
31
+ ### The overlay scope: opening the thing before measuring it (#355)
32
+
33
+ The gate used to load each frame in its default state and scan once, so every axe rule whose _condition only exists while an overlay is open_ — `aria-hidden-focus` first among them — was **outside its field of view**, not merely un-triggered. 164 frames × 2 viewports reported clean while `aria-hidden-focus` fired on the app background the moment a `Select` or `DropdownMenu` opened in a consumer app.
34
+
35
+ A frame opts in **declaratively**, from the demo itself:
36
+
37
+ ```tsx
38
+ // the trigger itself, when it forwards DOM props
39
+ <DropdownMenuTrigger asChild>
40
+ <Button data-axe-open …>…</Button>
41
+ </DropdownMenuTrigger>
42
+
43
+ // the region that owns the trigger, when the component renders it internally
44
+ <CardContent data-axe-open>
45
+ <FormField id="status" label="状態"><Select … /></FormField>
46
+ </CardContent>
47
+
48
+ // right-click, for the one overlay that opens on no other gesture
49
+ <ContextMenuTrigger data-axe-open="contextmenu" …>
50
+ ```
51
+
52
+ What the gate does with it, per frame/viewport, **after** the two default-state scans (opening an overlay is destructive to the state those measure):
53
+
54
+ 1. Take the **first** `[data-axe-open]` on the page. No declaration ⇒ the frame skips this scope entirely and costs nothing.
55
+ 2. Press **Escape** up to three times while any overlay is mounted. Several demos deliberately render one open at rest, and a modal one makes the rest of the page inert — so the trigger is unreachable until it is dismissed. This also makes the measurement a real **closed → open transition** rather than whatever the demo happened to leave mounted.
56
+ 3. Resolve the click target: the declaring element itself when it matches `button, [role=button], [role=combobox], [role=menuitem], a[href], summary`, otherwise the first such control inside it. (`DatePicker`, the data-driven `Select` and `SearchSelect` own their trigger DOM and forward no `data-*` to it — hence the "declare on the region" form. `DatePicker`'s combobox `<input>` opens the calendar on click, which is what gets pressed there.)
57
+ 4. Click (or right-click), then wait until **one more** overlay is mounted (`[data-radix-popper-content-wrapper], [role=dialog], [role=alertdialog], [role=menu], [role=listbox]`) plus a 400 ms animation settle — never a bare timer.
58
+ 5. Re-run axe with the same scope as the component pass and record it under `baseline.overlay`.
59
+
60
+ A declaration whose overlay never opens is an **infrastructure error** (blocking), not a silent skip: a broken declaration must not read as a clean frame.
61
+
62
+ **Frames declaring an open step today** (the overlay families named in #355): `data-entry-select`, `data-entry-select-matrix` (SearchSelect), `data-entry-date-picker`, `data-display-popover`, `navigation-dropdown-menu`, `navigation-context-menu`, `feedback-dialog`, `feedback-sheet`.
63
+
64
+ **Cost**: the overlay pass only runs on declaring frames, so it is ~8 frames × 2 viewports, measured at **+~55 s on a ~9.5 min local sweep (~10 %)**. Both viewports are kept for now; if the declaring set grows, narrow `OVERLAY_VIEWPORTS` in the script to `["desktop"]` — an overlay is portalled to `<body>` and its aria-hidden background is the same tree at either width.
65
+
66
+ **What is still outside this scope** — deliberately, and worth knowing before trusting a green run:
67
+
68
+ - Only the **first** declaration per frame is exercised. A frame with several distinct overlays (the Sheet demo has five) is measured on one of them.
69
+ - Only **one step deep**: submenus (`DropdownMenuSub`), an overlay opened _from inside_ another overlay, and the state _after_ a selection are not reached.
70
+ - **Keyboard-only opening** is not exercised — the gate clicks. A trigger that opens on click but not on `Enter`/`Space` would pass.
71
+ - The overlay's own **focus trap and focus order** are still not asserted by axe here (`focus-order-semantics` needs the focus to actually move); `src/components/__tests__/overlay-hidden-background.a11y.test.tsx` remains the DOM-level guard for the aria-hidden/inert contract, and it is what fails first if the `inert` mirror in `src/components/general/inert-background.ts` regresses in jsdom.
72
+ - Frames with **no declaration** (156 of 164) are unchanged: default state only.
28
73
 
29
74
  ## The component allowlist — and how it shrinks to zero
30
75
 
@@ -43,6 +88,10 @@ git add scripts/frame-axe.baseline.json && git commit -m "chore(a11y): tighten f
43
88
 
44
89
  There is **no blanket suppression**: a documented axe false positive would be an explicit per-rule entry with a comment, never a mute.
45
90
 
91
+ `baseline.overlay` is the same shape and obeys the same rule, for the overlay-open scope. It is only rewritten for frames whose open step **actually ran** — a frame that stopped declaring one has not been proven clean, it has been left unmeasured.
92
+
93
+ **Keys that are not violation data are preserved (#356).** The baseline also carries human notes — `scopeGap` records what the gate cannot see and which violations are deliberately unfixed. `--update-baseline` used to rebuild the file from scratch and delete them without a word. It now regenerates only the keys it owns (`generatedAt`, `note`, `component`, `overlay`), carries every other key over verbatim, and **names them in its log line** (`carried over: scopeGap`) so a disappearing key cannot go unnoticed.
94
+
46
95
  `0` chrome violations, `0` regressions. See [Remaining baseline debt](#remaining-baseline-debt--the-12-frames) below for exactly what's left and the path to zero.
47
96
 
48
97
  ## Running it locally
@@ -68,28 +117,47 @@ pnpm exec playwright install chromium # local dev — downloads Playwrigh
68
117
  | `pnpm check:frame-axe --format json` | Machine-readable result on stdout (same exit-code semantics) — no colored human report |
69
118
  | `pnpm check:frame-axe --update-baseline` | Regenerate the allowlist (2-pass union by default) — see below |
70
119
  | `AXE_RUNS=3 pnpm check:frame-axe --update-baseline` | More union passes, for a component with flaky portal timing |
120
+ | `AXE_SHARD=2/4 pnpm check:frame-axe` | Run only shard 2 of 4 (1-based) for CI fan-out — see below |
71
121
  | `pnpm check:frame-axe http://localhost:6008` | Point at an already-running preview instead of building+serving one |
72
122
 
123
+ #### Sharding the sweep (`AXE_SHARD=i/n`)
124
+
125
+ `AXE_SHARD=2/4` runs only the frames of shard 2 of 4. The partition is taken over frame ids sorted **by code point**, not in manifest/directory order, so the same tree always yields the same partition, every frame lands in exactly one shard, and the union of all `n` shards is exactly the full sweep. Unset (the default) leaves the frame list untouched — sharding changes nothing until you ask for it.
126
+
127
+ Two things are deliberately _not_ the same under a shard, because a shard cannot know them:
128
+
129
+ - **"Baseline rule no longer fires" (shrink hints) is skipped.** The baseline is keyed per frame, so _new_ rule detection stays exact on a partial run — but a frame this shard never visited looks identical to a frame that got fixed. Only a full sweep can tell those apart.
130
+ - **`--update-baseline` refuses to run** with `AXE_SHARD` set (exit 2). Writing the baseline from a fraction of the frames would delete every frame the shard did not visit.
131
+
132
+ `AXE_FRAMES_LIMIT` truncates the list instead of partitioning it; it is a local smoke-test knob, not a fan-out mechanism.
133
+
73
134
  If no preview server answers at the base URL (`http://localhost:6008` by default), the script builds one itself: `pnpm preview:build` then serves the static output with `vite preview` (`ensurePreviewServer` in `scripts/frame-harness.mjs`) — deterministic and stable under a long headless sweep, unlike the dev server's per-request recompilation. If a `pnpm preview` dev server (or a remote base) is already reachable, it's reused as-is and never rebuilt.
74
135
 
75
136
  ### Reading the output
76
137
 
77
- Terminal report (human mode) has two blocks:
138
+ Terminal report (human mode) has three blocks:
78
139
 
79
140
  ```
80
141
  Preview-chrome axe (blocking — must be 0):
81
- ✓ 0 chrome violations across all 107 frame(s).
142
+ ✓ 0 chrome violations across all 164 frame(s).
82
143
 
83
144
  Component/demo axe (allowlisted — baseline may only shrink):
84
- 47 component violation node(s) remaining across 12 frame(s).
145
+ 0 component violation node(s) remaining across 0 frame(s).
85
146
  ✓ no new component violation types (rule-set within baseline).
147
+
148
+ Overlay axe (frames declaring data-axe-open — baseline may only shrink):
149
+ 8 frame(s) opened an overlay before scanning; 128 violation node(s) while open.
150
+ ✓ no new overlay violation types (rule-set within baseline).
86
151
  ```
87
152
 
88
153
  - A **red `✗ <rule>`** under chrome is always blocking.
89
154
  - A **red `✗ N NEW violation-type(s) not in baseline`** under component means a frame gained a
90
155
  rule the baseline didn't have — that's the actual CI failure mode for a component regression.
156
+ - A **red `✗ N NEW violation-type(s) not in the overlay baseline`** is the same failure for the
157
+ overlay-open state — that is what turns red when, say, the `inert` background patch regresses
158
+ and `aria-hidden-focus` comes back.
91
159
  - A **yellow `↓ shrink hint`** means a baseline rule no longer fires anywhere it's listed — free
92
- baseline tightening available via `--update-baseline`.
160
+ baseline tightening available via `--update-baseline`. Not emitted under `AXE_SHARD`.
93
161
 
94
162
  ### Evidence JSON
95
163
 
@@ -103,19 +171,27 @@ Shape:
103
171
  "base": "http://localhost:6008",
104
172
  "summary": {
105
173
  "status": "ok", // "ok" | "fail"
106
- "frames": 107,
174
+ "frames": 164,
175
+ "shard": null, // "i/n" when AXE_SHARD is set
107
176
  "viewports": ["desktop", "mobile"],
108
177
  "chromeViolations": 0, // total chrome violation NODE count
109
- "componentViolations": 47, // total component violation NODE count (reporting only)
178
+ "componentViolations": 0, // total component violation NODE count (reporting only)
110
179
  "componentRegressions": 0, // rules present now but not in the baseline
180
+ "overlayFrames": 8, // frames whose declared open step ran
181
+ "overlayViolations": 128, // total NODE count measured with an overlay open
182
+ "overlayRegressions": 0, // overlay rules present now but not in baseline.overlay
111
183
  "infrastructureErrors": 0, // frames that failed to load / axe crashed
112
184
  },
113
185
  "chrome": {/* rule → { impact, nodes, frames: ["<frameId>@<viewport>", …] } */},
114
186
  "componentCurrent": {/* frameId → viewport → ruleId → node count, this run */},
115
187
  "regressions": [/* { frame, rule } — new rules, the actual gate failure */],
116
188
  "shrinkHints": [/* { frame, rule } — baseline rules that no longer fire */],
117
- "infraErrors": [/* { frame, viewport, message } */],
118
- "results": [/* per-frame { id, viewports: { desktop: { chrome, component }, mobile: {…} } } */],
189
+ "overlayCurrent": {/* frameId viewport → ruleId → node count, with the overlay OPEN */},
190
+ "overlayFrames": [/* frame ids whose declared open step ran */],
191
+ "overlayRegressions": [/* { frame, rule } — new rules in the overlay-open state */],
192
+ "overlayShrinkHints": [/* { frame, rule } */],
193
+ "infraErrors": [/* { frame, viewport, message } — includes "overlay open: …" failures */],
194
+ "results": [/* per-frame { id, viewports: { desktop: { chrome, component, overlay? }, … } } */],
119
195
  }
120
196
  ```
121
197
 
@@ -140,6 +216,8 @@ Playwright Inspector / browser devtools with the `axe-core` extension against th
140
216
  violation, not fixed an old one).
141
217
  4. Commit the baseline alongside the fix in the same PR.
142
218
 
219
+ `--update-baseline` also preserves every non-violation key in the file verbatim (see #356 above) and refuses to run under `AXE_SHARD`.
220
+
143
221
  `AXE_FRAMES_LIMIT` + `--update-baseline` together only touch the frames actually run — the merge
144
222
  logic in `check-frame-axe.mjs` overwrites `runFrameIds` and leaves every other frame's baseline
145
223
  entry untouched, so a scoped re-snapshot (e.g. after fixing just `Select`) never silently drops
@@ -258,10 +336,14 @@ Before opening a PR that adds or meaningfully changes a `/frame/**` example:
258
336
  8. If you do land a frame with a tracked violation, add it to `scripts/frame-axe.baseline.json` via
259
337
  `--update-baseline` (never hand-edit the JSON) and note _why_ + the follow-up issue in the PR
260
338
  description — the baseline file's own `note` field explains it may only shrink from here.
261
- 9. Register the case you authored in `preview/frame-coverage.ledger.json` so
262
- `check:frame-coverage-ledger` stops reporting that dimension `UNTESTED`. You **cannot** hand-write
263
- a verdict add an entry to the ledger's `cases` array (frame path + case heading + resolvable
264
- evidence paths + reviewer + HTTPS review link + ISO timestamp) and run
265
- `pnpm gen:frame-coverage-ledger`; the gate recomputes every cell from that evidence and rejects
266
- any verdict it cannot reproduce. See [FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
267
- [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
339
+ 9. **If the frame demos an overlay, declare its open step**: `data-axe-open` on the trigger (or on
340
+ the region that owns it when the component renders the trigger itself), `data-axe-open="contextmenu"`
341
+ for a context menu. Without it the frame is only ever measured closed, and the whole
342
+ `aria-hidden-focus` family of rules cannot fire see [the overlay scope](#the-overlay-scope-opening-the-thing-before-measuring-it-355).
343
+ 10. Register the case you authored in `preview/frame-coverage.ledger.json` so
344
+ `check:frame-coverage-ledger` stops reporting that dimension `UNTESTED`. You **cannot** hand-write
345
+ a verdict — add an entry to the ledger's `cases` array (frame path + case heading + resolvable
346
+ evidence paths + reviewer + HTTPS review link + ISO timestamp) and run
347
+ `pnpm gen:frame-coverage-ledger`; the gate recomputes every cell from that evidence and rejects
348
+ any verdict it cannot reproduce. See [FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
349
+ [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
@@ -3,8 +3,8 @@
3
3
  > Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
4
4
  > Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
5
5
 
6
- - Public components: **124**
7
- - With ≥1 frame: **124** · zero-frame: **0**
6
+ - Public components: **126**
7
+ - With ≥1 frame: **126** · zero-frame: **0**
8
8
  - Fully covered (every contract axis declared): **0**
9
9
 
10
10
  ## Per-component contract axes
@@ -19,6 +19,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
19
19
  | AppShell | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
20
20
  | AuthShell | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
21
21
  | CenteredShell | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
22
+ | MobileShell | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
22
23
  | Sidebar | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
23
24
  | Topbar | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
24
25
  | MasterDetail | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
@@ -47,6 +48,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
47
48
  | CodeBlock | ✓ | · | N/A | N/A | · | · | · | · | · | · |
48
49
  | Prose | ✓ | · | N/A | N/A | · | · | · | · | · | · |
49
50
  | Timeline | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
51
+ | TimelineGrid | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
50
52
  | Table | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
51
53
  | DataState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
52
54
  | InfiniteQueryState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
@@ -26,6 +26,27 @@ export default function Demo() {
26
26
  subtitle="leading · title/description · trailing action · short entity lists inside a Card"
27
27
  >
28
28
  <Flex direction="col" gap="lg">
29
+ <Card>
30
+ <CardHeader>
31
+ <CardTitle level={2}>Navigation rows</CardTitle>
32
+ <CardDescription>
33
+ The whole row is one link, including its metadata. Current location uses aria-current.
34
+ </CardDescription>
35
+ </CardHeader>
36
+ <CardContent flush>
37
+ <ListRow
38
+ asChild
39
+ title="Roles and permissions"
40
+ description="Inspect organization access"
41
+ overflow="wrap"
42
+ >
43
+ <a href="?section=roles" aria-current="page" />
44
+ </ListRow>
45
+ <ListRow asChild title="Members" description="Assign roles to organization members">
46
+ <a href="?section=members" />
47
+ </ListRow>
48
+ </CardContent>
49
+ </Card>
29
50
  <Card>
30
51
  <CardHeader>
31
52
  <CardTitle level={2}>アクティブなセッション Active sessions</CardTitle>
@@ -135,7 +135,10 @@ export default function Demo() {
135
135
  <Flex direction="row" wrap gap="sm">
136
136
  <Popover>
137
137
  <PopoverTrigger asChild>
138
- <Button variant="outline">side=top</Button>
138
+ {/* data-axe-open: opened by check-frame-axe before its overlay scan. */}
139
+ <Button data-axe-open variant="outline">
140
+ side=top
141
+ </Button>
139
142
  </PopoverTrigger>
140
143
  <PopoverContent side="top" aria-label="上側の配置例">
141
144
  <PopoverDescription>上に開きます。</PopoverDescription>
@@ -33,7 +33,8 @@ export default function Demo() {
33
33
  形式に整形され、 解釈できない入力は元の値へ復帰する。
34
34
  </CardDescription>
35
35
  </CardHeader>
36
- <CardContent>
36
+ {/* data-axe-open: check-frame-axe opens the calendar before scanning. */}
37
+ <CardContent data-axe-open>
37
38
  <FormField id="issue-date" label="発行日" required>
38
39
  <DatePicker
39
40
  id="issue-date"
@@ -130,7 +130,7 @@ export default function Demo() {
130
130
 
131
131
  <Card>
132
132
  <CardHeader>
133
- <CardTitle level={2}>Field identity · data-field (gh#337)</CardTitle>
133
+ <CardTitle level={2}>Field identity · data-field</CardTitle>
134
134
  <CardDescription>
135
135
  項目キーを control の data-field に出す。既定は id から導出。id が DOM
136
136
  一意化のためのもので項目キーと違う場合だけ field を明示する。AppProvider
@@ -176,7 +176,7 @@ export default function Demo() {
176
176
 
177
177
  <Card>
178
178
  <CardHeader>
179
- <CardTitle level={2}>Static value · mixed with editable fields (gh#294)</CardTitle>
179
+ <CardTitle level={2}>Static value · mixed with editable fields</CardTitle>
180
180
  <CardDescription>
181
181
  staticText renders a read-only VALUE as a FormField itself: Descriptions.Item's exact
182
182
  value typography, no control, no id/aria-* wiring. An immutable name/email row and an
@@ -7,6 +7,7 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
7
7
 
8
8
  /**
9
9
  * SearchInput — debounced search box with a built-in clear button.
10
+ * onSearch runs after a changed query settles, never on mount.
10
11
  * Always listen to onSearch (NOT onChange). Uncontrolled for local filters,
11
12
  * controlled when search state lives in a URL param. Never a raw <input>.
12
13
  * Composed only from real @godxjp/ui components.
@@ -142,7 +142,8 @@ export default function Demo() {
142
142
  references. The internal search textbox has its own visible-purpose accessible name.
143
143
  </CardDescription>
144
144
  </CardHeader>
145
- <CardContent>
145
+ {/* data-axe-open: check-frame-axe opens the searchable panel before scanning. */}
146
+ <CardContent data-axe-open>
146
147
  <FormField
147
148
  id="searchable-invalid"
148
149
  label="担当拠点"
@@ -82,7 +82,9 @@ export default function Demo() {
82
82
  Pass an options array; name= submits the value with the form.
83
83
  </CardDescription>
84
84
  </CardHeader>
85
- <CardContent>
85
+ {/* data-axe-open: check-frame-axe opens this Select before scanning, so the
86
+ overlay-open a11y state is measured. See docs/FRAME-A11Y-CI.md. */}
87
+ <CardContent data-axe-open>
86
88
  <FormField id="status" label="状態">
87
89
  <Select
88
90
  id="status"
@@ -109,7 +109,8 @@ export default function Demo() {
109
109
  <CardContent>
110
110
  <Dialog open={detailOpen} onOpenChange={setDetailOpen}>
111
111
  <DialogTrigger asChild>
112
- <Button variant="outline" size="sm">
112
+ {/* data-axe-open: opened by check-frame-axe before its overlay scan. */}
113
+ <Button data-axe-open variant="outline" size="sm">
113
114
  取引詳細を見る
114
115
  </Button>
115
116
  </DialogTrigger>
@@ -213,7 +213,8 @@ export default function Demo() {
213
213
  <CardContent>
214
214
  <Sheet open={editOpen} onOpenChange={setEditOpen}>
215
215
  <SheetTrigger asChild>
216
- <Button variant="ghost" size="sm">
216
+ {/* data-axe-open: opened by check-frame-axe before its overlay scan. */}
217
+ <Button data-axe-open variant="ghost" size="sm">
217
218
  取引先を編集
218
219
  </Button>
219
220
  </SheetTrigger>
@@ -10,3 +10,7 @@ For filter tabs / segmented toggles that show a per-option total, pass the `coun
10
10
  (`<Button variant="outline" count={18}>Chờ bay</Button>`) instead of nesting a `Badge` —
11
11
  it renders a borderless counter pill, formatted in the active locale and toned to the
12
12
  button variant, so an outline button never ends up with a doubled border.
13
+
14
+ For long collection labels, use `wrap` with `align="start"` and `fullWidth`. The button
15
+ grows vertically while retaining the size preset's minimum target height. Keep badges
16
+ and other row metadata outside the button. `align="end"` follows the document direction.
@@ -282,8 +282,8 @@ export default function Demo() {
282
282
  を切り替え、sidebar・topbar・page actions・grid・split pane
283
283
  の変換を同じ実用構成で確認する。900px 以下ではドックされたサイドバーが隠れ、
284
284
  AppShell 所有のモバイルドロワー(トップバー左のハンバーガー → フォーカストラップ付き
285
- Sheet · Esc で閉じてトリガーへフォーカス復帰)が navigation を提供する(gh#165)。
286
- このデモは mobileNav を渡していないため、同じ Sidebar がドロワーに再利用される。
285
+ Sheet · Esc で閉じてトリガーへフォーカス復帰)が navigation を提供する。 このデモは
286
+ mobileNav を渡していないため、同じ Sidebar がドロワーに再利用される。
287
287
  </CardDescription>
288
288
  <CardDescription>
289
289
  「トップバーなし(チャット型)」を押すと topbar / topbarLeft / topbarRight / logo の
@@ -297,8 +297,8 @@ export default function Demo() {
297
297
  で代用しないこと ― それは 4 スロットが埋まった状態と同じで、バーの行は残る。
298
298
  </CardDescription>
299
299
  <CardDescription>
300
- 横方向のページ余白軸の持ち主は <code>--space-page-x</code> ひとつ(gh#330)。
301
- トップバーは <code>.app-main</code> と同じグリッド列に載っている ―
300
+ 横方向のページ余白軸の持ち主は <code>--space-page-x</code> ひとつ。 トップバーは{" "}
301
+ <code>.app-main</code> と同じグリッド列に載っている ―
302
302
  つまりバー先頭のコントロールとページタイトルは同じ縦線に並ぶべきもので、
303
303
  <code>--app-shell-bar-inset</code> はその持ち主を読む (
304
304
  <code>--app-shell-bar-inset-compact</code> も同様に
@@ -15,6 +15,9 @@ import { Button, Text } from "@godxjp/ui/general";
15
15
  import { Flex, MasterDetail, PageContainer } from "@godxjp/ui/layout";
16
16
 
17
17
  /**
18
+ * Pass mobilePane="master" or "detail" for one-pane navigation below the token-owned collapse threshold.
19
+ * Supply detailBack with a native back link; own selection in URL/router history.
20
+ * Omit mobilePane to retain the original stacked composition.
18
21
  * MasterDetail · チーム画面 (SCR-110) の正準構成 — 流動的な一覧 + 固定幅の詳細レール。
19
22
  *
20
23
  * Shows: rail="detail" (既定 · 1fr / 320px) と rail="master" (先頭ナビゲーションレール)、
@@ -16,7 +16,7 @@ import { Flex, PageContainer, ResponsiveGrid, SplitPane } from "@godxjp/ui/layou
16
16
  * with no external container-type declaration. Direct children are typically
17
17
  * StatCard (self-contained bordered card · never wrap in Card/CardContent) or
18
18
  * Card+CardContent for richer tile bodies. columns accepts a number OR breakpoint
19
- * object { sm?, md?, lg? } — or use the named `preset` prop (e.g. "pricing-plans") for a
19
+ * object { base?, sm?, md?, lg? } — or use the named `preset` prop (e.g. "pricing-plans") for a
20
20
  * recognised collection shape instead of hand-rolling the breakpoint map. Composed only from
21
21
  * real @godxjp/ui components.
22
22
  */
@@ -27,6 +27,23 @@ export default function Demo() {
27
27
  subtitle="等幅タイルグリッド · KPI・カード比較・ダッシュボード行"
28
28
  >
29
29
  <Flex direction="col" gap="lg">
30
+ <Card>
31
+ <CardHeader>
32
+ <CardTitle level={2}>Compact mobile metrics</CardTitle>
33
+ <CardDescription>
34
+ columns=&#123;&#123; base: 2, sm: 4 &#125;&#125; keeps two columns below the first
35
+ container breakpoint and four above it.
36
+ </CardDescription>
37
+ </CardHeader>
38
+ <CardContent>
39
+ <ResponsiveGrid columns={{ base: 2, sm: 4 }}>
40
+ <StatCard label="Services" value="3" />
41
+ <StatCard label="Members" value="14" />
42
+ <StatCard label="Invitations" value="0" />
43
+ <StatCard label="Invoices" value="2" />
44
+ </ResponsiveGrid>
45
+ </CardContent>
46
+ </Card>
30
47
  <Card>
31
48
  <CardHeader>
32
49
  <CardTitle level={2}>コンテナクエリで折り返し(自己所有のクエリコンテナ)</CardTitle>
@@ -35,8 +52,8 @@ export default function Demo() {
35
52
  列、右の狭いサイドバー(約 20rem)は 1 列に折り返す。ビューポート幅は同一なので、
36
53
  折り返しの基準がコンテナ幅であることが分かる。ResponsiveGrid は自身のクエリコンテナ
37
54
  (container-type: inline-size)を持つため、外側に @container を用意しなくても正しく
38
- 折り返す(gh#165)。閾値はコンテナ幅 40 / 48 / 64rem。 列間の gap は
39
- var(--space-stack-md) に固定で、prop では変更できない。
55
+ 折り返す。閾値はコンテナ幅 40 / 48 / 64rem。 列間の gap は var(--space-stack-md)
56
+ が既定で、gap prop で変更できます。
40
57
  </CardDescription>
41
58
  </CardHeader>
42
59
  <CardContent>
@@ -72,7 +89,7 @@ export default function Demo() {
72
89
  <CardDescription>
73
90
  固定幅の親の中でも、同じ columns=&#123;4&#125; が親幅で列数を変える。親には
74
91
  container-type を付けていない。ResponsiveGrid が自身のスコープでクエリするため、
75
- 外側にコンテナ宣言がなくても 1 列に潰れない(gh#165)。
92
+ 外側にコンテナ宣言がなくても 1 列に潰れない。
76
93
  </CardDescription>
77
94
  </CardHeader>
78
95
  <CardContent>