@godxjp/ui 24.0.0 → 25.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.
Files changed (45) hide show
  1. package/dist/components/data-display/badge.js +3 -0
  2. package/dist/components/data-entry/select.js +7 -0
  3. package/dist/components/general/logo.d.ts +16 -0
  4. package/dist/components/general/logo.js +10 -2
  5. package/dist/components/layout/auth-identity.d.ts +1 -1
  6. package/dist/components/layout/auth-identity.js +5 -2
  7. package/dist/components/navigation/dropdown-menu.js +19 -2
  8. package/dist/components/navigation/tabs.js +24 -6
  9. package/dist/contracts/measurement.json +1 -1
  10. package/dist/email/tokens.generated.d.ts +1 -1
  11. package/dist/email/tokens.generated.js +1 -1
  12. package/dist/props/components/layout.prop.d.ts +32 -1
  13. package/dist/props/registry.d.ts +4 -0
  14. package/dist/props/registry.js +5 -0
  15. package/dist/props/vocabulary/interaction.prop.d.ts +10 -1
  16. package/dist/styles/control.css +5 -1
  17. package/dist/styles/data-display-layout.css +16 -5
  18. package/dist/styles/data-entry-layout.css +5 -0
  19. package/dist/styles/layout.css +5 -0
  20. package/dist/styles/logo-layout.css +30 -3
  21. package/dist/styles/motion.css +4 -0
  22. package/dist/styles/shell-layout.css +22 -6
  23. package/dist/styles/text-layout.css +5 -0
  24. package/dist/tokens/components/logo.css +8 -0
  25. package/dist/tokens/components/shell.css +7 -3
  26. package/dist/tokens/derived.css +8 -8
  27. package/dist/tokens/foundation.css +2 -2
  28. package/docs/DESIGN-AUTHORITY.md +36 -22
  29. package/docs/DEVELOPMENT.md +20 -0
  30. package/docs/FRAME-A11Y-CI.md +164 -0
  31. package/docs/foundation/colors.tsx +4 -1
  32. package/docs/general/logo.tsx +63 -0
  33. package/docs/general/typography.tsx +31 -0
  34. package/docs/layout/auth-identity.tsx +34 -0
  35. package/docs/layout/topbar.tsx +19 -8
  36. package/docs/showcase/acme-website.tsx +3 -1
  37. package/docs/showcase/case4-login.tsx +2 -2
  38. package/docs/showcase/case5-shift-calendar.tsx +5 -3
  39. package/docs/showcase/case6-agency-handy.tsx +13 -9
  40. package/docs/showcase/futurelastic-web.tsx +11 -3
  41. package/docs/showcase/table-bulk-actions.tsx +13 -2
  42. package/docs/showcase/table-density.tsx +3 -1
  43. package/docs/showcase/table-expandable-rows.tsx +25 -15
  44. package/docs/showcase/table-grouped-subtotals.tsx +5 -1
  45. package/package.json +4 -2
