@godxjp/ui 19.4.0 → 19.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-display/badge.d.ts +1 -1
- package/dist/components/data-display/popover.d.ts +1 -1
- package/dist/components/data-display/table.d.ts +1 -1
- package/dist/components/layout/mobile-shell.d.ts +1 -1
- package/dist/props/components/data-display.prop.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +2 -2
- package/dist/props/vocabulary/layout.prop.d.ts +1 -1
- package/docs/FRAME-A11Y-CI.md +99 -17
- package/docs/FRAME-COVERAGE-REPORT.md +4 -2
- package/docs/data-display/popover.tsx +4 -1
- package/docs/data-entry/date-picker.tsx +2 -1
- package/docs/data-entry/form-field/index.tsx +2 -2
- package/docs/data-entry/select-matrix.tsx +2 -1
- package/docs/data-entry/select.tsx +3 -1
- package/docs/feedback/dialog.tsx +2 -1
- package/docs/feedback/sheet.tsx +2 -1
- package/docs/layout/app-shell.tsx +4 -4
- package/docs/layout/responsive-grid.tsx +3 -3
- package/docs/layout/sidebar.tsx +3 -3
- package/docs/layout/split-pane.tsx +4 -5
- package/docs/navigation/context-menu.tsx +6 -1
- package/docs/navigation/dropdown-menu.tsx +2 -1
- package/docs/navigation/tabs.tsx +2 -2
- package/docs/showcase/case6-agency-handy.tsx +2 -2
- package/docs/showcase/org-switcher.tsx +1 -1
- package/docs/showcase/settings-account-sections.tsx +2 -2
- package/docs/showcase/table-master-detail.tsx +1 -1
- package/docs/showcase/table-view-tabs.tsx +1 -1
- package/package.json +2 -2
|
@@ -19,7 +19,7 @@ export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
|
|
|
19
19
|
* `<div>` would be invalid HTML: inside a `TabsTrigger`/`PopoverTrigger`/`Button` (all render a
|
|
20
20
|
* `<button>`, whose content model is phrasing content only), inside a `<label>` or a `<p>`.
|
|
21
21
|
* It swaps the TAG only — the chip's own `inline-flex` box, icon and label are unchanged, and
|
|
22
|
-
* both the icon `<svg>` and the label `<span>` are already phrasing content
|
|
22
|
+
* both the icon `<svg>` and the label `<span>` are already phrasing content.
|
|
23
23
|
*/
|
|
24
24
|
as?: "div" | "span";
|
|
25
25
|
/** Corner shape — `default` (badge radius) · `pill` (fully rounded) · `sharp` (square). */
|
|
@@ -9,7 +9,7 @@ export declare function PopoverAnchor(props: React.ComponentProps<typeof Popover
|
|
|
9
9
|
* edge and draw its own separators across the full width. The popover drops its own padding by
|
|
10
10
|
* zeroing `--popover-space-inset` ON THE PANEL, so the inset stays one token (a service that
|
|
11
11
|
* retunes `--popover-space-inset` still owns every padded popover) and no consumer has to reach
|
|
12
|
-
* for a zero-padding utility, which no service theme can reach
|
|
12
|
+
* for a zero-padding utility, which no service theme can reach.
|
|
13
13
|
*/
|
|
14
14
|
type PopoverContentFlush = {
|
|
15
15
|
flush?: FlushProp;
|
|
@@ -65,7 +65,7 @@ export declare const TableHead: React.ForwardRefExoticComponent<React.ThHTMLAttr
|
|
|
65
65
|
/**
|
|
66
66
|
* The cell's CONTENT owns its inset — an expanded detail panel, a nested table, a full-bleed media
|
|
67
67
|
* strip. The cell drops its own padding so the child reaches the cell edges; without it the only
|
|
68
|
-
* route was a zero-padding utility at the call site, which no service theme can reach
|
|
68
|
+
* route was a zero-padding utility at the call site, which no service theme can reach.
|
|
69
69
|
*/
|
|
70
70
|
type TableCellFlush = {
|
|
71
71
|
flush?: FlushProp;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MobileShellProp } from "../../props/components/layout.prop.js";
|
|
2
2
|
export type { MobileShellProp, MobileShellProp as MobileShellProps, } from "../../props/components/layout.prop.js";
|
|
3
3
|
/**
|
|
4
|
-
* MobileShell — the HANDHELD app shell
|
|
4
|
+
* MobileShell — the HANDHELD app shell: a status band, an app bar, ONE scroll region,
|
|
5
5
|
* a sticky action bar and a bottom tab bar, in that fixed order.
|
|
6
6
|
*
|
|
7
7
|
* The fourth root shell. The other three cannot express a phone app: `AppShell` REQUIRES a sidebar
|
|
@@ -140,7 +140,7 @@ export type BadgeProp = {
|
|
|
140
140
|
/**
|
|
141
141
|
* Render element — `div` (default) or `span` when the chip sits in a phrasing context where a
|
|
142
142
|
* `<div>` is invalid HTML (inside a `<button>` rendered by TabsTrigger/PopoverTrigger/Button,
|
|
143
|
-
* a `<label>`, a `<p>`). Swaps the tag only
|
|
143
|
+
* a `<label>`, a `<p>`). Swaps the tag only.
|
|
144
144
|
*/
|
|
145
145
|
as?: "div" | "span";
|
|
146
146
|
variant?: "default" | "secondary" | "outline";
|
|
@@ -107,7 +107,7 @@ export type FlexProp = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
107
107
|
* `<div>` would be invalid HTML: inside a `TabsTrigger`/`PopoverTrigger`/`Button` (all of which
|
|
108
108
|
* render a `<button>`, whose content model is phrasing content only), inside a `<label>`, or
|
|
109
109
|
* inside a `<p>`. Same closed shape as `ListRow`'s `as` — it swaps the TAG, nothing else: the
|
|
110
|
-
* `.ui-flex` rules carry `display: flex`, so the box is identical either way
|
|
110
|
+
* `.ui-flex` rules carry `display: flex`, so the box is identical either way.
|
|
111
111
|
*/
|
|
112
112
|
as?: "div" | "span";
|
|
113
113
|
direction?: FlexDirectionProp;
|
|
@@ -282,7 +282,7 @@ export type AuthShellProp = {
|
|
|
282
282
|
* `AppShell` REQUIRES a sidebar (its bar is a grid area beside the nav rail), `AuthShell` is the
|
|
283
283
|
* UNAUTHENTICATED root and centres a ~24rem card, and `CenteredShell` is a scrolling DOCUMENT —
|
|
284
284
|
* its `main` scrolls the page, which is exactly what a handheld app must not do. Composing one out
|
|
285
|
-
* of `Card` + `ui-card-inset*` (what docs/showcase/case6 did
|
|
285
|
+
* of `Card` + `ui-card-inset*` (what docs/showcase/case6 once did) reproduces the look
|
|
286
286
|
* and none of the two behaviours that matter on a real device:
|
|
287
287
|
*
|
|
288
288
|
* 1. The shell is the only scroll container. The root is exactly one screen tall, so the DOCUMENT
|
|
@@ -57,7 +57,7 @@ export type AuthShellPresetProp = "default" | "login" | "registration" | "device
|
|
|
57
57
|
* `"none"` is a DELIBERATE zero, not the absence of a value: two lines that read as ONE block —
|
|
58
58
|
* a name over its role, a weekday over its date, a tab bar with no seam between its triggers.
|
|
59
59
|
* Without it those stacks had to carry `gap="xs"`, which is a visual change forced by a missing
|
|
60
|
-
* step rather than by design
|
|
60
|
+
* step rather than by design.
|
|
61
61
|
*/
|
|
62
62
|
export type GapProp = "none" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
63
63
|
/** DataTable row density subset. */
|
package/docs/FRAME-A11Y-CI.md
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
142
|
+
✓ 0 chrome violations across all 164 frame(s).
|
|
82
143
|
|
|
83
144
|
Component/demo axe (allowlisted — baseline may only shrink):
|
|
84
|
-
|
|
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":
|
|
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":
|
|
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
|
-
"
|
|
118
|
-
"
|
|
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.
|
|
262
|
-
|
|
263
|
-
a
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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: **
|
|
7
|
-
- With ≥1 frame: **
|
|
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 | · | · | · | · | · | · |
|
|
@@ -135,7 +135,10 @@ export default function Demo() {
|
|
|
135
135
|
<Flex direction="row" wrap gap="sm">
|
|
136
136
|
<Popover>
|
|
137
137
|
<PopoverTrigger asChild>
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|
package/docs/feedback/dialog.tsx
CHANGED
|
@@ -109,7 +109,8 @@ export default function Demo() {
|
|
|
109
109
|
<CardContent>
|
|
110
110
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
|
111
111
|
<DialogTrigger asChild>
|
|
112
|
-
|
|
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>
|
package/docs/feedback/sheet.tsx
CHANGED
|
@@ -213,7 +213,8 @@ export default function Demo() {
|
|
|
213
213
|
<CardContent>
|
|
214
214
|
<Sheet open={editOpen} onOpenChange={setEditOpen}>
|
|
215
215
|
<SheetTrigger asChild>
|
|
216
|
-
|
|
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>
|
|
@@ -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
|
|
286
|
-
|
|
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>
|
|
301
|
-
|
|
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> も同様に
|
|
@@ -35,8 +35,8 @@ export default function Demo() {
|
|
|
35
35
|
列、右の狭いサイドバー(約 20rem)は 1 列に折り返す。ビューポート幅は同一なので、
|
|
36
36
|
折り返しの基準がコンテナ幅であることが分かる。ResponsiveGrid は自身のクエリコンテナ
|
|
37
37
|
(container-type: inline-size)を持つため、外側に @container を用意しなくても正しく
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
折り返す。閾値はコンテナ幅 40 / 48 / 64rem。 列間の gap は var(--space-stack-md)
|
|
39
|
+
に固定で、prop では変更できない。
|
|
40
40
|
</CardDescription>
|
|
41
41
|
</CardHeader>
|
|
42
42
|
<CardContent>
|
|
@@ -72,7 +72,7 @@ export default function Demo() {
|
|
|
72
72
|
<CardDescription>
|
|
73
73
|
固定幅の親の中でも、同じ columns={4} が親幅で列数を変える。親には
|
|
74
74
|
container-type を付けていない。ResponsiveGrid が自身のスコープでクエリするため、
|
|
75
|
-
外側にコンテナ宣言がなくても 1
|
|
75
|
+
外側にコンテナ宣言がなくても 1 列に潰れない。
|
|
76
76
|
</CardDescription>
|
|
77
77
|
</CardHeader>
|
|
78
78
|
<CardContent>
|
package/docs/layout/sidebar.tsx
CHANGED
|
@@ -402,7 +402,7 @@ export default function Demo() {
|
|
|
402
402
|
<CardDescription>
|
|
403
403
|
行の中身まで利用側が書く旧エスケープハッチ。これが原因で
|
|
404
404
|
<code><Link>{"{item.label}"}</Link></code>
|
|
405
|
-
|
|
405
|
+
がアイコンを全て落とす回帰が発生しました。 現在は rowProps.children
|
|
406
406
|
にライブラリ製の行内容が入るので、それを描画すれば 正規の行が復元されます(ここでは
|
|
407
407
|
その右にお気に入りスターを添えています)。 新規コードでは linkComponent か
|
|
408
408
|
SidebarItem asChild を使ってください。
|
|
@@ -436,7 +436,7 @@ export default function Demo() {
|
|
|
436
436
|
sections を使わず SidebarSection / SidebarItem を直接組み立てて、
|
|
437
437
|
ナビゲーション全体を 自前で構成します。右は同じ構成に SidebarItem の asChild
|
|
438
438
|
を足したものです。子として ルーターの Link
|
|
439
|
-
を「要素だけ」渡すと、アイコン・ラベル・バッジは
|
|
439
|
+
を「要素だけ」渡すと、アイコン・ラベル・バッジは ライブラリが差し込みます(。
|
|
440
440
|
children は書きません)。
|
|
441
441
|
</CardDescription>
|
|
442
442
|
</CardHeader>
|
|
@@ -590,7 +590,7 @@ export default function Demo() {
|
|
|
590
590
|
"badgeTone で未読(neutral)と自分宛て(destructive)を色だけで区別",
|
|
591
591
|
"disabled=true で項目を非活性化(クリック不可)",
|
|
592
592
|
"footer prop でスクロール外にユーザー情報を固定",
|
|
593
|
-
"linkComponent · ルーター Link
|
|
593
|
+
"linkComponent · ルーター Link は「要素だけ」渡す。行の中身はライブラリが組み立てる",
|
|
594
594
|
"linkComponent は葉・サブメニュー・折りたたみレール・フライアウトの全てに適用される",
|
|
595
595
|
"グループのトリガーは aria-expanded を持つ開閉ボタンのままなので linkComponent は適用されない",
|
|
596
596
|
].map((note) => (
|
|
@@ -302,8 +302,8 @@ export default function Demo() {
|
|
|
302
302
|
</Text>
|
|
303
303
|
<Text as="p" tone="muted">
|
|
304
304
|
パネル自身の幅が 48rem 未満になると縦積みにフォールバックします
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
(ビューポートではなくコンテナ基準)。 常にサイドバイサイドが必要な場合は CSS
|
|
306
|
+
Grid を使用してください。
|
|
307
307
|
</Text>
|
|
308
308
|
</Flex>
|
|
309
309
|
</CardContent>
|
|
@@ -410,9 +410,8 @@ export default function Demo() {
|
|
|
410
410
|
<CardHeader>
|
|
411
411
|
<CardTitle level={2}>Container stress · narrow vs wide embed</CardTitle>
|
|
412
412
|
<CardDescription>
|
|
413
|
-
大きな viewport
|
|
414
|
-
|
|
415
|
-
幅は同一。
|
|
413
|
+
大きな viewport 内でも、パネル自身の幅で分割が決まる。狭い埋め込み(約 28rem)は 1
|
|
414
|
+
カラムに潰れ、広い埋め込み(約 56rem)は 2 カラムに分割する。ビューポート 幅は同一。
|
|
416
415
|
</CardDescription>
|
|
417
416
|
</CardHeader>
|
|
418
417
|
<CardContent>
|
|
@@ -48,7 +48,12 @@ export default function Demo() {
|
|
|
48
48
|
<CardContent>
|
|
49
49
|
<ContextMenu>
|
|
50
50
|
{/* ui-audit-disable-next-line no-hand-rolled-surface — the dashed area is the right-click target the demo asks you to use */}
|
|
51
|
-
|
|
51
|
+
{/* data-axe-open="contextmenu": check-frame-axe right-clicks here before its
|
|
52
|
+
overlay scan. */}
|
|
53
|
+
<ContextMenuTrigger
|
|
54
|
+
data-axe-open="contextmenu"
|
|
55
|
+
className="border-border h-36 w-full cursor-context-menu rounded-md border border-dashed"
|
|
56
|
+
>
|
|
52
57
|
<Flex align="center" justify="center" gap="xs">
|
|
53
58
|
<Text tone="muted">ここを右クリックしてください</Text>
|
|
54
59
|
</Flex>
|
|
@@ -52,7 +52,8 @@ export default function Demo() {
|
|
|
52
52
|
<Text>JE-0042 · 売上計上 ¥480,000</Text>
|
|
53
53
|
<DropdownMenu>
|
|
54
54
|
<DropdownMenuTrigger asChild>
|
|
55
|
-
|
|
55
|
+
{/* data-axe-open: opened by check-frame-axe before its overlay scan. */}
|
|
56
|
+
<Button data-axe-open variant="ghost" size="icon-sm" aria-label="行アクション">
|
|
56
57
|
<MoreHorizontal />
|
|
57
58
|
</Button>
|
|
58
59
|
</DropdownMenuTrigger>
|
package/docs/navigation/tabs.tsx
CHANGED
|
@@ -73,7 +73,7 @@ export default function Demo() {
|
|
|
73
73
|
<CardTitle level={2}>先頭タブが disabled · フォールバック選択</CardTitle>
|
|
74
74
|
<CardDescription>
|
|
75
75
|
defaultValue/value を渡さない場合、Tabs は先頭の ENABLED タブを自動選択する(disabled
|
|
76
|
-
の先頭タブは選ばない)。全タブが disabled
|
|
76
|
+
の先頭タブは選ばない)。全タブが disabled の場合は何も選択しない。
|
|
77
77
|
</CardDescription>
|
|
78
78
|
</CardHeader>
|
|
79
79
|
<CardContent>
|
|
@@ -106,7 +106,7 @@ export default function Demo() {
|
|
|
106
106
|
<CardTitle level={2}>320px stress · 長いローカライズラベル</CardTitle>
|
|
107
107
|
<CardDescription>
|
|
108
108
|
狭いコンテナでも長いラベルはクリップされず、水平タブリストが自身でスクロールする
|
|
109
|
-
|
|
109
|
+
。compact navigation への変換は Tabs API に存在しないため擬似実装しない —
|
|
110
110
|
スクロールが意図した縮退動作。
|
|
111
111
|
</CardDescription>
|
|
112
112
|
</CardHeader>
|
|
@@ -333,7 +333,7 @@ function SectionHeader({ children, count }: { children: React.ReactNode; count?:
|
|
|
333
333
|
// ── Phone shell ──────────────────────────────────────────────────────────────
|
|
334
334
|
|
|
335
335
|
/**
|
|
336
|
-
*
|
|
336
|
+
* This file used to BUILD the shell: a `Card` + `CardContent flush` phone frame, a
|
|
337
337
|
* `ui-card-inset-x h-9` status row, an `h-14 border-b` header, a `flex-1 overflow-y-auto` body, a
|
|
338
338
|
* `shrink-0 border-t` action strip and a `ResponsiveGrid` tab bar. It reproduced the look and
|
|
339
339
|
* neither behaviour that matters on a device — the document still scrolled, and nothing padded out
|
|
@@ -962,7 +962,7 @@ export default function AgencyHandyShowcase() {
|
|
|
962
962
|
|
|
963
963
|
// SELECT MODE REPLACES THE APP BAR — it does not stack a second strip under it. That is the
|
|
964
964
|
// platform pattern on both iOS and Android, and it is what the `header` slot is for: one bar to
|
|
965
|
-
// read at a time.
|
|
965
|
+
// read at a time. This file once rendered the title bar AND a contextual strip, because
|
|
966
966
|
// there was no bar to swap.
|
|
967
967
|
const header = selectMode ? (
|
|
968
968
|
<Flex align="center" justify="between" gap="xs" className="w-full">
|
|
@@ -120,7 +120,7 @@ function OrgSwitcher({ organizations, currentId, onSelect, onCreate, onJoin }: O
|
|
|
120
120
|
className="w-full justify-between"
|
|
121
121
|
>
|
|
122
122
|
{/* Inside a Button, i.e. inside a <button>, whose content model is phrasing content
|
|
123
|
-
only — so every Flex on this branch renders as a <span
|
|
123
|
+
only — so every Flex on this branch renders as a <span>. */}
|
|
124
124
|
<Flex as="span" align="center" gap="sm" className="min-w-0">
|
|
125
125
|
<Avatar className="size-7 rounded-md">
|
|
126
126
|
<AvatarFallback>{monogram(current.name)}</AvatarFallback>
|
|
@@ -61,7 +61,7 @@ import { AlertDialog } from "@godxjp/ui/feedback";
|
|
|
61
61
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
62
62
|
|
|
63
63
|
// ── Label bundles: the SAME screen at its longest JA / EN / VI wording ────────────────────────
|
|
64
|
-
// Long-label stress is a first-class acceptance criterion
|
|
64
|
+
// Long-label stress is a first-class acceptance criterion, so the bundle intentionally
|
|
65
65
|
// carries the verbose form of each label instead of the tidy demo form.
|
|
66
66
|
|
|
67
67
|
type LabelBundle = {
|
|
@@ -348,7 +348,7 @@ export default function SettingsAccountSectionsShowcase() {
|
|
|
348
348
|
title={b.pageTitle}
|
|
349
349
|
subtitle={b.pageSubtitle}
|
|
350
350
|
breadcrumb={[{ label: b.pageTitle }]}
|
|
351
|
-
// PageHeader action composition
|
|
351
|
+
// PageHeader action composition: the header owns the page-level actions through
|
|
352
352
|
// `extra`. It wraps at 390 instead of overflowing, so no page-local header geometry.
|
|
353
353
|
extra={
|
|
354
354
|
<Flex direction="row" align="end" gap="sm" wrap>
|
|
@@ -417,7 +417,7 @@ export default function Demo() {
|
|
|
417
417
|
</Card>
|
|
418
418
|
|
|
419
419
|
{/* ── Empty detail state (nothing selected) ──
|
|
420
|
-
Fixed-track split, so the geometry belongs to MasterDetail, not this page
|
|
420
|
+
Fixed-track split, so the geometry belongs to MasterDetail, not this page:
|
|
421
421
|
rail="master" keeps the leading 一覧 on the fixed track while the detail surface stays
|
|
422
422
|
fluid, and collapseBelow="md" stacks 一覧→詳細 once the card body itself is too narrow
|
|
423
423
|
for two tracks. No grid tracks, no per-breakpoint widths, no page-local spacing. */}
|
|
@@ -369,7 +369,7 @@ const DOT_CLASS: Record<SavedView["dot"], string> = {
|
|
|
369
369
|
|
|
370
370
|
// TabsTrigger renders a <button>, whose content model is phrasing content only — so the row and
|
|
371
371
|
// the count chip take `as="span"`. Before Flex/Badge had that seam the only valid shape here was a
|
|
372
|
-
// raw `<span className="flex …">`, i.e. utility layout
|
|
372
|
+
// raw `<span className="flex …">`, i.e. utility layout.
|
|
373
373
|
function ViewTrigger({ view, count }: { view: SavedView; count: number }) {
|
|
374
374
|
return (
|
|
375
375
|
<Flex as="span" align="center" gap="sm">
|