@godxjp/ui 22.0.0 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/components/data-display/scroll-area.d.ts +24 -4
- package/dist/components/data-display/scroll-area.js +34 -72
- package/dist/components/data-entry/cascader.js +8 -6
- package/dist/components/data-entry/checkbox.js +2 -0
- package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
- package/dist/components/data-entry/choice-hit-target.js +12 -0
- package/dist/components/data-entry/control-surface.d.ts +8 -0
- package/dist/components/data-entry/control-surface.js +10 -1
- package/dist/components/data-entry/form-field.js +3 -2
- package/dist/components/data-entry/radio.js +3 -2
- package/dist/components/data-entry/search-select.js +225 -95
- package/dist/components/data-entry/select.d.ts +76 -14
- package/dist/components/data-entry/select.js +495 -163
- package/dist/components/data-entry/slider.d.ts +3 -11
- package/dist/components/data-entry/slider.js +582 -93
- package/dist/components/data-entry/switch.js +2 -0
- package/dist/components/navigation/dropdown-menu.d.ts +11 -3
- package/dist/components/navigation/dropdown-menu.js +190 -15
- package/dist/components/navigation/index.d.ts +0 -3
- package/dist/components/navigation/index.js +0 -78
- package/dist/components/ui/index.d.ts +0 -3
- package/dist/components/ui/index.js +0 -3
- package/dist/components/ui/input-otp.d.ts +1 -1
- package/dist/components/ui/segmented.js +3 -1
- package/dist/components/ui/tag-input.js +3 -5
- package/dist/lib/select-options.d.ts +21 -0
- package/dist/lib/select-options.js +46 -0
- package/dist/props/components/data-display.prop.d.ts +9 -6
- package/dist/props/components/data-entry.prop.d.ts +323 -30
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +17 -0
- package/dist/props/registry.d.ts +63 -0
- package/dist/props/registry.js +69 -0
- package/dist/styles/control.css +205 -23
- package/dist/styles/data-display-layout.css +18 -10
- package/dist/styles/focus-ring.css +2 -2
- package/dist/styles/fonts.css +88 -4
- package/dist/styles/navigation-layout.css +11 -236
- package/dist/styles/shell-layout.css +144 -139
- package/dist/styles/table-layout.css +7 -3
- package/dist/tokens/components/control.css +4 -0
- package/dist/tokens/components/data-display.css +0 -5
- package/dist/tokens/components/navigation.css +3 -5
- package/dist/tokens/components/scroll-area.css +7 -0
- package/docs/CUSTOMER-THEMING.md +7 -1
- package/docs/FRAME-COVERAGE-REPORT.md +4 -7
- package/docs/data-display/data-table/index.tsx +27 -1
- package/docs/data-display/popover.tsx +4 -6
- package/docs/data-display/scroll-area.tsx +51 -45
- package/docs/data-entry/date-picker.tsx +2 -3
- package/docs/data-entry/input-otp.tsx +5 -2
- package/docs/data-entry/segmented.tsx +40 -1
- package/docs/data-entry/select-matrix.tsx +1 -2
- package/docs/data-entry/select.tsx +108 -3
- package/docs/data-entry/slider.tsx +125 -47
- package/docs/feedback/dialog.tsx +1 -2
- package/docs/feedback/sheet.tsx +1 -2
- package/docs/layout/app-shell-states.tsx +143 -0
- package/docs/navigation/dropdown-menu.tsx +75 -4
- package/docs/roadmap/parity-audit-data-entry.md +44 -33
- package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
- package/docs/roadmap/parity-backlog.md +2 -3
- package/package.json +20 -44
- package/scripts/ui-audit.mjs +1 -1
- package/scripts/visual-audit-rules.mjs +0 -7
- package/scripts/visual-audit.mjs +6 -27
- package/dist/components/navigation/context-menu.d.ts +0 -21
- package/dist/components/navigation/context-menu.js +0 -149
- package/dist/components/navigation/menubar.d.ts +0 -28
- package/dist/components/navigation/menubar.js +0 -136
- package/dist/components/navigation/navigation-menu.d.ts +0 -10
- package/dist/components/navigation/navigation-menu.js +0 -91
- package/dist/components/ui/context-menu.d.ts +0 -1
- package/dist/components/ui/context-menu.js +0 -2
- package/dist/components/ui/menubar.d.ts +0 -1
- package/dist/components/ui/menubar.js +0 -2
- package/dist/components/ui/navigation-menu.d.ts +0 -1
- package/dist/components/ui/navigation-menu.js +0 -2
- package/docs/FRAME-A11Y-CI.md +0 -349
- package/docs/navigation/context-menu.tsx +0 -128
- package/docs/navigation/menubar.tsx +0 -141
- package/docs/navigation/navigation-menu.tsx +0 -158
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
|
5
|
-
import { ChevronDown } from "lucide-react";
|
|
6
|
-
import { cn } from "../../lib/utils.js";
|
|
7
|
-
const NavigationMenu = NavigationMenuPrimitive.Root;
|
|
8
|
-
const NavigationMenuList = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
9
|
-
NavigationMenuPrimitive.List,
|
|
10
|
-
{
|
|
11
|
-
ref,
|
|
12
|
-
"data-slot": "navigation-menu-list",
|
|
13
|
-
className: cn("ui-navigation-menu-list", className),
|
|
14
|
-
...props
|
|
15
|
-
}
|
|
16
|
-
));
|
|
17
|
-
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
|
18
|
-
const NavigationMenuItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
19
|
-
NavigationMenuPrimitive.Item,
|
|
20
|
-
{
|
|
21
|
-
ref,
|
|
22
|
-
"data-slot": "navigation-menu-item",
|
|
23
|
-
className: cn("ui-navigation-menu-item", className),
|
|
24
|
-
...props
|
|
25
|
-
}
|
|
26
|
-
));
|
|
27
|
-
NavigationMenuItem.displayName = NavigationMenuPrimitive.Item.displayName;
|
|
28
|
-
const NavigationMenuTrigger = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
29
|
-
NavigationMenuPrimitive.Trigger,
|
|
30
|
-
{
|
|
31
|
-
ref,
|
|
32
|
-
"data-slot": "navigation-menu-trigger",
|
|
33
|
-
className: cn("ui-navigation-menu-trigger", className),
|
|
34
|
-
...props,
|
|
35
|
-
children: [
|
|
36
|
-
children,
|
|
37
|
-
/* @__PURE__ */ jsx(ChevronDown, { className: "ui-navigation-menu-trigger-icon", "aria-hidden": "true" })
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
));
|
|
41
|
-
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
|
42
|
-
const NavigationMenuContent = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
43
|
-
NavigationMenuPrimitive.Content,
|
|
44
|
-
{
|
|
45
|
-
ref,
|
|
46
|
-
"data-slot": "navigation-menu-content",
|
|
47
|
-
className: cn("ui-navigation-menu-content", className),
|
|
48
|
-
...props
|
|
49
|
-
}
|
|
50
|
-
));
|
|
51
|
-
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
|
52
|
-
const NavigationMenuLink = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
53
|
-
NavigationMenuPrimitive.Link,
|
|
54
|
-
{
|
|
55
|
-
ref,
|
|
56
|
-
"data-slot": "navigation-menu-link",
|
|
57
|
-
className: cn("ui-navigation-menu-link", className),
|
|
58
|
-
...props
|
|
59
|
-
}
|
|
60
|
-
));
|
|
61
|
-
NavigationMenuLink.displayName = NavigationMenuPrimitive.Link.displayName;
|
|
62
|
-
const NavigationMenuIndicator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
63
|
-
NavigationMenuPrimitive.Indicator,
|
|
64
|
-
{
|
|
65
|
-
ref,
|
|
66
|
-
"data-slot": "navigation-menu-indicator",
|
|
67
|
-
className: cn("ui-navigation-menu-indicator", className),
|
|
68
|
-
...props
|
|
69
|
-
}
|
|
70
|
-
));
|
|
71
|
-
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
|
|
72
|
-
const NavigationMenuViewport = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
73
|
-
NavigationMenuPrimitive.Viewport,
|
|
74
|
-
{
|
|
75
|
-
ref,
|
|
76
|
-
"data-slot": "navigation-menu-viewport",
|
|
77
|
-
className: cn("ui-navigation-menu-viewport", className),
|
|
78
|
-
...props
|
|
79
|
-
}
|
|
80
|
-
));
|
|
81
|
-
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
|
|
82
|
-
export {
|
|
83
|
-
NavigationMenu,
|
|
84
|
-
NavigationMenuContent,
|
|
85
|
-
NavigationMenuIndicator,
|
|
86
|
-
NavigationMenuItem,
|
|
87
|
-
NavigationMenuLink,
|
|
88
|
-
NavigationMenuList,
|
|
89
|
-
NavigationMenuTrigger,
|
|
90
|
-
NavigationMenuViewport
|
|
91
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../navigation/context-menu.js";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../navigation/menubar.js";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../navigation/navigation-menu.js";
|
package/docs/FRAME-A11Y-CI.md
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
# Per-frame accessibility & coverage CI
|
|
2
|
-
|
|
3
|
-
They render every catalog frame in real Chromium and hold the line on accessibility, responsive geometry and contract coverage. Static checks (`audit:examples`, source regexes) cannot see rendered colour/layout/ARIA — these gates do.
|
|
4
|
-
|
|
5
|
-
## The three gates
|
|
6
|
-
|
|
7
|
-
| Script | `pnpm` | Drives | Blocking? |
|
|
8
|
-
| ----------------------------- | ---------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
9
|
-
| `scripts/check-frame-axe.mjs` | `check:frame-axe` | axe over **every frame** at desktop 1440×900 + mobile-sm 375×667 | **Preview-chrome: yes.** Component/demo: allowlisted (regression = fail) |
|
|
10
|
-
| `scripts/frame-geometry.mjs` | `check:frame-geometry` | overflow + clipped-control sweep across **320·375·390·768·1024·1280·1440·1920** | Allowlisted (regression = fail) |
|
|
11
|
-
| `scripts/frame-coverage.mjs` | `check:frame-coverage` | component-inventory ↔ frame ↔ contract-axis cross-check (browser-free) | Report-only (`--strict` fails on any zero-frame component) |
|
|
12
|
-
|
|
13
|
-
`check:frame-geometry` works identically for responsive overflow (see its own header comment); `check:frame-coverage` is covered briefly at the end and fully in [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
|
|
14
|
-
|
|
15
|
-
### What geometry counts as a "clipped control"
|
|
16
|
-
|
|
17
|
-
A control the **user cannot bring into the frame** — not merely one whose box currently sticks out. Everything inside a deliberately scrollable surface sticks out by design (a `DataTable`'s `.ui-data-table-scroll` at 320px, a `FilterBar overflow="scroll"` strip, a tabs list), and those controls are reachable: a control entirely out of the scrollport is scrolled fully in the moment it takes keyboard focus, and a partially visible one keeps its focus ring on screen (Chromium's `CenterIfNeeded` focus alignment does not move a partially visible target — browser behaviour, and still inside 2.4.7 / 2.4.11 AA). So an out-of-frame focusable is re-probed: the sweep scrolls **only** its user-scrollable ancestors (computed `overflow: auto|scroll|overlay` that actually has scroll room — never `hidden`/`clip`, which no user can scroll) and re-measures.
|
|
18
|
-
|
|
19
|
-
## Why a per-frame gate, not the old hand-picked checks
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
## Chrome vs component vs overlay: three independently-tracked scopes
|
|
24
|
-
|
|
25
|
-
`check:frame-axe` runs axe **twice** per frame/viewport, and a **third** time on the frames that declare an open step:
|
|
26
|
-
|
|
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.
|
|
73
|
-
|
|
74
|
-
## The component allowlist — and how it shrinks to zero
|
|
75
|
-
|
|
76
|
-
`scripts/frame-axe.baseline.json` is the allowlist of **pre-existing** component violations, keyed `frame → [axe-rule-id]` — the **set of violation types** still firing on each frame (union across desktop + mobile). We key on rule _presence_, not exact node counts, because axe counts jitter run-to-run as a Radix portal (Dialog/Popover/Sheet) opens or closes — an exact-count gate would be flaky. `--update-baseline` unions **two passes** by default (`AXE_RUNS`) so an intermittently-mounted overlay is captured.
|
|
77
|
-
|
|
78
|
-
- **fails** if a frame gains a rule **not** in its baseline set (a genuinely new violation type);
|
|
79
|
-
- **passes** rule sets equal to or a subset of baseline, printing exactly what remains (nothing hidden);
|
|
80
|
-
|
|
81
|
-
The baseline can **only shrink**.
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
git pull # get the merged component fixes
|
|
85
|
-
pnpm check:frame-axe --update-baseline # re-snapshot: rule sets only shrink
|
|
86
|
-
git add scripts/frame-axe.baseline.json && git commit -m "chore(a11y): tighten frame-axe baseline"
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
There is **no blanket suppression**: a documented axe false positive would be an explicit per-rule entry with a comment, never a mute.
|
|
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
|
-
|
|
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.
|
|
96
|
-
|
|
97
|
-
## Running it locally
|
|
98
|
-
|
|
99
|
-
### Prerequisites
|
|
100
|
-
|
|
101
|
-
`playwright` and `@axe-core/playwright` are **optional peers** — `check:frame-axe` detects a missing/unbuilt peer and skips with a warning instead of failing the build (so a browser-less environment, e.g. a docs-only contributor, never gets blocked). To actually run it:
|
|
102
|
-
|
|
103
|
-
```sh
|
|
104
|
-
pnpm install
|
|
105
|
-
pnpm exec playwright install chromium # local dev — downloads Playwright's managed Chromium
|
|
106
|
-
# CI uses: pnpm exec playwright install --with-deps chromium (adds OS-level deps)
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
`frame-harness.mjs` looks for a Chromium binary at `PLAYWRIGHT_CHROMIUM_EXECUTABLE`, defaulting to a **CI-only** path (`/opt/pw-browsers/chromium-1228/chrome-linux64/chrome`). That path won't exist on a laptop — when it's absent the harness falls back to Playwright's own managed browser (from `playwright install` above), so a plain local checkout works without setting anything.
|
|
110
|
-
|
|
111
|
-
### Commands
|
|
112
|
-
|
|
113
|
-
| Command | What it does |
|
|
114
|
-
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
115
|
-
| `pnpm check:frame-axe` | The gate: 1 pass over every frame × 2 viewports, compares to the baseline, exits non-zero on fail |
|
|
116
|
-
| `AXE_FRAMES_LIMIT=8 pnpm check:frame-axe` | Quick local smoke — only the first 8 frames (manifest order) |
|
|
117
|
-
| `pnpm check:frame-axe --format json` | Machine-readable result on stdout (same exit-code semantics) — no colored human report |
|
|
118
|
-
| `pnpm check:frame-axe --update-baseline` | Regenerate the allowlist (2-pass union by default) — see below |
|
|
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 |
|
|
121
|
-
| `pnpm check:frame-axe http://localhost:6008` | Point at an already-running preview instead of building+serving one |
|
|
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
|
-
|
|
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.
|
|
135
|
-
|
|
136
|
-
### Reading the output
|
|
137
|
-
|
|
138
|
-
Terminal report (human mode) has three blocks:
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
Preview-chrome axe (blocking — must be 0):
|
|
142
|
-
✓ 0 chrome violations across all 164 frame(s).
|
|
143
|
-
|
|
144
|
-
Component/demo axe (allowlisted — baseline may only shrink):
|
|
145
|
-
0 component violation node(s) remaining across 0 frame(s).
|
|
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).
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
- A **red `✗ <rule>`** under chrome is always blocking.
|
|
154
|
-
- A **red `✗ N NEW violation-type(s) not in baseline`** under component means a frame gained a
|
|
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.
|
|
159
|
-
- A **yellow `↓ shrink hint`** means a baseline rule no longer fires anywhere it's listed — free
|
|
160
|
-
baseline tightening available via `--update-baseline`. Not emitted under `AXE_SHARD`.
|
|
161
|
-
|
|
162
|
-
### Evidence JSON
|
|
163
|
-
|
|
164
|
-
Every non-`--update-baseline` run writes `audit-evidence/frame-axe/frame-axe-results.json`
|
|
165
|
-
(gitignored — a CI artifact, uploaded by `frame-a11y.yml` under the `frame-axe-evidence` name).
|
|
166
|
-
Shape:
|
|
167
|
-
|
|
168
|
-
```jsonc
|
|
169
|
-
{
|
|
170
|
-
"generatedAt": "2026-07-13T00:45:34.614Z",
|
|
171
|
-
"base": "http://localhost:6008",
|
|
172
|
-
"summary": {
|
|
173
|
-
"status": "ok", // "ok" | "fail"
|
|
174
|
-
"frames": 164,
|
|
175
|
-
"shard": null, // "i/n" when AXE_SHARD is set
|
|
176
|
-
"viewports": ["desktop", "mobile"],
|
|
177
|
-
"chromeViolations": 0, // total chrome violation NODE count
|
|
178
|
-
"componentViolations": 0, // total component violation NODE count (reporting only)
|
|
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
|
|
183
|
-
"infrastructureErrors": 0, // frames that failed to load / axe crashed
|
|
184
|
-
},
|
|
185
|
-
"chrome": {/* rule → { impact, nodes, frames: ["<frameId>@<viewport>", …] } */},
|
|
186
|
-
"componentCurrent": {/* frameId → viewport → ruleId → node count, this run */},
|
|
187
|
-
"regressions": [/* { frame, rule } — new rules, the actual gate failure */],
|
|
188
|
-
"shrinkHints": [/* { frame, rule } — baseline rules that no longer fire */],
|
|
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? }, … } } */],
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Node counts are recorded for triage but are **not** what the gate compares — the gate compares
|
|
199
|
-
rule _presence_ per frame (`currentRules` vs `baseline.component[frame]`), because node counts
|
|
200
|
-
jitter with portal open/close timing (see "The component allowlist" above). The evidence file does
|
|
201
|
-
**not** carry the underlying axe `nodes[].any[].data` (e.g. the exact contrast ratio or CSS
|
|
202
|
-
selector) — `scan()` in `check-frame-axe.mjs` only keeps `{ id, impact, nodes: v.nodes.length }`.
|
|
203
|
-
To see the raw axe payload for a specific violation while triaging, run `@axe-core/playwright`
|
|
204
|
-
directly against `http://localhost:6008/frame/<id>` in a small throwaway script, or use the
|
|
205
|
-
Playwright Inspector / browser devtools with the `axe-core` extension against the same URL.
|
|
206
|
-
|
|
207
|
-
### Regenerating / shrinking the baseline after a component fix
|
|
208
|
-
|
|
209
|
-
1. Fix the root cause in the component (or the docs demo, if the violation is demo-content-only —
|
|
210
|
-
see the categories below for which is which).
|
|
211
|
-
2. `pnpm check:frame-axe --update-baseline` — re-snapshots every frame's current rule set (2-pass
|
|
212
|
-
union). Frames that are now clean are **removed** from the JSON entirely; frames whose rule set
|
|
213
|
-
shrank keep only what still fires.
|
|
214
|
-
3. `git diff scripts/frame-axe.baseline.json` — confirm the diff is a **strict shrink** (no rule
|
|
215
|
-
should reappear on a frame that previously didn't have it — if one does, you introduced a new
|
|
216
|
-
violation, not fixed an old one).
|
|
217
|
-
4. Commit the baseline alongside the fix in the same PR.
|
|
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
|
-
|
|
221
|
-
`AXE_FRAMES_LIMIT` + `--update-baseline` together only touch the frames actually run — the merge
|
|
222
|
-
logic in `check-frame-axe.mjs` overwrites `runFrameIds` and leaves every other frame's baseline
|
|
223
|
-
entry untouched, so a scoped re-snapshot (e.g. after fixing just `Select`) never silently drops
|
|
224
|
-
unrelated frames from the allowlist.
|
|
225
|
-
|
|
226
|
-
## Violation categories & how each was root-caused (reference)
|
|
227
|
-
|
|
228
|
-
The original #157 audit's 101-frame baseline collapsed to 12 through structural fixes, not
|
|
229
|
-
whack-a-mole per-frame patches. Each category below is a pattern — recognize it the next time a
|
|
230
|
-
new frame trips the same rule.
|
|
231
|
-
|
|
232
|
-
| Category | axe rule(s) | Root cause | Fix pattern |
|
|
233
|
-
| ------------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
234
|
-
| **Heading order** | `heading-order` | Every docs demo nests `PageContainer`'s `<h1>` directly around sibling `<Card>` sections whose `CardTitle` defaulted to `level={3}` (`<h3>`) — skipping `<h2>` (91 frames). | AST codemod bumped every **top-level** `CardTitle` to `level={2}`; nested Card-in-Card titles correctly kept the `h3` default (`CardTitle`'s own jsdoc already documents this contract). Commit `030e82a`. |
|
|
235
|
-
| **Landmark — main** | `landmark-one-main`, `landmark-no-duplicate-main`, `landmark-main-is-top-level` | `preview/src/frame-main.tsx` hardcoded a `<main>` around every frame. A story that renders its own page shell (`AppShell`/`AuthShell`, which already emit their own `<main>`) got a nested/duplicate main. Story metadata can't disambiguate — every catalog entry defaults to `layout: "fullscreen"` regardless of content. | `FrameLandmark` (`preview/src/frame-main.tsx`) detects it **after mount**: `useLayoutEffect` checks `node.querySelector('main, [role="main"]')` — stays a plain `<div>` if the subtree already owns a main, otherwise renders the real `<main>` **tag** itself (not `role="main"` — several axe landmark checks walk ancestors by native tag name, not computed role). Commit `030e82a`, `281c8d4`. |
|
|
236
|
-
| **Landmark — banner (chrome)** | `landmark-no-duplicate-banner` | The demo toolbar was a candidate `<header>`, which resolves to an implicit `banner` landmark whenever the rendered story also provides its own (`AppShell` demos). | `demo-block.tsx`'s toolbar is a plain `<div className="demo-block-toolbar">`, never a `<header>` — it's preview chrome, not page banner content (see the comment right above it in the file). |
|
|
237
|
-
| **Landmark — uniqueness** | `landmark-unique` | `Pagination`, `Breadcrumb` (standalone + `PageContainer`'s built-in slot), and `Carousel` each hardcoded ONE localized `aria-label` with no override. Fine for a real screen (one instance); the component-gallery docs stack several instances of the same component on one page, so same-role + same-name landmarks become indistinguishable in a screen-reader landmark list (WCAG 2.4.1 / 1.3.1). | Added an `aria-label` override prop (`PaginationProp`, `BreadcrumbProps`, `PageContainer`'s `breadcrumbAriaLabel`); every repeated docs instance now passes a distinguishing label. Also fixed `PageContainer`'s breadcrumb slot to route its label through `t()` instead of a hardcoded English `"Breadcrumb"` literal (an i18n-contract bug found in the same pass). Commit `741ccf5`. |
|
|
238
|
-
| **Scrollable region focusable** | `scrollable-region-focusable` | An `overflow: auto`/`overflow-x: auto` container was in the tab order for mouse users but unreachable by keyboard (WCAG 2.1.1). Hit three places: `.demo-block-canvas`/`.demo-block-frame` (the preview harness's own scroll wrappers), and the `Table` primitive's/`DataTable`'s/`ScrollArea`'s overflow wrappers. | Add `tabIndex={0}` to the scrolling element — **no `role="region"`**: a landmark role there would break `landmark-*-is-top-level` for any story that renders its own regions (e.g. `AppShell` demos). See the inline comments in `demo-block.tsx` (`.demo-block-canvas`) and `src/components/data-display/table.tsx`. Commits `030e82a`, `8675570`. |
|
|
239
|
-
| **List semantics** | `listitem` | `docs/data-display/list-row.tsx` rendered two `<ListRow as="li">` siblings directly inside `CardContent` (a plain `<div>`) — axe requires an `<li>`'s parent to actually be a list container (`ul`/`ol`/`role="list"`). `ListRow`'s own jsdoc already documents `as="li"` for "when the rows are a semantic list" — the demo just hadn't supplied the wrapping list it implied. | Wrap the `<li>` rows in a real `<ul>` in the demo. Commit `30d22f1`. |
|
|
240
|
-
| **Accessible name — button** | `button-name` | A `<button>` (native or a `SelectTrigger`) with no discernible text (WCAG 4.1.2). | Case-by-case: give the control real text, an `aria-label`, or route it through `FormField` (see below). Remaining `button-name` entries all trace to the `SelectTrigger`/`DataSelect` prop-forwarding gap fixed by #177 — see [Remaining baseline debt](#remaining-baseline-debt--the-12-frames). |
|
|
241
|
-
| **Accessible name — form control / label** | (implicit via `FormField` wiring, WCAG 1.3.1/4.1.2) | A control inside `FormField` was named only via `aria-labelledby`, which some AT/axe paths don't resolve as reliably as a direct name. | `FormField` (`src/components/data-entry/form-field.tsx`) now **also** injects a redundant `aria-label` mirroring the visible label text (when it's plain text), alongside its existing `aria-labelledby` wiring — belt-and-suspenders, only when the child hasn't already set its own `aria-label`. Commit `8675570`. |
|
|
242
|
-
| **Accessible name — dialog** | `aria-dialog-name` / `aria-hidden-focus` | A `role="dialog"` (Popover in `modal` mode) needs a discernible name; a modal Popover **shown open at rest** in a demo (`Popover modal defaultOpen`) also puts Radix's `aria-hide` on everything outside the portal, which can hide the page's own `<main>`/`<h1>` from the accessibility tree scan. | Label the titled/modal Popover demos with `aria-label` on `PopoverContent`. The `aria-hidden-focus`/`landmark-one-main`/`page-has-heading-one` combination on an **open-at-rest modal demo** is the deferred exception — see below. Commit `8675570`. |
|
|
243
|
-
| **Color contrast** | `color-contrast` | `Calendar`'s selected-day ghost `<button>` kept dark text on the blue fill through hover/focus (insufficient contrast on the selected date). | Force `text-primary-foreground` on the day `<button>` through hover/focus states. Commit `8675570`. Some `color-contrast` entries remain on `text-muted-foreground` small text — tracked, not yet fixed (see below and [color-extensibility.md](./roadmap/color-extensibility.md)). |
|
|
244
|
-
| **Stray heading from empty state** | `heading-order` | `DataTable`'s built-in empty state rendered its "no rows" message as an `<h3>` (`EmptyState`'s default `titleAs`), injecting a heading into the outline for a state message, not a section. | `DataTable`'s empty state now renders `titleAs="p"` (plain text) — `EmptyState`/`CardTitle` both expose `titleLevel`/`titleAs` (`level`/`as` for `CardTitle`) precisely so callers can pick outline position without changing visual size. Commit `8675570`. |
|
|
245
|
-
|
|
246
|
-
## Remaining baseline debt — the 12 frames
|
|
247
|
-
|
|
248
|
-
As of `frame-axe.baseline.json` (`generatedAt: 2026-07-13`), 12 frames still carry an allowlisted
|
|
249
|
-
component violation. Grouped by root cause (per the tracking note in commit `8675570`):
|
|
250
|
-
|
|
251
|
-
| Frame(s) | Rule(s) | Why it's still open | Path to zero |
|
|
252
|
-
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
253
|
-
| `data-entry-select`, `data-entry-select-matrix`, `feedback-sheet`, `navigation-toolbar` | `button-name` (+ `feedback-sheet` also `color-contrast`) | All four demos use `Select`/`SelectTrigger`. `#177` (merged, commit `1ef05be`) shipped the `DataSelect` prop/aria-forwarding + `SelectTrigger showIndicator` fix this was tracked against, but the axe baseline was **not re-verified/re-snapshotted** against `main` after that PR merged. | Run `pnpm check:frame-axe --update-baseline` on current `main`; if `#177` closed the underlying gap, these 4 entries drop out automatically. If any remain, re-triage against the current `SelectTrigger`/`FormField` DOM. |
|
|
254
|
-
| `navigation-tabs` | `color-contrast` | `#176` (merged, commit `78c1c3b`) fixed `Tabs`' scrollable-overflow + disabled-fallback selection; the `color-contrast` entry here was tracked against that same PR but likewise **not re-verified** since. | `pnpm check:frame-axe --update-baseline` after confirming the fix; re-triage if it persists. |
|
|
255
|
-
| `data-entry-transfer`, `feedback-sheet` | `color-contrast` | Both demos use `text-muted-foreground` on small text (`transfer.tsx:83,101,121`, `sheet.tsx:226`). This is the same **default-theme contrast tradeoff** already tracked in [`docs/roadmap/color-extensibility.md`](./roadmap/color-extensibility.md) — resolving it is a brand-colour decision (darken the token or add a dedicated `--text-muted` variant), deliberately deferred pre-release rather than shipped as an unflagged palette shift. | Land the token decision in `color-extensibility.md`, then re-run `--update-baseline`. |
|
|
256
|
-
| `data-entry-calendar` | `landmark-unique` | `Calendar` wraps `react-day-picker` (`^10.0.1`), which renders its **own internal `<nav>`** for month navigation inside every `DayPicker` instance. The docs demo renders 4 `Calendar` instances on one page (each with a distinguishing container `aria-label`, but the _nested_ `nav` isn't currently overridable through this wrapper's `classNames` config) — 4 identical unnamed `nav` landmarks on one page. | Needs either upstream `react-day-picker` support for a per-instance nav label, or wrapping its nav slot with a custom, labelled replacement in `src/components/data-entry/calendar.tsx`. |
|
|
257
|
-
| `layout-split-pane` | `landmark-unique` | `SplitPane` (`src/components/layout/split-pane.tsx`) renders a bare `<aside className="ui-split-pane-aside">` with **no `aria-label` prop at all**. `docs/layout/split-pane.tsx` renders 4 `SplitPane` instances on one page → 4 identical unnamed `complementary` landmarks. | Add an `aria-label`/`asideAriaLabel` override prop to `SplitPane`, same pattern as `Pagination`/`Breadcrumb` (commit `741ccf5`), then give each docs instance a distinguishing label. |
|
|
258
|
-
| `data-display-popover` | `aria-hidden-focus`, `landmark-one-main`, `page-has-heading-one` | `docs/data-display/popover.tsx` (line ~198) renders `<Popover modal defaultOpen>` — a modal Popover **shown open at rest** so the demo illustrates the modal behavior without a click. Radix's modal mode `aria-hide`s everything outside the portal while open, which hides the frame's own `<main>`/`<h1>` from axe's accessibility-tree scan (and leaves focusable content behind the hidden boundary). | Either stop shipping this specific demo `defaultOpen` (illustrate modal-ness a different way, e.g. a screenshot/GIF or a "click to open" note) or accept it as a **known, deliberate** demo-only artifact of showing a Radix modal-trap mid-interaction and add a per-rule documented exception (not a blanket mute) once the maintainers decide which. |
|
|
259
|
-
| `layout-resizable-panel` | `scrollable-region-focusable` | `docs/layout/resizable-panel.tsx:72` — a demo panel with an inline `style={{ overflowY: "auto" }}`, added directly in the doc content, not through a primitive that already carries the `tabIndex={0}` fix. | Add `tabIndex={0}` to that panel's `<div>` in the docs demo (same fix pattern as `.demo-block-canvas`/`Table`, just not yet applied to this hand-rolled demo wrapper). |
|
|
260
|
-
| `navigation-breadcrumb` | `scrollable-region-focusable` | `docs/navigation/breadcrumb.tsx:89,95` — two `<pre className="overflow-x-auto">` code-snippet blocks rendered directly in the demo body (not the footer "View code" panel, which is disabled in frame mode). | Add `tabIndex={0}` to the `<pre>` elements in the docs demo. |
|
|
261
|
-
| `navigation-pagination` | `scrollable-region-focusable` | This one is a **real component gap**, not docs-only: `.ui-pagination-list` (`src/styles/layout.css`, the `#153`/`#165` no-wrap pagination CSS) sets `overflow-x: auto` so the page-number strip scrolls instead of wrapping to a second line — but `PaginationContent`'s `<ul>` (`src/components/navigation/pagination.tsx`) never received a `tabIndex={0}` to match. | Add `tabIndex={0}` to `PaginationContent`'s `<ul>` in `pagination.tsx` (mirrors the `Table`/`ScrollArea` fix), then `--update-baseline`. |
|
|
262
|
-
|
|
263
|
-
Overall rule histogram across the 12 frames: `button-name` ×4, `color-contrast` ×3,
|
|
264
|
-
`scrollable-region-focusable` ×3, `landmark-unique` ×2, `aria-hidden-focus` ×1,
|
|
265
|
-
`landmark-one-main` ×1, `page-has-heading-one` ×1 (the last three all on the same
|
|
266
|
-
`data-display-popover` frame).
|
|
267
|
-
|
|
268
|
-
**Fastest path to a fully green baseline:** re-run `pnpm check:frame-axe --update-baseline` on
|
|
269
|
-
current `main` first (it's plausible `#176`/`#177` already closed 5 of the 12 entries and nobody's
|
|
270
|
-
re-snapshotted since) — then tackle the remaining `SplitPane`/`Calendar` landmark props, the two
|
|
271
|
-
docs-only `tabIndex` additions, the one real `pagination.tsx` `tabIndex` fix, the muted-foreground
|
|
272
|
-
token decision, and finally decide the modal-Popover-at-rest exception.
|
|
273
|
-
|
|
274
|
-
## Infra frame modes (issue #163)
|
|
275
|
-
|
|
276
|
-
Any `/frame/<id>` accepts query params (implemented in `preview/src/frame-main.tsx`) so a route can
|
|
277
|
-
be exercised across contract axes without editing per-component example content:
|
|
278
|
-
|
|
279
|
-
| Param | Values | Effect |
|
|
280
|
-
| --------- | --------------------------------------- | --------------------------------------------------------- |
|
|
281
|
-
| `dir` | `ltr` \| `rtl` | flips logical CSS for the whole subtree (global RTL mode) |
|
|
282
|
-
| `density` | `compact` \| `default` \| `comfortable` | drives `AppProvider` density |
|
|
283
|
-
| `theme` | `light` \| `dark` | drives `AppProvider` theme |
|
|
284
|
-
| `locale` | `ja` \| `vi` \| `en` \| `ar` … | drives `AppProvider` locale (BCP-47) |
|
|
285
|
-
|
|
286
|
-
Example: `/frame/data-entry-select?dir=rtl&density=compact&theme=dark`.
|
|
287
|
-
|
|
288
|
-
`check:frame-axe` itself always runs the default chrome (`ltr`/`default`/`light`/`ja`) — these
|
|
289
|
-
params are for manual/exploratory testing (e.g. checking a component under RTL + dark in a real
|
|
290
|
-
browser), not currently swept by the automated gate.
|
|
291
|
-
|
|
292
|
-
## Coverage tracker
|
|
293
|
-
|
|
294
|
-
`check:frame-coverage` reads the public inventory (`mcp/src/data/components.ts`), the frames that
|
|
295
|
-
exist (`docs/` tsx), and the declared ledger (`preview/frame-coverage.ledger.json`), then emits
|
|
296
|
-
`docs/FRAME-COVERAGE-REPORT.md` + `audit-evidence/frame-coverage/coverage.json`. Every contract axis
|
|
297
|
-
is `covered` / `N/A:<reason>` / **`UNTESTED`** — a missing axis is never a silent pass. Those axes
|
|
298
|
-
are rolled up from the 14 contract dimensions the ledger tracks per export.
|
|
299
|
-
|
|
300
|
-
`check:frame-coverage-ledger` is the enforcing gate: it regenerates the ledger from the real public
|
|
301
|
-
surface, recomputes every dimension from the evidence, and fails on regression (coverage falling, a
|
|
302
|
-
new export with no frame, a weakened viewport matrix, a growing geometry/axe baseline). Dimension
|
|
303
|
-
totals climb toward full coverage (the #163 exit criterion). See
|
|
304
|
-
[FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
|
|
305
|
-
[FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
|
|
306
|
-
|
|
307
|
-
## Adding a new component/frame without regressing the gate
|
|
308
|
-
|
|
309
|
-
Before opening a PR that adds or meaningfully changes a `/frame/**` example:
|
|
310
|
-
|
|
311
|
-
1. **Give every top-level `Card` a `CardTitle level={2}`** when the frame sits directly under a
|
|
312
|
-
`PageContainer` (`<h1>`) — nested Card-in-Card titles keep the `h3` default. Skipping this
|
|
313
|
-
reintroduces `heading-order`.
|
|
314
|
-
2. **Don't hand-roll a scroll container.** If a demo needs `overflow: auto`/`overflow-x: auto`,
|
|
315
|
-
add `tabIndex={0}` to the scrolling element and do **not** add `role="region"` (that turns it
|
|
316
|
-
into a landmark and can trip `landmark-*-is-top-level` for stories with their own regions).
|
|
317
|
-
Prefer reusing `Table`/`DataTable`/`ScrollArea`, which already carry this.
|
|
318
|
-
3. **Stacking more than one instance of a landmark-owning component** (`Pagination`, `Breadcrumb`,
|
|
319
|
-
`SplitPane`'s `aside`, custom `<nav>`/`<aside>`/`<section aria-label>`) **on one page** — give
|
|
320
|
-
each instance a distinguishing `aria-label` (use the `aria-label`/`breadcrumbAriaLabel` override
|
|
321
|
-
props already on `Pagination`/`Breadcrumb`/`PageContainer` where they exist), or the frame trips
|
|
322
|
-
`landmark-unique`.
|
|
323
|
-
4. **`<li>` needs a real list parent.** `ListRow as="li"` (or any bare `<li>`) must be inside a
|
|
324
|
-
`<ul>`/`<ol>`/`role="list"` container, never a plain `<div>`.
|
|
325
|
-
5. **Every interactive control needs a discernible name.** Route form controls through `FormField`
|
|
326
|
-
(it injects both `aria-labelledby` and a redundant `aria-label`); give icon-only buttons/triggers
|
|
327
|
-
an explicit `aria-label`.
|
|
328
|
-
6. **A `role="dialog"` (Popover `modal`, custom dialogs) needs a name**, and avoid shipping a demo
|
|
329
|
-
with a **modal** dialog `defaultOpen`/open-at-rest unless you've checked what it hides from the
|
|
330
|
-
rest of the page (see the `data-display-popover` entry above) — prefer "click to open."
|
|
331
|
-
7. **Run the gate before pushing:** `pnpm exec playwright install chromium` (once) then
|
|
332
|
-
`AXE_FRAMES_LIMIT=<n> pnpm check:frame-axe` scoped to your new/changed frame(s), or the full
|
|
333
|
-
`pnpm check:frame-axe` if you have time. A brand-new frame with **zero** violations needs no
|
|
334
|
-
baseline entry at all — it's only added if you're knowingly landing a pre-existing violation
|
|
335
|
-
that a follow-up will fix (and even then, prefer fixing it before merge).
|
|
336
|
-
8. If you do land a frame with a tracked violation, add it to `scripts/frame-axe.baseline.json` via
|
|
337
|
-
`--update-baseline` (never hand-edit the JSON) and note _why_ + the follow-up issue in the PR
|
|
338
|
-
description — the baseline file's own `note` field explains it may only shrink from here.
|
|
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).
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
|
|
3
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
4
|
-
import { Text } from "@godxjp/ui/general";
|
|
5
|
-
import {
|
|
6
|
-
ContextMenu,
|
|
7
|
-
ContextMenuCheckboxItem,
|
|
8
|
-
ContextMenuContent,
|
|
9
|
-
ContextMenuItem,
|
|
10
|
-
ContextMenuLabel,
|
|
11
|
-
ContextMenuRadioGroup,
|
|
12
|
-
ContextMenuRadioItem,
|
|
13
|
-
ContextMenuSeparator,
|
|
14
|
-
ContextMenuShortcut,
|
|
15
|
-
ContextMenuSub,
|
|
16
|
-
ContextMenuSubContent,
|
|
17
|
-
ContextMenuSubTrigger,
|
|
18
|
-
ContextMenuTrigger,
|
|
19
|
-
} from "@godxjp/ui/navigation";
|
|
20
|
-
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* ContextMenu — right-click action menu over a target area. Compose
|
|
24
|
-
* ContextMenu > ContextMenuTrigger + ContextMenuContent > items. Supports
|
|
25
|
-
* sub-menus, checkbox/radio items, separators, and keyboard shortcuts.
|
|
26
|
-
* Composed only from real @godxjp/ui components.
|
|
27
|
-
*/
|
|
28
|
-
export default function Demo() {
|
|
29
|
-
const [showRuby, setShowRuby] = useState(true);
|
|
30
|
-
const [showGrid, setShowGrid] = useState(false);
|
|
31
|
-
const [sort, setSort] = useState("date");
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<PageContainer
|
|
35
|
-
title="ContextMenu"
|
|
36
|
-
subtitle="右クリックで開くコンテキストメニュー · 行・カード・エリアへのアクション"
|
|
37
|
-
>
|
|
38
|
-
<Flex direction="col" gap="lg">
|
|
39
|
-
<Card>
|
|
40
|
-
<CardHeader>
|
|
41
|
-
<CardTitle level={2}>基本アクション</CardTitle>
|
|
42
|
-
<CardDescription>
|
|
43
|
-
領域を右クリックするとメニューが開きます。サブメニュー・区切り・ショートカットを含む。
|
|
44
|
-
ContextMenuItem の variant は default と destructive の 2 つ。破棄系のアクションだけを
|
|
45
|
-
destructive にする。
|
|
46
|
-
</CardDescription>
|
|
47
|
-
</CardHeader>
|
|
48
|
-
<CardContent>
|
|
49
|
-
<ContextMenu>
|
|
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
|
-
{/* 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
|
-
>
|
|
57
|
-
<Flex align="center" justify="center" gap="xs">
|
|
58
|
-
<Text tone="muted">ここを右クリックしてください</Text>
|
|
59
|
-
</Flex>
|
|
60
|
-
</ContextMenuTrigger>
|
|
61
|
-
<ContextMenuContent>
|
|
62
|
-
<ContextMenuLabel>仕訳操作</ContextMenuLabel>
|
|
63
|
-
<ContextMenuItem>
|
|
64
|
-
編集
|
|
65
|
-
<ContextMenuShortcut>⌘E</ContextMenuShortcut>
|
|
66
|
-
</ContextMenuItem>
|
|
67
|
-
<ContextMenuItem>
|
|
68
|
-
複製
|
|
69
|
-
<ContextMenuShortcut>⌘D</ContextMenuShortcut>
|
|
70
|
-
</ContextMenuItem>
|
|
71
|
-
<ContextMenuSeparator />
|
|
72
|
-
<ContextMenuSub>
|
|
73
|
-
<ContextMenuSubTrigger>エクスポート</ContextMenuSubTrigger>
|
|
74
|
-
<ContextMenuSubContent>
|
|
75
|
-
<ContextMenuItem>CSV</ContextMenuItem>
|
|
76
|
-
<ContextMenuItem>PDF</ContextMenuItem>
|
|
77
|
-
<ContextMenuItem>Excel</ContextMenuItem>
|
|
78
|
-
</ContextMenuSubContent>
|
|
79
|
-
</ContextMenuSub>
|
|
80
|
-
<ContextMenuSeparator />
|
|
81
|
-
<ContextMenuItem variant="default">アーカイブに移動</ContextMenuItem>
|
|
82
|
-
<ContextMenuItem variant="destructive">
|
|
83
|
-
削除
|
|
84
|
-
<ContextMenuShortcut>⌫</ContextMenuShortcut>
|
|
85
|
-
</ContextMenuItem>
|
|
86
|
-
</ContextMenuContent>
|
|
87
|
-
</ContextMenu>
|
|
88
|
-
</CardContent>
|
|
89
|
-
</Card>
|
|
90
|
-
|
|
91
|
-
<Card>
|
|
92
|
-
<CardHeader>
|
|
93
|
-
<CardTitle level={2}>チェックボックス・ラジオアイテム</CardTitle>
|
|
94
|
-
<CardDescription>
|
|
95
|
-
表示設定の切り替えに使用。CheckboxItem は個別トグル、RadioGroup は排他選択。
|
|
96
|
-
</CardDescription>
|
|
97
|
-
</CardHeader>
|
|
98
|
-
<CardContent>
|
|
99
|
-
<ContextMenu>
|
|
100
|
-
{/* ui-audit-disable-next-line no-hand-rolled-surface — the dashed area is the right-click target the demo asks you to use */}
|
|
101
|
-
<ContextMenuTrigger className="border-border h-36 w-full cursor-context-menu rounded-md border border-dashed">
|
|
102
|
-
<Flex align="center" justify="center" gap="xs">
|
|
103
|
-
<Text tone="muted">右クリック · 表示設定</Text>
|
|
104
|
-
</Flex>
|
|
105
|
-
</ContextMenuTrigger>
|
|
106
|
-
<ContextMenuContent>
|
|
107
|
-
<ContextMenuLabel>表示オプション</ContextMenuLabel>
|
|
108
|
-
<ContextMenuCheckboxItem checked={showRuby} onCheckedChange={setShowRuby}>
|
|
109
|
-
ルビ表示
|
|
110
|
-
</ContextMenuCheckboxItem>
|
|
111
|
-
<ContextMenuCheckboxItem checked={showGrid} onCheckedChange={setShowGrid}>
|
|
112
|
-
グリッド線
|
|
113
|
-
</ContextMenuCheckboxItem>
|
|
114
|
-
<ContextMenuSeparator />
|
|
115
|
-
<ContextMenuLabel>並び順</ContextMenuLabel>
|
|
116
|
-
<ContextMenuRadioGroup value={sort} onValueChange={setSort}>
|
|
117
|
-
<ContextMenuRadioItem value="date">日付順</ContextMenuRadioItem>
|
|
118
|
-
<ContextMenuRadioItem value="amount">金額順</ContextMenuRadioItem>
|
|
119
|
-
<ContextMenuRadioItem value="account">科目順</ContextMenuRadioItem>
|
|
120
|
-
</ContextMenuRadioGroup>
|
|
121
|
-
</ContextMenuContent>
|
|
122
|
-
</ContextMenu>
|
|
123
|
-
</CardContent>
|
|
124
|
-
</Card>
|
|
125
|
-
</Flex>
|
|
126
|
-
</PageContainer>
|
|
127
|
-
);
|
|
128
|
-
}
|