@@ -126,8 +126,10 @@ function ShiftPill({
126
126
  <Text as="span" size="2xs" weight="medium">
127
127
  {meta.label}
128
128
  </Text>
129
+ {/* `inherit`, not `muted` (gh#643): the chip is a TINTED surface, and `muted` is an absolute
130
+ token that measured 4.46:1 on it. Inheriting lets the chip's own ink win. */}
129
131
  {staff ? (
130
- <Text as="span" size="2xs" tone="muted" truncate>
132
+ <Text as="span" size="2xs" tone="inherit" truncate>
131
133
  {staff}
132
134
  </Text>
133
135
  ) : null}
@@ -433,7 +435,7 @@ export default function ShiftCalendarShowcase() {
433
435
  <Text size="xs" weight="medium" className="whitespace-nowrap">
434
436
  {SHIFT_META[k].label}
435
437
  </Text>
436
- <Text size="xs" tone="muted" tabular className="whitespace-nowrap">
438
+ <Text size="xs" tone="inherit" tabular className="whitespace-nowrap">
437
439
  {SHIFT_META[k].time}
438
440
  </Text>
439
441
  </Badge>
@@ -582,7 +584,7 @@ function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
582
584
  tone="destructive"
583
585
  variant="outline"
584
586
  shape="sharp"
585
- className="text-[var(--font-size-2xs)]"
587
+ className="text-[length:var(--font-size-2xs)]"
586
588
  >
587
589
  祝
588
590
  </Badge>
@@ -368,7 +368,7 @@ function TabBar({ active, onChange }: { active: string; onChange: (id: string) =
368
368
  onClick={() => onChange(t.id)}
369
369
  aria-current={isActive ? "page" : undefined}
370
370
  className={
371
- "h-full flex-col rounded-none font-medium text-[var(--font-size-2xs)] " +
371
+ "h-full flex-col rounded-none text-[length:var(--font-size-2xs)] font-medium " +
372
372
  (isActive ? "text-primary" : "text-muted-foreground")
373
373
  }
374
374
  >
@@ -637,12 +637,16 @@ function InboundTab({
637
637
  <ToggleGroupItem
638
638
  key={f.id}
639
639
  value={f.id}
640
- className="h-9 shrink-0 rounded-full whitespace-nowrap text-[var(--font-size-xs)]"
640
+ className="h-9 shrink-0 rounded-full text-[length:var(--font-size-xs)] whitespace-nowrap"
641
+ // The library's OWN counter pill, not a `Text` at 70% opacity (gh#643). The
642
+ // hand-rolled one measured 2.15:1 on the selected chip's --primary fill, because
643
+ // `opacity-70` blends whatever ink it inherits back towards the fill underneath it —
644
+ // `tone="inherit"` cannot help with that. `.ui-toggle[data-state="on"]
645
+ // .ui-toggle-count` inverts instead (--primary on --primary-foreground, 6.32:1) and
646
+ // folds the number into the accessible name.
647
+ count={f.count}
641
648
  >
642
649
  {f.label}
643
- <Text size="xs" mono tabular className="opacity-70">
644
- {f.count}
645
- </Text>
646
650
  </ToggleGroupItem>
647
651
  ))}
648
652
  </Flex>
@@ -848,19 +852,19 @@ function OutboundTab({ seg, setSeg }: { seg: string; setSeg: (v: string) => void
848
852
  <ResponsiveGrid columns={3} gap="xs">
849
853
  <ToggleGroupItem
850
854
  value="seal"
851
- className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
855
+ className="h-9 rounded-xl text-[length:var(--font-size-xs)] whitespace-nowrap"
852
856
  >
853
857
  Chờ niêm phong
854
858
  </ToggleGroupItem>
855
859
  <ToggleGroupItem
856
860
  value="handoff"
857
- className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
861
+ className="h-9 rounded-xl text-[length:var(--font-size-xs)] whitespace-nowrap"
858
862
  >
859
863
  Chờ bàn giao
860
864
  </ToggleGroupItem>
861
865
  <ToggleGroupItem
862
866
  value="done"
863
- className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
867
+ className="h-9 rounded-xl text-[length:var(--font-size-xs)] whitespace-nowrap"
864
868
  >
865
869
  Đã bàn giao
866
870
  </ToggleGroupItem>
@@ -1014,7 +1018,7 @@ export default function AgencyHandyShowcase() {
1014
1018
  <ToggleGroupItem
1015
1019
  key={s}
1016
1020
  value={s}
1017
- className="size-7 rounded-md text-[var(--font-size-2xs)] uppercase"
1021
+ className="size-7 rounded-md text-[length:var(--font-size-2xs)] uppercase"
1018
1022
  title={s}
1019
1023
  >
1020
1024
  {s[0]}
@@ -63,7 +63,15 @@ const THEME = `
63
63
  [data-tenant="futurelastic"] .fl-section { padding-block: 5.5rem; }
64
64
  [data-tenant="futurelastic"] .fl-navbar { position: sticky; top: 0; z-index: 30; background: hsl(var(--background) / 0.8);
65
65
  backdrop-filter: blur(12px); border-bottom: 1px solid hsl(var(--border)); }
66
- [data-tenant="futurelastic"] .fl-navbar-inner { height: 68px; display: flex; align-items: center; gap: 2rem; }
66
+ /* REFLOW, NOT DECORATION (gh#643). This row was a fixed 68px flex line with no wrap, so at 320 its
67
+ content ran to x=569.6 inside a 320px nav: every link painted OUTSIDE the navbar's own
68
+ translucent box, over the page behind it, and axe read the white labels against the light
69
+ isolate background at 1.01:1 — 2 nodes at 320, 1 at 375. It was never a contrast bug; it was
70
+ WCAG 2.2 SC 1.4.10 reflow, reported by the contrast rule because that is what an overflowing
71
+ element looks like from the outside. min-height rather than height, so a wrapped row may be
72
+ taller than one line and the language cluster stops overflowing the bar vertically (measured at
73
+ 320: it was 148.3px tall, starting at y=-40.2). */
74
+ [data-tenant="futurelastic"] .fl-navbar-inner { min-height: 68px; display: flex; align-items: center; flex-wrap: wrap; column-gap: 2rem; row-gap: 0.75rem; padding-block: 0.75rem; }
67
75
  [data-tenant="futurelastic"] .fl-brand { font-family: var(--font-family-display); font-weight: 700; font-size: 1.25rem; color: hsl(var(--foreground)); }
68
76
  [data-tenant="futurelastic"] .fl-eyebrow { font-family: var(--font-family-body); font-weight: 600; font-size: 0.75rem;
69
77
  letter-spacing: 0.18em; text-transform: uppercase; color: hsl(var(--primary)); }
@@ -122,14 +130,14 @@ function Navbar() {
122
130
  <span className="fl-brand">
123
131
  futur<span className="fl-gold">elastic</span>
124
132
  </span>
125
- <Flex direction="row" gap="xs" align="center">
133
+ <Flex direction="row" gap="xs" align="center" wrap>
126
134
  {links.map((l, i) => (
127
135
  <Button key={l} variant="ghost" size="sm" aria-current={i === 0 ? "page" : undefined}>
128
136
  {l}
129
137
  </Button>
130
138
  ))}
131
139
  </Flex>
132
- <Flex direction="row" gap="sm" align="center" className="ms-auto">
140
+ <Flex direction="row" gap="sm" align="center" wrap className="ms-auto">
133
141
  <Text as="span" size="xs" tone="muted">
134
142
  EN · 日本語 · VI
135
143
  </Text>
@@ -205,8 +205,19 @@ function BulkActionBar({
205
205
  <Card className="bg-primary/5 border-primary/30">
206
206
  <CardContent solo>
207
207
  <Flex role="region" aria-label="一括操作" wrap align="center" gap="md">
208
- {/* left: count + cross-page select-all escalation + safe batch actions */}
209
- <Flex direction="row" align="center" gap="sm" className="min-w-0 flex-1">
208
+ {/* left: count + cross-page select-all escalation + safe batch actions.
209
+ THE `wrap`, AND THE ABSENT `min-w-0`, ARE BOTH LOAD-BEARING (gh#643). This cluster
210
+ used to carry `min-w-0 flex-1` — "squeeze me to nothing" — while every child inside
211
+ refuses to be squeezed: two `Button`s and a `whitespace-nowrap` count. At 375 the box
212
+ duly shrank to 113.6px, the content still ran to x=462, and it painted straight over
213
+ the destructive cluster beside it — 一括承認 at 264.6→360.2 under 解除 at
214
+ 272.3→341.0, which axe reported as `target-size` with 19.2×30.2 of the button left
215
+ unobscured against SC 2.5.8's 24×24. Without `min-w-0` the cluster keeps a real
216
+ min-content width, and `wrap` is what makes that width small enough to be honest:
217
+ measured after, 375 puts the two clusters on separate rows (left 307px over two
218
+ lines, destructive below) and 1440 is unchanged — one 30.2px row, destructive still
219
+ pushed to x=1217.5 by `flex-1`. */}
220
+ <Flex direction="row" align="center" gap="sm" wrap className="flex-1">
210
221
  <Text tone="primary" weight="medium" tabular className="whitespace-nowrap">
211
222
  {effective}件を選択中
212
223
  </Text>
@@ -267,7 +267,9 @@ export default function Demo() {
267
267
  >
268
268
  <Flex direction="row" align="center" gap="xs">
269
269
  <span>{DENSITY_META[key].label}</span>
270
- <Text size="2xs" tone="muted" tabular>
270
+ {/* `inherit` (gh#643): a SELECTED item fills with --primary, and `muted`
271
+ is an absolute token — it measured 1.11:1 on that fill. */}
272
+ <Text size="2xs" tone="inherit" tabular>
271
273
  {DENSITY_META[key].px}
272
274
  </Text>
273
275
  </Flex>
@@ -299,7 +299,7 @@ function DetailPanel({ employee }: { employee: Employee }) {
299
299
  {/* Per-day breakdown — quiet nested list, real Card chrome */}
300
300
  <Card>
301
301
  <CardHeader>
302
- <CardTitle level={2} className="text-[var(--font-size-xs)]">
302
+ <CardTitle level={2} className="text-[length:var(--font-size-xs)]">
303
303
  今週の打刻
304
304
  </CardTitle>
305
305
  <CardAction>
@@ -396,27 +396,37 @@ function ExpandableList() {
396
396
  const panelId = `row-detail-${emp.id}`;
397
397
  return (
398
398
  <React.Fragment key={emp.id}>
399
+ {/* THE DISCLOSURE IS THE BUTTON, NOT THE ROW (gh#643, found by check:frame-axe).
400
+ This row used to carry `tabIndex={0}` + `aria-expanded` + `aria-controls` and be
401
+ the control itself. `aria-expanded` is valid on a TREEGRID row and not on a table
402
+ row, so axe scored it `aria-conditional-attr` — and the row was a focus stop with
403
+ no role and no name, which is the worse half of the same mistake. The chevron is
404
+ now a real button: it owns the state, the name and the tab stop. Clicking the row
405
+ still works, as a pointer convenience that claims no semantics of its own. */}
399
406
  <TableRow
400
407
  data-state={isOpen ? "selected" : undefined}
401
- tabIndex={0}
402
- aria-expanded={isOpen}
403
- aria-controls={panelId}
404
408
  onClick={() => {
405
409
  toggle(emp.id);
406
410
  }}
407
- onKeyDown={(e) => {
408
- if (e.key !== "Enter" && e.key !== " ") return;
409
- if (e.target !== e.currentTarget) return;
410
- e.preventDefault();
411
- toggle(emp.id);
412
- }}
413
- className="hover:bg-muted/50 focus-visible:ring-ring cursor-pointer focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset"
411
+ className="hover:bg-muted/50 cursor-pointer"
414
412
  >
415
413
  <TableCell className={CELL}>
416
- <ChevronRight
417
- className={`text-muted-foreground size-4 transition-transform ${isOpen ? "rotate-90" : ""}`}
418
- aria-hidden="true"
419
- />
414
+ <Button
415
+ variant="ghost"
416
+ size="icon-xs"
417
+ aria-expanded={isOpen}
418
+ aria-controls={isOpen ? panelId : undefined}
419
+ aria-label={`${emp.name} の詳細を${isOpen ? "折りたたむ" : "展開する"}`}
420
+ onClick={(e) => {
421
+ e.stopPropagation();
422
+ toggle(emp.id);
423
+ }}
424
+ >
425
+ <ChevronRight
426
+ className={`text-muted-foreground size-4 transition-transform ${isOpen ? "rotate-90" : ""}`}
427
+ aria-hidden="true"
428
+ />
429
+ </Button>
420
430
  </TableCell>
421
431
  <TableCell className={CELL}>
422
432
  <Text as="div" weight="medium">
@@ -232,7 +232,11 @@ function GroupHeaderRow({
232
232
  size="sm"
233
233
  onClick={onToggle}
234
234
  aria-expanded={open}
235
- aria-controls={`grp-${group.id}`}
235
+ /* NO `aria-controls` (gh#643, found by check:frame-axe). It pointed at `grp-<id>`, and
236
+ no element in this table ever carried that id — axe: `aria-valid-attr-value`, 5 nodes.
237
+ It cannot simply be added, either: what this button expands is a RUN OF SIBLING `<tr>`s
238
+ with no single container to name. `aria-expanded` alone is complete and valid here —
239
+ `aria-controls` is optional, and a broken one is worse than none. */
236
240
  className="h-7 font-medium"
237
241
  >
238
242
  <Chevron className="text-muted-foreground size-4" aria-hidden="true" />
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "24.0.0",
4
- "godxUiMcp": "24.0.0",
3
+ "version": "25.0.0",
4
+ "godxUiMcp": "25.0.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -322,6 +322,7 @@
322
322
  "check:example-imports": "node scripts/check-example-imports.mjs",
323
323
  "check:frame-coverage": "node scripts/frame-coverage.mjs",
324
324
  "check:frame-coverage-report": "node scripts/frame-coverage.mjs --check",
325
+ "check:frame-axe": "node scripts/check-frame-axe.mjs",
325
326
  "gen:frame-coverage-ledger": "node scripts/gen-frame-coverage-ledger.mjs",
326
327
  "check:frame-coverage-ledger": "node scripts/gen-frame-coverage-ledger.mjs --check && node scripts/check-frame-coverage-ledger.mjs",
327
328
  "check:screen-reader-evidence": "node scripts/check-screen-reader-evidence.mjs",
@@ -469,6 +470,7 @@
469
470
  "vaul": "^1.1.2"
470
471
  },
471
472
  "devDependencies": {
473
+ "@axe-core/playwright": "^4.13.0",
472
474
  "@eslint/js": "^10.0.1",
473
475
  "@hookform/resolvers": "^5.9.1",
474
476
  "@radix-ui/react-accordion": "^1.2.20",