@godxjp/ui 25.2.0 → 25.4.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/dist/components/data-entry/calendar.js +1 -1
- package/dist/components/data-entry/date-picker.js +2 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/data-entry.prop.d.ts +10 -2
- package/dist/styles/control.css +10 -6
- package/dist/styles/shell-layout.css +4 -1
- package/dist/tokens/components/control.css +2 -0
- package/dist/tokens/components/shell.css +2 -1
- package/docs/DEVELOPMENT.md +2 -2
- package/docs/FRAME-A11Y-CI.md +39 -28
- package/docs/README.md +1 -1
- package/docs/data-entry/calendar.tsx +46 -0
- package/docs/data-entry/date-picker.tsx +30 -0
- package/docs/layout/app-shell-states.tsx +20 -2
- package/docs/showcase/case5-shift-calendar.tsx +9 -5
- package/package.json +4 -3
- package/scripts/ui-audit.mjs +53 -19
|
@@ -49,6 +49,7 @@ function DatePicker(props) {
|
|
|
49
49
|
toDate,
|
|
50
50
|
disabledDate,
|
|
51
51
|
cellRender,
|
|
52
|
+
bordered,
|
|
52
53
|
allowClear,
|
|
53
54
|
triggerLabel,
|
|
54
55
|
format: formatProp,
|
|
@@ -381,6 +382,7 @@ function DatePicker(props) {
|
|
|
381
382
|
onMonthChange: setViewAnchor,
|
|
382
383
|
locale: dayPickerLocale,
|
|
383
384
|
cellRender,
|
|
385
|
+
bordered,
|
|
384
386
|
showWeekNumber: showWeek,
|
|
385
387
|
startMonth: minimum,
|
|
386
388
|
endMonth: maximum,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.4.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -725,8 +725,10 @@ export type CalendarProp = DayPickerProps & CalendarFooterProp & {
|
|
|
725
725
|
* What a month grid needs is the ruling BETWEEN days, so a week reads as a row of cells the
|
|
726
726
|
* eye can track across.
|
|
727
727
|
*
|
|
728
|
-
* Default `
|
|
729
|
-
*
|
|
728
|
+
* Default `true` — the owner's call, from a real screen: with no ruling the month read as a
|
|
729
|
+
* cloud of numbers and was "very hard to read". Pass `bordered={false}` for the floating-button
|
|
730
|
+
* look. The line colour is `--calendar-grid-border-color` (default `--input`, the ≥3:1 tier;
|
|
731
|
+
* the decorative `--border` measured 1.15:1 and was invisible as a 1px ruling).
|
|
730
732
|
*/
|
|
731
733
|
bordered?: boolean;
|
|
732
734
|
/** Decorate a day cell — 祝日, a booked day, a deadline. @see CalendarCellRenderProp */
|
|
@@ -861,6 +863,12 @@ export type DatePickerBaseProp = FieldA11yProps & PickerChromeProp & {
|
|
|
861
863
|
toDate?: Date;
|
|
862
864
|
/** Decorate a day cell — 祝日, a booked day, a deadline. @see CalendarCellRenderProp */
|
|
863
865
|
cellRender?: CalendarCellRenderProp;
|
|
866
|
+
/**
|
|
867
|
+
* Rule the day grid in the panel — forwarded to `Calendar bordered`, and defaults to `true`
|
|
868
|
+
* like it. Applies to the DAY grid (`picker="date"` / `"week"`, single, `multiple` and `range`);
|
|
869
|
+
* the month / quarter / year period grid has no day cells to rule and ignores it.
|
|
870
|
+
*/
|
|
871
|
+
bordered?: boolean;
|
|
864
872
|
/**
|
|
865
873
|
* Forbid individual dates by predicate — the rule `minDate`/`maxDate` cannot express, because a
|
|
866
874
|
* business calendar is rarely one contiguous range: 土日, a closed accounting period, a 祝日, a
|
package/dist/styles/control.css
CHANGED
|
@@ -2180,13 +2180,13 @@
|
|
|
2180
2180
|
|
|
2181
2181
|
.ui-calendar[data-bordered="true"] .ui-calendar-day,
|
|
2182
2182
|
.ui-calendar[data-bordered="true"] .ui-calendar-weekday {
|
|
2183
|
-
border-inline-start: 1px solid hsl(var(--
|
|
2184
|
-
border-block-start: 1px solid hsl(var(--
|
|
2183
|
+
border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
|
|
2184
|
+
border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
|
|
2185
2185
|
}
|
|
2186
2186
|
|
|
2187
2187
|
.ui-calendar[data-bordered="true"] .ui-calendar-week,
|
|
2188
2188
|
.ui-calendar[data-bordered="true"] .ui-calendar-weekdays {
|
|
2189
|
-
border-inline-end: 1px solid hsl(var(--
|
|
2189
|
+
border-inline-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
|
|
2190
2190
|
}
|
|
2191
2191
|
|
|
2192
2192
|
.ui-calendar[data-bordered="true"] .ui-calendar-weekday {
|
|
@@ -2198,7 +2198,7 @@
|
|
|
2198
2198
|
}
|
|
2199
2199
|
|
|
2200
2200
|
.ui-calendar[data-bordered="true"] .ui-calendar-grid {
|
|
2201
|
-
border-block-end: 1px solid hsl(var(--
|
|
2201
|
+
border-block-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
|
|
2202
2202
|
}
|
|
2203
2203
|
|
|
2204
2204
|
.ui-calendar[data-bordered="true"] .ui-calendar-day.invisible {
|
|
@@ -2219,8 +2219,12 @@
|
|
|
2219
2219
|
|
|
2220
2220
|
.ui-calendar[data-bordered="true"] .ui-calendar-week-number,
|
|
2221
2221
|
.ui-calendar[data-bordered="true"] .ui-calendar-week-number-header {
|
|
2222
|
-
border-inline-start: 1px solid hsl(var(--
|
|
2223
|
-
border-block-start: 1px solid hsl(var(--
|
|
2222
|
+
border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
|
|
2223
|
+
border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.ui-calendar[data-bordered="true"] .ui-calendar-day {
|
|
2227
|
+
inline-size: var(--control-height);
|
|
2224
2228
|
}
|
|
2225
2229
|
|
|
2226
2230
|
.ui-calendar[data-bordered="true"] .ui-calendar-day-button {
|
|
@@ -285,7 +285,10 @@
|
|
|
285
285
|
|
|
286
286
|
.app-main .ui-page-container {
|
|
287
287
|
width: 100%;
|
|
288
|
-
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.app-main :where(.ui-page-header, .ui-page-toolbar, .ui-page-body) {
|
|
291
|
+
max-inline-size: var(--app-shell-page-max-width);
|
|
289
292
|
}
|
|
290
293
|
|
|
291
294
|
.ui-auth-shell {
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
--app-shell-nav-rail-height: var(--app-shell-nav-rail-width);
|
|
54
54
|
|
|
55
55
|
--app-shell-nav-rail-inset: var(--space-3);
|
|
56
|
-
|
|
56
|
+
|
|
57
|
+
--app-shell-page-max-width: none;
|
|
57
58
|
|
|
58
59
|
--app-shell-main-background: initial;
|
|
59
60
|
--app-shell-mobile-nav-width: 22.5rem;
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -122,7 +122,7 @@ pnpm test # FULL suite — CI only, never from an agent loop
|
|
|
122
122
|
pnpm preview:build # integration test: examples + docs must build — at most once, pre-PR
|
|
123
123
|
pnpm audit # godxjp-ui-audit — 0 errors for touched files
|
|
124
124
|
pnpm check:mcp-sync # MCP registry ↔ library export drift guard
|
|
125
|
-
pnpm check:frame-axe # WCAG 2.2 AA over every frame —
|
|
125
|
+
pnpm check:frame-axe # WCAG 2.2 AA over every frame — LOCAL ONLY, never in CI (FRAME-A11Y-CI.md)
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
`pnpm verify` and `pnpm verify:release` run these together (verify:release also builds) — **both include the full suite, so both belong to CI.** Locally, run them at most once immediately before opening a PR, never inside an edit loop and never while other agents are working on the same machine. It needs `pnpm exec playwright install chromium` once locally; see [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) for how to run/scope it, read the evidence, and regenerate its baseline after an accessibility fix.
|
|
@@ -190,5 +190,5 @@ npm versions are immutable, so you can never "fix" a bad published version in pl
|
|
|
190
190
|
- [README](../README.md) — overview, component groups, consumer setup.
|
|
191
191
|
- [CONSUMER-RULES.md](./CONSUMER-RULES.md) — the ten rules every consumer follows (the audit enforces them).
|
|
192
192
|
- `docs/TOKENS.md`, `docs/SPACING.md`, `docs/PROPS-VOCABULARY.md`, `docs/PROPS-REGISTRY.md`.
|
|
193
|
-
- [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y + geometry + coverage
|
|
193
|
+
- [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y (**local only, never in CI**) + geometry + coverage gates.
|
|
194
194
|
- **`godx-ui-component-placement`** skill — decide whether a component belongs here.
|
package/docs/FRAME-A11Y-CI.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
# Per-frame accessibility
|
|
1
|
+
# Per-frame accessibility — `check:frame-axe` (LOCAL ONLY)
|
|
2
2
|
|
|
3
|
-
One ruler, run in the repository that owns the CSS
|
|
3
|
+
One ruler, run in the repository that owns the CSS — **on a developer machine, never in CI/CD.**
|
|
4
|
+
|
|
5
|
+
> **Owner's standing rule (2026-09-17): axe must not run in any GitHub Actions workflow.** It is a
|
|
6
|
+
> local measurement only. gh#643 had wired it into `ci-browser.yml` (four shards on every merge) and
|
|
7
|
+
> `ci-browser-full.yml` (nightly); both jobs are removed. `check:gate-coverage` lists
|
|
8
|
+
> `check:frame-axe` as EXEMPT with this reason, so its absence from the workflows is a declaration,
|
|
9
|
+
> not a dead gate. **Do not re-wire it into a workflow.** The file keeps its historical name so
|
|
10
|
+
> existing links still resolve.
|
|
4
11
|
|
|
5
12
|
## Why it exists (gh#643)
|
|
6
13
|
|
|
@@ -234,41 +241,45 @@ which changes what `color-contrast` resolves a background to). Two sweeps after
|
|
|
234
241
|
|
|
235
242
|
## Cost
|
|
236
243
|
|
|
237
|
-
| |
|
|
238
|
-
| ------------------------------------- |
|
|
239
|
-
| full sweep | **
|
|
240
|
-
| sequential | ~55 min — the three viewport passes run concurrently, which is the whole difference
|
|
241
|
-
| showcase only (`--scope=showcase`) | 30s
|
|
242
|
-
|
|
|
244
|
+
| | |
|
|
245
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
246
|
+
| full sweep | **2m14s** locally, with the overlay scope — 215 routes × 3 viewports |
|
|
247
|
+
| sequential | ~55 min — the three viewport passes run concurrently, which is the whole difference |
|
|
248
|
+
| showcase only (`--scope=showcase`) | ~30s |
|
|
249
|
+
| one route (`pnpm check:frame-axe /isolate/…`) | seconds — the loop to use while fixing |
|
|
243
250
|
|
|
244
251
|
The overlay pass is bounded by declaration: only the 7 declaring routes pay it (21 extra scans plus
|
|
245
|
-
their open steps), and no `/showcase/**` route declares one, so `--scope=showcase`
|
|
246
|
-
|
|
252
|
+
their open steps), and no `/showcase/**` route declares one, so `--scope=showcase` is unchanged by it.
|
|
253
|
+
|
|
254
|
+
## Where it runs: your machine
|
|
247
255
|
|
|
248
|
-
|
|
249
|
-
names, which costs nothing here because none of them is in `REQUIRED_CI_CHECK_RUNS`.
|
|
256
|
+
**Never in CI/CD** — see the rule at the top. The procedure:
|
|
250
257
|
|
|
251
|
-
|
|
258
|
+
```bash
|
|
259
|
+
pnpm build && pnpm preview:build # the STATIC preview — not `pnpm dev` / `pnpm preview`
|
|
260
|
+
pnpm check:frame-axe # full sweep; must end with 0 baselined entries, 0 nodes
|
|
261
|
+
pnpm check:frame-axe /isolate/<frame> # one route, while fixing
|
|
262
|
+
```
|
|
252
263
|
|
|
253
|
-
|
|
264
|
+
Run it before a PR that touches markup, ARIA, focus behaviour or colour, and commit what it writes —
|
|
265
|
+
`audit-evidence/frame-axe/results.json` and, only when a full sweep genuinely changed it,
|
|
266
|
+
`frame-axe-baseline.json`.
|
|
254
267
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
268
|
+
**The static preview, not the dev server.** Against the vite dev server the sweep produces 1–3
|
|
269
|
+
spurious `route-did-not-render` / `axe-did-not-run` rows on different routes each run — 30s
|
|
270
|
+
`page.goto` timeouts while three viewport contexts hammer an on-demand-transforming server. Each is
|
|
271
|
+
clean alone. The static build does not do this.
|
|
259
272
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
repository already reserves for wide matrices.
|
|
273
|
+
**`--shard=i/n` and `--scope=` are for splitting a LOCAL run**, e.g. across two terminals. Two guards
|
|
274
|
+
hold for both: `--update` refuses under either (a partial run would rewrite the ledger from a fraction
|
|
275
|
+
of the frames and delete the rest), and the "no longer fires" report is suppressed on a partial run
|
|
276
|
+
(every other partition's rows would read as fixed). Regenerate the baseline from a full sweep only.
|
|
265
277
|
|
|
266
|
-
|
|
267
|
-
measured 653–745s, and that decision is not reopened here.
|
|
278
|
+
### What leaving CI costs, stated rather than discovered
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
A regression is now caught when someone runs this, not on merge. Nothing in the pipeline will turn
|
|
281
|
+
red on a new `aria-*` defect, a nameless button or a sub-24px target. That is the trade the rule
|
|
282
|
+
makes, and the only thing that closes it is the local run above actually happening before a PR.
|
|
272
283
|
|
|
273
284
|
## Related
|
|
274
285
|
|
package/docs/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| [SPACING.md](./SPACING.md) | Golden ratio (φ) macro spacing — PageContainer sections, Flex gap, Card rhythm |
|
|
13
13
|
| [FORMS.md](./FORMS.md) | **Mandatory** react-hook-form + Zod 4 |
|
|
14
14
|
| [TESTING.md](./TESTING.md) | **Mandatory** Vitest per component |
|
|
15
|
-
| [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) | Per-frame axe a11y + geometry + coverage
|
|
15
|
+
| [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) | Per-frame axe a11y (LOCAL ONLY, never CI) + geometry + coverage gates — how to run/regenerate |
|
|
16
16
|
| [../README.md](../README.md) | Setup, workspace wiring, theme |
|
|
17
17
|
|
|
18
18
|
## Preview
|
|
@@ -29,6 +29,10 @@ export default function Demo() {
|
|
|
29
29
|
from: new Date(2026, 3, 1),
|
|
30
30
|
to: new Date(2026, 3, 30),
|
|
31
31
|
});
|
|
32
|
+
const [ruledRange, setRuledRange] = useState<DateRange | undefined>({
|
|
33
|
+
from: new Date(2026, 3, 13),
|
|
34
|
+
to: new Date(2026, 3, 22),
|
|
35
|
+
});
|
|
32
36
|
const [shiftDays, setShiftDays] = useState<Date[] | undefined>([
|
|
33
37
|
new Date(2026, 3, 6),
|
|
34
38
|
new Date(2026, 3, 13),
|
|
@@ -67,6 +71,48 @@ export default function Demo() {
|
|
|
67
71
|
</CardContent>
|
|
68
72
|
</Card>
|
|
69
73
|
|
|
74
|
+
<Card>
|
|
75
|
+
<CardHeader>
|
|
76
|
+
<CardTitle level={2}>
|
|
77
|
+
罫線 (bordered) · 既定で表示 / bordered={"{false}"} で解除
|
|
78
|
+
</CardTitle>
|
|
79
|
+
<CardDescription>
|
|
80
|
+
日と日の間に罫線を引くのが既定。線色は --calendar-grid-border-color (既定
|
|
81
|
+
--input、背景に対し 3:1
|
|
82
|
+
以上)。選択日・今日・範囲の始点/中間/終点・無効日・前後月の日が
|
|
83
|
+
罫線の中でも判別できる。罫線なしの浮いたボタン表示にしたい場合のみ bordered=
|
|
84
|
+
{"{false}"} を渡す。
|
|
85
|
+
</CardDescription>
|
|
86
|
+
</CardHeader>
|
|
87
|
+
<CardContent>
|
|
88
|
+
<Flex gap="lg" wrap>
|
|
89
|
+
<Calendar
|
|
90
|
+
mode="range"
|
|
91
|
+
selected={ruledRange}
|
|
92
|
+
onSelect={setRuledRange}
|
|
93
|
+
defaultMonth={new Date(2026, 3, 1)}
|
|
94
|
+
today={new Date(2026, 3, 8)}
|
|
95
|
+
locale={ja}
|
|
96
|
+
disabled={[new Date(2026, 3, 5), new Date(2026, 3, 27)]}
|
|
97
|
+
aria-label="罫線あり (既定) の期間カレンダー"
|
|
98
|
+
data-testid="calendar-ruled"
|
|
99
|
+
/>
|
|
100
|
+
<Calendar
|
|
101
|
+
mode="range"
|
|
102
|
+
selected={ruledRange}
|
|
103
|
+
onSelect={setRuledRange}
|
|
104
|
+
defaultMonth={new Date(2026, 3, 1)}
|
|
105
|
+
today={new Date(2026, 3, 8)}
|
|
106
|
+
locale={ja}
|
|
107
|
+
disabled={[new Date(2026, 3, 5), new Date(2026, 3, 27)]}
|
|
108
|
+
bordered={false}
|
|
109
|
+
aria-label="罫線なし (bordered=false) の期間カレンダー"
|
|
110
|
+
data-testid="calendar-unruled"
|
|
111
|
+
/>
|
|
112
|
+
</Flex>
|
|
113
|
+
</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
|
|
70
116
|
<Card>
|
|
71
117
|
<CardHeader>
|
|
72
118
|
<CardTitle level={2}>範囲選択 · Popover 内</CardTitle>
|
|
@@ -55,6 +55,36 @@ export default function Demo() {
|
|
|
55
55
|
</CardContent>
|
|
56
56
|
</Card>
|
|
57
57
|
|
|
58
|
+
<Card>
|
|
59
|
+
<CardHeader>
|
|
60
|
+
<CardTitle level={2}>カレンダーの罫線 (bordered)</CardTitle>
|
|
61
|
+
<CardDescription>
|
|
62
|
+
ポップアップのカレンダーは既定で日ごとに罫線を引く (Calendar bordered を転送)。
|
|
63
|
+
bordered={"{false}"} で罫線なしに戻せる。range / multiple / week
|
|
64
|
+
でも同じ。月・四半期・年の グリッドには日のセルがないため対象外。
|
|
65
|
+
</CardDescription>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent>
|
|
68
|
+
<Flex gap="lg" wrap>
|
|
69
|
+
<FormField id="ruled-date" label="罫線あり (既定)">
|
|
70
|
+
<DatePicker
|
|
71
|
+
id="ruled-date"
|
|
72
|
+
name="ruled_date"
|
|
73
|
+
defaultValue={new Date(2026, 0, 15)}
|
|
74
|
+
/>
|
|
75
|
+
</FormField>
|
|
76
|
+
<FormField id="unruled-date" label="罫線なし (bordered={false})">
|
|
77
|
+
<DatePicker
|
|
78
|
+
id="unruled-date"
|
|
79
|
+
name="unruled_date"
|
|
80
|
+
defaultValue={new Date(2026, 0, 15)}
|
|
81
|
+
bordered={false}
|
|
82
|
+
/>
|
|
83
|
+
</FormField>
|
|
84
|
+
</Flex>
|
|
85
|
+
</CardContent>
|
|
86
|
+
</Card>
|
|
87
|
+
|
|
58
88
|
<Card>
|
|
59
89
|
<CardHeader>
|
|
60
90
|
<CardTitle level={2}>fromDate で過去日を無効化</CardTitle>
|
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
* single page:
|
|
25
25
|
*
|
|
26
26
|
* ?topbarSpan=content|full ?navRail=start|end|top|bottom ?collapsed=1 ?sidebar=none
|
|
27
|
-
* ?responsive=docked
|
|
27
|
+
* ?responsive=docked ?measure=narrow|medium ?footer=1
|
|
28
|
+
*
|
|
29
|
+
* `measure` and `footer` feed `scripts/check-app-shell-page-width.mjs` (gh#672): the page is fluid
|
|
30
|
+
* in the main column at every sidebar state, and a sticky footer band always spans `.app-main`,
|
|
31
|
+
* even when a service caps content with `--app-shell-page-max-width` or the page sets `measure`.
|
|
28
32
|
*
|
|
29
33
|
* With no query it is `topbarSpan="full"` without a rail — the state gh#474 found collapsing
|
|
30
34
|
* `main` into a 170px implicit track below 900px, with the page actions laid out past the edge.
|
|
@@ -61,11 +65,13 @@ function readState() {
|
|
|
61
65
|
hasSidebar: params.get("sidebar") !== "none",
|
|
62
66
|
responsiveNavigation:
|
|
63
67
|
params.get("responsive") === "docked" ? ("docked" as const) : ("drawer" as const),
|
|
68
|
+
measure: (["narrow", "medium"] as const).find((m) => m === params.get("measure")),
|
|
69
|
+
hasFooter: params.get("footer") === "1",
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
export default function Demo() {
|
|
68
|
-
const [{ topbarSpan, navRail, collapsed, hasSidebar, responsiveNavigation }] =
|
|
74
|
+
const [{ topbarSpan, navRail, collapsed, hasSidebar, responsiveNavigation, measure, hasFooter }] =
|
|
69
75
|
useState(readState);
|
|
70
76
|
const [activeId, setActiveId] = useState("dashboard");
|
|
71
77
|
const strip = navRail === "top" || navRail === "bottom";
|
|
@@ -113,6 +119,18 @@ export default function Demo() {
|
|
|
113
119
|
<PageContainer
|
|
114
120
|
title="請求書"
|
|
115
121
|
subtitle="株式会社アクメ · 2026年5月"
|
|
122
|
+
measure={measure}
|
|
123
|
+
footer={
|
|
124
|
+
hasFooter ? (
|
|
125
|
+
<Flex gap="sm" align="center" justify="end">
|
|
126
|
+
<Button size="sm" variant="outline">
|
|
127
|
+
下書き保存
|
|
128
|
+
</Button>
|
|
129
|
+
<Button size="sm">コメントを送信する</Button>
|
|
130
|
+
</Flex>
|
|
131
|
+
) : undefined
|
|
132
|
+
}
|
|
133
|
+
stickyFooter={hasFooter}
|
|
116
134
|
extra={
|
|
117
135
|
<Flex gap="sm" wrap>
|
|
118
136
|
<Button size="sm" variant="outline">
|
|
@@ -555,12 +555,16 @@ function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
|
|
|
555
555
|
opacity: cell.dim ? 0.5 : 1,
|
|
556
556
|
}}
|
|
557
557
|
>
|
|
558
|
-
<
|
|
559
|
-
|
|
558
|
+
<Button
|
|
559
|
+
variant="ghost"
|
|
560
|
+
shape="sharp"
|
|
561
|
+
fullWidth
|
|
562
|
+
wrap
|
|
563
|
+
align="start"
|
|
560
564
|
onClick={() => onPick(cell)}
|
|
561
|
-
className="
|
|
565
|
+
className="min-h-[96px]"
|
|
562
566
|
>
|
|
563
|
-
<Flex direction="col" gap="xs">
|
|
567
|
+
<Flex direction="col" gap="xs" fill>
|
|
564
568
|
<Flex align="center" justify="between">
|
|
565
569
|
<Text
|
|
566
570
|
as="span"
|
|
@@ -604,7 +608,7 @@ function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
|
|
|
604
608
|
) : null}
|
|
605
609
|
</Flex>
|
|
606
610
|
</Flex>
|
|
607
|
-
</
|
|
611
|
+
</Button>
|
|
608
612
|
</TableCell>
|
|
609
613
|
);
|
|
610
614
|
})}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "25.
|
|
4
|
-
"godxUiMcp": "25.
|
|
3
|
+
"version": "25.4.0",
|
|
4
|
+
"godxUiMcp": "25.4.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -334,8 +334,9 @@
|
|
|
334
334
|
"check:choice-hit-target": "node scripts/check-choice-hit-target.mjs",
|
|
335
335
|
"check:state-legible": "node scripts/check-state-legible.mjs",
|
|
336
336
|
"check:app-shell-narrow-grid": "node scripts/check-app-shell-narrow-grid.mjs",
|
|
337
|
+
"check:app-shell-page-width": "node scripts/check-app-shell-page-width.mjs",
|
|
337
338
|
"check:frame-contracts": "pnpm check:component-api-manifest && pnpm audit:component-cases && pnpm check:frame-coverage && pnpm check:frame-coverage-ledger && pnpm check:screen-reader-evidence && pnpm check:data-entry-owner-source && pnpm check:display-runtime-evidence",
|
|
338
|
-
"check:frame-runtime": "pnpm check:data-entry-touch-aria && pnpm check:app-shell-narrow-grid && pnpm check:choice-hit-target",
|
|
339
|
+
"check:frame-runtime": "pnpm check:data-entry-touch-aria && pnpm check:app-shell-narrow-grid && pnpm check:app-shell-page-width && pnpm check:choice-hit-target",
|
|
339
340
|
"verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
|
|
340
341
|
"verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
|
|
341
342
|
"verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -145,6 +145,19 @@ const EMOJI_FLAG = /\p{Regional_Indicator}/u;
|
|
|
145
145
|
*/
|
|
146
146
|
const ATTRS = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|=>|[^>"'])*`;
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* A raw lowercase HTML opening tag, wherever its line ends (gh#673).
|
|
150
|
+
*
|
|
151
|
+
* The control rules were `/<button[\s>]/` run ONE LINE at a time. Prettier leaves `<button` alone on
|
|
152
|
+
* its line when it wraps an element, and the newline after the tag name is not part of that line, so
|
|
153
|
+
* every wrapped element was invisible: a consumer's audit reported 0 `no-raw-button` while ~166
|
|
154
|
+
* multi-line `<button` remained, and "0" read as "migration finished". Matched over the whole file
|
|
155
|
+
* (`spansElement`), the lookahead `[\s/>]` accepts that newline while `<buttonish`, `<Button` and
|
|
156
|
+
* `</button` stay unmatched. `[^\n]*` only carries the rest of the line into the snippet — and keeps
|
|
157
|
+
* one finding per line, which is what the per-line rule reported for a single-line element.
|
|
158
|
+
*/
|
|
159
|
+
const rawTag = (tag) => new RegExp(`<${tag}(?=[\\s/>])[^\\n]*`, "g");
|
|
160
|
+
|
|
148
161
|
/**
|
|
149
162
|
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string,
|
|
150
163
|
* exempt?:RegExp, classOnly?:boolean}[]}
|
|
@@ -185,7 +198,9 @@ const RULES = [
|
|
|
185
198
|
severity: "error",
|
|
186
199
|
// A hand-rolled flex/grid container. The primitives exist precisely so a row is <Flex>, a
|
|
187
200
|
// stack is <Flex direction="col">, a grid is <ResponsiveGrid> — token gaps, RTL, density.
|
|
188
|
-
|
|
201
|
+
// Whole-file: a className template literal can hold `flex` several lines below `className=` (gh#673).
|
|
202
|
+
spansElement: true,
|
|
203
|
+
test: /className=(?:"[^"]*|'[^']*|\{`[^`]*)(?<![\w-])(?:flex|inline-flex|grid|inline-grid)(?![\w-])/g,
|
|
189
204
|
message:
|
|
190
205
|
'No hand-rolled flex/grid — use <Flex> (row), <Flex direction="col"> (stack) or <ResponsiveGrid> (docs/CONSUMER-RULES.md §3).',
|
|
191
206
|
},
|
|
@@ -203,17 +218,23 @@ const RULES = [
|
|
|
203
218
|
// rounded-full` dot was clean, `prettier --write` reordered it to `rounded-full bg-current`,
|
|
204
219
|
// and the warning appeared with no source change. A rule a formatter can flip is a rule
|
|
205
220
|
// nobody can trust.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
//
|
|
222
|
+
// Whole-file, one verdict per className: a template literal spread over lines puts `rounded`
|
|
223
|
+
// and `border` on lines of their own, where a per-line read saw neither (gh#673).
|
|
224
|
+
spansElement: true,
|
|
225
|
+
matches: function* (source) {
|
|
226
|
+
for (const m of source.matchAll(/className=(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`)/g)) {
|
|
227
|
+
const cls = m[1] ?? m[2] ?? m[3] ?? "";
|
|
228
|
+
// A MARKER is not a surface. `size-1` is 4px — a dot, a pip, a status bead. The primitives
|
|
229
|
+
// this rule points at (Card, Badge, Avatar, ListRow, Descriptions, EmptyState) all start at
|
|
230
|
+
// control height, so none of them can express one, and flagging it sends the reader looking
|
|
231
|
+
// for a component that does not exist. The cut-off is `size-2` / 8px: below that there is no
|
|
232
|
+
// room for the padding and type that make something a surface.
|
|
233
|
+
if (/\b(?:size|[wh])-(?:0\.5|1|1\.5|2)\b/.test(cls)) continue;
|
|
234
|
+
if (/\brounded(?:-(?:full|sm|md|lg|xl|2xl))?\b/.test(cls) && /\b(?:border|bg-)/.test(cls)) {
|
|
235
|
+
yield m;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
217
238
|
},
|
|
218
239
|
message:
|
|
219
240
|
"Hand-rolled surface (rounded + border/bg) — use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from the tokens (docs/CONSUMER-RULES.md §4). A read-only sample of a colour a USER chose is Swatch, which takes that value as a prop.",
|
|
@@ -360,14 +381,16 @@ const RULES = [
|
|
|
360
381
|
replacement: "Select",
|
|
361
382
|
scope: "consumer-control",
|
|
362
383
|
severity: "error",
|
|
363
|
-
|
|
384
|
+
spansElement: true,
|
|
385
|
+
test: rawTag("select"),
|
|
364
386
|
message: "Use <Select> from @godxjp/ui, not a raw <select> (rules §3).",
|
|
365
387
|
},
|
|
366
388
|
{
|
|
367
389
|
id: "no-raw-table",
|
|
368
390
|
replacement: "DataTable / Table",
|
|
369
391
|
severity: "error",
|
|
370
|
-
|
|
392
|
+
spansElement: true,
|
|
393
|
+
test: rawTag("table"),
|
|
371
394
|
message: "Use the <Table> family from @godxjp/ui, not a raw <table> (rules §3).",
|
|
372
395
|
},
|
|
373
396
|
{
|
|
@@ -375,7 +398,8 @@ const RULES = [
|
|
|
375
398
|
replacement: "Textarea",
|
|
376
399
|
scope: "consumer-control",
|
|
377
400
|
severity: "warn",
|
|
378
|
-
|
|
401
|
+
spansElement: true,
|
|
402
|
+
test: rawTag("textarea"),
|
|
379
403
|
message: "Use <Textarea> from @godxjp/ui, not a raw <textarea> (rules §3).",
|
|
380
404
|
},
|
|
381
405
|
{
|
|
@@ -392,7 +416,8 @@ const RULES = [
|
|
|
392
416
|
replacement: "Button",
|
|
393
417
|
scope: "consumer-control",
|
|
394
418
|
severity: "error",
|
|
395
|
-
|
|
419
|
+
spansElement: true,
|
|
420
|
+
test: rawTag("button"),
|
|
396
421
|
message: "Use <Button> from @godxjp/ui, not a raw <button> (rules §3).",
|
|
397
422
|
},
|
|
398
423
|
{
|
|
@@ -423,7 +448,9 @@ const RULES = [
|
|
|
423
448
|
{
|
|
424
449
|
id: "manual-field-helper",
|
|
425
450
|
severity: "warn",
|
|
426
|
-
|
|
451
|
+
// Whole-file: prettier wraps `<p` and its className onto separate lines (gh#673).
|
|
452
|
+
spansElement: true,
|
|
453
|
+
test: new RegExp(`<p${ATTRS}className="text-xs text-muted-foreground"`, "g"),
|
|
427
454
|
message:
|
|
428
455
|
"Field helper text should use <FormField helper=…>, not a hand-rolled <p> (rules §1). OK only for checkbox/radio groups.",
|
|
429
456
|
},
|
|
@@ -541,7 +568,9 @@ const RULES = [
|
|
|
541
568
|
* — one of ten standing warnings here. Flagging it would push every consumer doing the right
|
|
542
569
|
* thing toward either a suppression comment or a worse glyph.
|
|
543
570
|
*/
|
|
544
|
-
|
|
571
|
+
// Whole-file: prettier puts a wrapped element's text on its own line, `>` and `<` elsewhere (gh#673).
|
|
572
|
+
spansElement: true,
|
|
573
|
+
test: />\s*[✕✖×╳]\s*</g,
|
|
545
574
|
exempt: /<VisuallyHidden\b|sr-only/,
|
|
546
575
|
standard: "WAI-ARIA 1.2 (dialog) · WCAG 2.2 SC 4.1.2 · @godxjp/ui Alert/Dialog anatomy",
|
|
547
576
|
message:
|
|
@@ -1190,7 +1219,11 @@ for (const dir of SCAN_DIRS) {
|
|
|
1190
1219
|
continue;
|
|
1191
1220
|
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
1192
1221
|
if (suppressed(rule.id, lineNo - 1)) continue;
|
|
1193
|
-
|
|
1222
|
+
// "The line after" is the one after the match ENDS: a wrapped glyph's name follows its `</span>`.
|
|
1223
|
+
const endLineNo = lineNo + (match[0].match(/\n/g)?.length ?? 0);
|
|
1224
|
+
if (isExempt(rule, origLines[lineNo - 1], origLines[lineNo - 2], origLines[endLineNo])) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1194
1227
|
findings.push({
|
|
1195
1228
|
file: rel,
|
|
1196
1229
|
line: lineNo,
|
|
@@ -1198,6 +1231,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
1198
1231
|
severity: rule.severity,
|
|
1199
1232
|
standard: rule.standard,
|
|
1200
1233
|
message: rule.message,
|
|
1234
|
+
replacement: rule.replacement,
|
|
1201
1235
|
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
1202
1236
|
});
|
|
1203
1237
|
}
|