@godxjp/ui 25.3.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.
@@ -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.0",
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,
@@ -285,7 +285,10 @@
285
285
 
286
286
  .app-main .ui-page-container {
287
287
  width: 100%;
288
- max-width: var(--app-shell-page-max-width);
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
- --app-shell-page-max-width: 80rem;
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;
@@ -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
- <button
559
- type="button"
558
+ <Button
559
+ variant="ghost"
560
+ shape="sharp"
561
+ fullWidth
562
+ wrap
563
+ align="start"
560
564
  onClick={() => onPick(cell)}
561
- className="hover:bg-accent focus-visible:ring-ring min-h-[96px] w-full text-start transition-colors focus:outline-none focus-visible:ring-2"
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
- </button>
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.3.0",
4
- "godxUiMcp": "25.3.0",
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",
@@ -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
- test: /className=(?:"[^"]*|'[^']*|\{`[^`]*)(?<![\w-])(?:flex|inline-flex|grid|inline-grid)(?![\w-])/,
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
- test: (line) => {
207
- const m = line.match(/className=(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`)/);
208
- if (!m) return false;
209
- const cls = m[1] ?? m[2] ?? m[3] ?? "";
210
- // A MARKER is not a surface. `size-1` is 4px — a dot, a pip, a status bead. The primitives
211
- // this rule points at (Card, Badge, Avatar, ListRow, Descriptions, EmptyState) all start at
212
- // control height, so none of them can express one, and flagging it sends the reader looking
213
- // for a component that does not exist. The cut-off is `size-2` / 8px: below that there is no
214
- // room for the padding and type that make something a surface.
215
- if (/\b(?:size|[wh])-(?:0\.5|1|1\.5|2)\b/.test(cls)) return false;
216
- return /\brounded(?:-(?:full|sm|md|lg|xl|2xl))?\b/.test(cls) && /\b(?:border|bg-)/.test(cls);
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
- test: /<select[\s>]/,
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
- test: /<table[\s>]/,
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
- test: /<textarea[\s>]/,
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
- test: /<button[\s>]/,
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
- test: /<p[^>]*className="text-xs text-muted-foreground"/,
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
- test: />\s*[✕✖×╳]\s*</,
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
- if (isExempt(rule, origLines[lineNo - 1], origLines[lineNo - 2])) continue;
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
  }