@godxjp/ui 24.1.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 (42) 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/tabs.js +24 -6
  8. package/dist/contracts/measurement.json +1 -1
  9. package/dist/email/tokens.generated.d.ts +1 -1
  10. package/dist/email/tokens.generated.js +1 -1
  11. package/dist/props/components/layout.prop.d.ts +32 -1
  12. package/dist/props/registry.d.ts +4 -0
  13. package/dist/props/registry.js +5 -0
  14. package/dist/props/vocabulary/interaction.prop.d.ts +10 -1
  15. package/dist/styles/control.css +5 -1
  16. package/dist/styles/data-display-layout.css +16 -5
  17. package/dist/styles/data-entry-layout.css +5 -0
  18. package/dist/styles/layout.css +5 -0
  19. package/dist/styles/logo-layout.css +30 -3
  20. package/dist/styles/motion.css +4 -0
  21. package/dist/styles/shell-layout.css +11 -4
  22. package/dist/styles/text-layout.css +5 -0
  23. package/dist/tokens/components/logo.css +8 -0
  24. package/dist/tokens/components/shell.css +5 -3
  25. package/dist/tokens/derived.css +8 -8
  26. package/dist/tokens/foundation.css +2 -2
  27. package/docs/DESIGN-AUTHORITY.md +36 -22
  28. package/docs/FRAME-A11Y-CI.md +46 -17
  29. package/docs/foundation/colors.tsx +4 -1
  30. package/docs/general/logo.tsx +63 -0
  31. package/docs/general/typography.tsx +31 -0
  32. package/docs/layout/auth-identity.tsx +34 -0
  33. package/docs/showcase/acme-website.tsx +3 -1
  34. package/docs/showcase/case4-login.tsx +2 -2
  35. package/docs/showcase/case5-shift-calendar.tsx +5 -3
  36. package/docs/showcase/case6-agency-handy.tsx +13 -9
  37. package/docs/showcase/futurelastic-web.tsx +11 -3
  38. package/docs/showcase/table-bulk-actions.tsx +13 -2
  39. package/docs/showcase/table-density.tsx +3 -1
  40. package/docs/showcase/table-expandable-rows.tsx +25 -15
  41. package/docs/showcase/table-grouped-subtotals.tsx +5 -1
  42. package/package.json +2 -2
@@ -18,8 +18,8 @@ It changes no code by itself. It is the tie-breaker a reviewer points at.
18
18
  | Behaviour primitives | **Radix** | already the implementation — 193 references |
19
19
  | Component composition shape | **shadcn** | already the structural convention — 23 references |
20
20
  | Component taxonomy / grouping | **Ant Design** groups | already the catalog shape: `data-entry`, `data-display`, `layout`, `feedback`, `navigation`, `general` — a naming precedent, nothing is installed |
21
- | Colour foundation | **SmartHR** | already the palette source — `--primary` = SmartHR MAIN `#0071bd`, `--foreground` = TEXT_BLACK, `--border` = BORDER |
22
- | **Derived colour — the interaction states hanging off each seed** | **Measured contrast (WCAG 2.2 / JIS X 8341-3)** | Authored in `src/tokens/derived.css`; no algorithm derives them. Four contrast suites read that file and hold every value to a threshold — see below |
21
+ | Colour foundation | **SmartHR**, brand roles aside | the NEUTRAL spine is SmartHR's — `--foreground` = TEXT_BLACK, `--border` = BORDER. `--primary` was SmartHR MAIN `#0071bd` until identity v2.3 took the brand roles back: it is GoDX violet `#7A00FF` today |
22
+ | **Derived colour — the interaction states hanging off each seed** | **Measured contrast (WCAG 2.2 / JIS X 8341-3)** | Authored in `src/tokens/derived.css`; no algorithm derives them. Four contrast suites read that file and hold every value to a threshold, and a fifth holds it to the seed's HUE (gh#648) — see below |
23
23
  | **Japanese UI convention — density, JP typography, form patterns** | **SmartHR** | **NEW — this decision.** Extends SmartHR from "where the colours came from" to the authority for how a JP business screen behaves |
24
24
  | **Japanese accessibility / public-sector convention** | **デジタル庁 Design System** (Digital Agency) | **NEW — this decision.** The reference when a JP customer asks which standard a screen meets (JIS X 8341-3) |
25
25
  | **Spacing, density, type scale, information architecture** | **IBM Carbon** | **NEW — this decision** |
@@ -385,17 +385,25 @@ the property actually being sold.
385
385
  suite — that is CI's job on the PR), and if a threshold breaks the value is
386
386
  wrong. Do not relax the threshold.
387
387
 
388
- **The four overrides that existed even while a generator did** — the clearest evidence the
389
- algorithm was never the real authority. Stepping an interactive fill towards the label sitting on
390
- it lands under 4.5:1, so those four states take the same ramp at the same step size in the
391
- opposite direction:
392
-
393
- | token | conventional step | shipped |
394
- | ------------------------ | ----------------- | ----------------- |
395
- | light `--primary-hover` | #208bc9 · 3.69:1 | #005596 · 7.53:1 |
396
- | light `--primary-active` | #005596 · 7.53:1 | #003c70 · 10.97:1 |
397
- | dark `--primary-hover` | #61b6e8 · 7.92:1 | #61b6e8 · 7.92:1 |
398
- | dark `--primary-active` | #2f76a6 · 3.60:1 | #8bd0f3 · 10.50:1 |
388
+ **The primary interaction ramp, and the one override left in it.** The light states are TAKEN from
389
+ the identity v2.3 kit rather than computed — the kit publishes its own violet ramp — and each is
390
+ snapped to the seed's hue (1.008:1 / 1.010:1 from the kit value, i.e. the same colour). Dark takes
391
+ the kit's `action.primary.hover` and then reflects the pressed step, because the dark seed sits at
392
+ 86.9% L and the conventional step runs out of LIGHTNESS rather than out of contrast:
393
+
394
+ | token | conventional step | shipped |
395
+ | ------------------------ | ------------------------ | ---------------------------------- |
396
+ | light `--primary-hover` | kit #6400D4 | #6500d4 · 8.23:1 |
397
+ | light `--primary-active` | kit #5200B0 | #5400b0 · 10.31:1 |
398
+ | dark `--primary-hover` | kit #E8DAFF | #ecdaff · 13.51:1 |
399
+ | dark `--primary-active` | #fbf7ff · 1.056:1 from white | #cd9fff · 8.41:1 (sign flipped) |
400
+
401
+ **The ratio is blind to hue, and that cost a release (gh#648).** Identity v2.3 moved the seed 63
402
+ degrees and this tier stayed on the blue it was derived from: 24.0.0 and 24.1.0 shipped a primary
403
+ button that was violet at rest and navy on hover, with all four contrast suites green throughout,
404
+ because #005596 clears 7.53:1 on the primary label whatever the seed underneath it is. A threshold
405
+ is still the right authority — it just answers one axis of two.
406
+ `src/tokens/__tests__/derived-hue-lock.test.ts` answers the other, and asserts nothing else.
399
407
 
400
408
  **What was deliberately never derived, and why — each a measurement, not a preference:**
401
409
 
@@ -482,11 +490,16 @@ the **field** indicator on every control — one hairline (1px) in the focus hue
482
490
  `--control-outline` halo — rather than the heavy 3px outline form. Measured, in Chromium, on
483
491
  `ql.test` after the transition settles:
484
492
 
485
- | control | switch off | switch on |
486
- | -------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
487
- | Input / Select trigger | border `1px rgb(144,135,127)`, resting shadow intact | border `1px rgb(0,113,189)` + `rgba(0,182,228,0.11) 0 0 0 2px` |
488
- | Button (primary) | outline `0px`, resting shadow intact | `outline: 1px solid rgb(0,113,189)` @ `0px` + same halo |
489
- | Sidebar nav row / list row | outline `0px` | `outline: 1px solid rgb(0,113,189)` @ **`-1px`** (inset into the row) |
493
+ | control | switch off | switch on |
494
+ | -------------------------- | ---------------------------------------------------- | -------------------------------------------------------------- |
495
+ | Input / Select trigger | border `1px rgb(144,135,127)`, resting shadow intact | border `1px` of `--ring` + `--control-outline` halo `0 0 0 2px` |
496
+ | Button (primary) | outline `0px`, resting shadow intact | `outline: 1px solid` `--ring` @ `0px` + same halo |
497
+ | Sidebar nav row / list row | outline `0px` | `outline: 1px solid` `--ring` @ **`-1px`** (inset into the row) |
498
+
499
+ The GEOMETRY above is the Chromium measurement; the COLOUR is named by token rather than pinned as
500
+ a hex, because it follows the seed and the hexes this table used to carry were the pre-v2.3 blue —
501
+ the same staleness gh#648 found one tier down. Today `--ring` resolves to rgb(122,0,255) light /
502
+ rgb(220,188,255) dark and the halo to `rgba(109,0,228,0.11)` / `rgba(153,61,254,0.29)`.
490
503
 
491
504
  The field pair is the recoloured boundary plus the halo, exactly as the convention specifies for
492
505
  this seed. The nav row insets its mark into its own shape rather than wrapping an already-shaded surface,
@@ -497,10 +510,11 @@ after: the field form only recolours a border and an `outline` is painted outsid
497
510
  Input, Save and Delete all stay at 32.00px.
498
511
 
499
512
  **Two criteria, and only one is met by thickness alone.** SC 1.4.11 (AA, non-text contrast) is about
500
- COLOUR — the 1px mark measures 5.05:1 light and 7.07:1 dark on every surface a control sits on, so
501
- the light weight costs nothing there. `--primary-border` (#6dc0e3) measures **2.00:1 light /
502
- 1.66:1 dark** and could not have satisfied it at any thickness, which is why the on-state takes the
503
- focus hue instead — the single place it departs from the outline form. SC 2.4.13 Focus Appearance
513
+ COLOUR — the 1px mark measures at worst 5.30:1 light and 6.87:1 dark across every surface a control
514
+ sits on (an accent panel in both themes), so the light weight costs nothing there. `--primary-border`
515
+ (#a66de3) reaches 3.49:1 on the page and **2.92:1 light / 1.03:1 dark on that same accent panel**, so
516
+ it still could not satisfy the criterion everywhere at any thickness, which is why the on-state takes
517
+ the focus hue instead — the single place it departs from the outline form. SC 2.4.13 Focus Appearance
504
518
  (AAA) additionally wants a 2px perimeter; the on-state does not target it, and
505
519
  `--focus-outline-weight: var(--stroke-lg)` restores the 3px weight if a customer needs the area
506
520
  clause.
@@ -39,23 +39,52 @@ Showcases are included on purpose. They are the only frames here shaped like a r
39
39
  page, a landmark tree, a focus order — which is precisely the class the component frames cannot
40
40
  reach and the consumer has been carrying alone.
41
41
 
42
- ## What it found on its first run
43
-
44
- 54 rows, **159 violation nodes**, on code that passed every other gate in this repository:
45
-
46
- | nodes | rows | rule | what it means |
47
- | ----: | ---: | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
48
- | 62 | 17 | `color-contrast` | text under 4.5:1 (SC 1.4.3) |
49
- | 28 | 16 | `target-size` | under 24×24 with no spacing exception (SC 2.5.8) — on `Carousel`, `Attachments`, `FilterBar` and `Toolbar`, at **every** width |
50
- | 21 | 6 | `aria-prohibited-attr` | an `aria-*` on a role that does not allow it |
51
- | 18 | 6 | `button-name` | **a button a screen reader announces as nothing** |
52
- | 18 | 6 | `aria-valid-attr-value` | an `aria-*` pointing at an id that is not there |
53
- | 12 | 3 | `aria-conditional-attr` | an `aria-*` that is invalid in the state it is in |
54
-
55
- 48 of those nodes are on `/isolate/**` — single components, this package's own output — and 111 on
56
- `/showcase/**`. None of the six rules exists in `scripts/visual-audit.mjs`. The `target-size` rows
57
- are the sharpest: our own `target-size-min` rule was running on those same frames and reporting them
58
- clean, because it measures a painted box.
42
+ ## What it found on its first run — and what came of it
43
+
44
+ 54 rows, **159 violation nodes**, on code that passed every other gate in this repository. Fixing
45
+ them took it to **12 rows / 19 nodes**, and then to **0**:
46
+
47
+ | rule | first run | now | what it was |
48
+ | ----------------------- | --------: | --: | ---------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `color-contrast` | 62 | 0 | text under 4.5:1 (SC 1.4.3) |
50
+ | `target-size` | 28 | 0 | under 24×24 (SC 2.5.8) — `Carousel` dots, `SearchInput`'s clear button, `Attachments`' file input |
51
+ | `aria-prohibited-attr` | 21 | 0 | `Badge`'s `aria-label` on a `generic` div — the name was dropped by every screen reader |
52
+ | `button-name` | 18 | 0 | **`Select`'s trigger is `role="combobox"`, which cannot be named by its contents** — every unlabelled Select shipped a nameless combobox |
53
+ | `aria-valid-attr-value` | 18 | 0 | `aria-controls` pointing at ids that do not exist |
54
+ | `aria-conditional-attr` | 12 | 0 | `aria-expanded` on a `<tr>`, which only a `treegrid` row may carry |
55
+
56
+ Four of those were LIBRARY defects, not demo slips — `Badge`, `Select`, `Carousel`, `SearchInput` —
57
+ and the fifth produced a new vocabulary member: `tone="inherit"` on `Text` / `Heading` / `Title` /
58
+ `Activity` / `Separator`, because every other tone is an absolute token and there was no way to put
59
+ text on a coloured surface without fighting it.
60
+
61
+ The `target-size` rows are the sharpest point about two rulers. Our own `target-size-min` was
62
+ running on those same frames and reporting them clean, because it measures a painted box. And the
63
+ fix axe wants is not the fix the standard wants: a centred `::after` genuinely makes the target
64
+ 24×24 — `docs/MEASUREMENT-CONTRACT.md` records that under `expanders` — but axe reads
65
+ `getBoundingClientRect()`, which cannot see a pseudo-element. Both fixes were tried; the boxes had
66
+ to grow.
67
+
68
+ ## The last twelve rows, and what each one turned out to be
69
+
70
+ The table above is what the sweep found; this is what the remaining 12 rows / 19 nodes turned out
71
+ to be once each was measured rather than reasoned about. The rows are kept, with the verdict this
72
+ file used to carry quoted back, because four of the five were wrong: two had no diagnosis at all
73
+ and two had one the measurement contradicted — and in both of those the guess pointed AWAY from
74
+ this package.
75
+
76
+ | rows | what | the measurement, and where it was fixed |
77
+ | ---: | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | 3 | `foundation-colors` · `--success-foreground` on `--success`, `--attention-foreground` on `--attention` | Called "a PALETTE decision, an owner-level call". It was not the palette: the FILL was fine and the INK was wrong. Two of the light theme's eight fills carried a near-white ink their own lightness cannot support (2.19:1 / 3.32:1) while the sibling `--warning` had always used the near-black one on the equally pale 山吹, and the DARK theme already flipped both. `src/tokens/foundation.css`: ink only — success 2.19 → **7.08**, attention 3.32 → **4.68**. Guarded by `__tests__/status-fill-contrast.test.ts`. |
79
+ | 3 | `case6-agency-handy` · a SELECTED `ToggleGroupItem` paints `--foreground` over `--primary` | "Cause not yet found." Found, with `CSS.getMatchedStylesForNode`: the demo wrote `text-[var(--font-size-xs)]`, and Tailwind v4 reads a bare `var()` in `text-*` as a COLOUR — it compiled to `color: var(--font-size-xs)` in the UTILITIES layer, an invalid colour that clobbered the component's own `color: hsl(var(--primary-foreground))` in `components` and left the trigger inheriting the page ink. The component rule was right all along. Demo fix (`text-[length:var(…)]`, 10 sites in `docs/`), plus the count pill: the hand-rolled `Text` at `opacity-70` measured 2.15:1 and `ToggleGroupItem`'s own `count` prop inverts to 6.32:1. |
80
+ | 3 | `table-view-tabs` · a `Tab`'s `aria-controls` points at a `tabpanel` that is never rendered | A LIBRARY defect, and the composition is published: `Tabs` / `TabsList` / `TabsTrigger` are three separate exports, nothing in their types asks for a fourth, and the attribute could not be overridden (`withDomProps` puts RAC's props after the caller's). `TabsContent` now registers its value and a trigger claims a panel only when one is declared. No new public prop. `__tests__/tabs-panel-reference-643.test.tsx`. |
81
+ | 2 | `futurelastic-web` @320/@375 · white ghost buttons read as 1.01:1 | "Probably a false positive." It was not. The tenant's `.fl-navbar-inner` was a fixed-height flex row with no wrap, so at 320 its content ran to **x=569.6 inside a 320px nav** — the links painted outside the navbar's own box, over the page behind it. A WCAG 2.2 SC 1.4.10 reflow failure reported by the contrast rule, because that is what an overflowing element looks like from the outside. Demo CSS + `Flex wrap`. |
82
+ | 1 | `table-bulk-actions` @375 · `target-size` | "Not yet diagnosed." The left cluster carried `min-w-0 flex-1` — "squeeze me to nothing" — over children that refuse to shrink, so its box shrank to 113.6px while its content ran to x=462 and painted 一括承認 (264.6→360.2) under 解除 (272.3→341.0), leaving 19.2×30.2 unobscured against SC 2.5.8's 24×24. Demo: `wrap`, and no `min-w-0`. |
83
+
84
+ Two of the five were library defects and three were genuine demo slips — a different split from the
85
+ first sweep, where four of five were the library's. Which is the point of writing the number down
86
+ next to each one: "it is the demo" and "it is the palette" are both cheap to say and neither
87
+ survived a measurement here.
59
88
 
60
89
  ## The baseline is a debt ledger, not an allowlist
61
90
 
@@ -69,7 +69,10 @@ function SwatchTile({ swatch, text }: { swatch: Swatch; text?: boolean }) {
69
69
  {/* ui-audit-disable-next-line no-hand-rolled-surface no-utility-layout no-utility-spacing — a swatch showing the token itself; the fill IS the subject */}
70
70
  <div className={`border-border flex h-14 items-end rounded-md border p-2 ${swatch.cls}`}>
71
71
  {text ? (
72
- <Text size="xs" mono className="leading-none">
72
+ /* `tone="inherit"` — the specimen's whole job is to show the token's PAIRED foreground,
73
+ and every other tone is an absolute colour. Without it `Text` repainted itself in
74
+ `--foreground` over `--primary` and measured 2.45:1 on this very page (gh#643). */
75
+ <Text size="xs" mono tone="inherit" className="leading-none">
73
76
  Aa 亜
74
77
  </Text>
75
78
  ) : null}
@@ -125,6 +125,69 @@ export default function Demo() {
125
125
  </Flex>
126
126
  </CardContent>
127
127
  </Card>
128
+
129
+ <Card>
130
+ <CardHeader>
131
+ <CardTitle level={2}>Product suffix — 「GoDX | ID」</CardTitle>
132
+ <CardDescription>
133
+ productSuffix は製品名を lockup の内側に置きます。マスターは
134
+ mark=&quot;godx-lockup&quot; の 1 つだけで、区切り線・その前後の間隔・size
135
+ スケール・light/dark
136
+ の切替はすべてパッケージのトークンが所有します(--logo-divider-color / -width /
137
+ -height / -alpha、--logo-product-suffix-gap / -color /
138
+ -font-weight)。製品名は「描いた図形」ではなく wordmark と同じ組版
139
+ (--logo-wordmark-font-size-* /
140
+ -font-family)です。別座標系のマスターを増やさず、製品が増えても
141
+ リリースが要らず、ローカライズできるためです。 区切り線の色はロゴタイプの ink
142
+ (--logo-godx-ink-color)の 0.25 alpha で、light では #c1c2cb(1.74:1、 公式キットの
143
+ #c5c8d6 = 1.64:1 とほぼ同じ)、dark では #515150(2.30:1)。 キットの固定値と違い dark
144
+ の対応があります。
145
+ </CardDescription>
146
+ </CardHeader>
147
+ <CardContent>
148
+ <Flex direction="col" gap="md">
149
+ {/* size は suffix にも効く — マークもロゴタイプも製品名も同じティアで動きます。 */}
150
+ <Flex direction="row" align="center" wrap gap="lg">
151
+ <Logo mark="godx-lockup" productSuffix="ID" size="xs" />
152
+ <Logo mark="godx-lockup" productSuffix="ID" size="sm" />
153
+ <Logo mark="godx-lockup" productSuffix="ID" />
154
+ <Logo mark="godx-lockup" productSuffix="ID" size="lg" />
155
+ </Flex>
156
+ {/* 将来の製品名も同じ API。追加のアセットもリリースも要りません。 */}
157
+ <Flex direction="row" align="center" wrap gap="lg">
158
+ <Logo mark="godx-lockup" productSuffix="Console" />
159
+ <Logo mark="godx-lockup" productSuffix="Admin" />
160
+ <Logo mark="godx-lockup" productSuffix="管理コンソール" />
161
+ </Flex>
162
+ {/* 描かれた "GoDX" は sr-only テキストで補われ、名前は "GoDX ID" になります。
163
+ * role="img" を使わないのは、asChild でリンクにしたとき link ロールを
164
+ * 上書きしてしまうためです。 */}
165
+ <Flex direction="col" gap="sm">
166
+ <Logo mark="godx-lockup" productSuffix="ID" size="lg" />
167
+ <Text size="2xs" mono tone="muted">
168
+ {`mark="godx-lockup" productSuffix="ID" → accessible name "GoDX ID"`}
169
+ </Text>
170
+ </Flex>
171
+ {/* 組版の wordmark と併用すると、suffix はその後ろに入ります。 */}
172
+ <Flex direction="col" gap="sm">
173
+ <Logo glyph="c" wordmark="CoreBooks" productSuffix="Admin" />
174
+ <Text size="2xs" mono tone="muted">
175
+ {`glyph + wordmark + productSuffix → name "CoreBooks Admin"`}
176
+ </Text>
177
+ </Flex>
178
+ {/* suffix がノードでも名前は壊れません。label は従来どおり上書きできます。 */}
179
+ <Logo
180
+ mark="godx-lockup"
181
+ productSuffix={<Text size="xs">ID</Text>}
182
+ label="GoDX ID ホーム"
183
+ />
184
+ {/* リンク化も同じ。lockup そのものが <a> になります。 */}
185
+ <Logo asChild mark="godx-lockup" productSuffix="ID">
186
+ <a href="#logo-product-suffix" />
187
+ </Logo>
188
+ </Flex>
189
+ </CardContent>
190
+ </Card>
128
191
  </Flex>
129
192
  </PageContainer>
130
193
  );
@@ -94,6 +94,37 @@ export default function Demo() {
94
94
  </CardContent>
95
95
  </Card>
96
96
 
97
+ <Card>
98
+ <CardHeader>
99
+ <CardTitle level={2}>tone=&quot;inherit&quot; · 面が決める</CardTitle>
100
+ <CardDescription>
101
+ 他の tone は絶対的なトークンを塗るため、色つきの面に置いた途端にページの文字色へ戻って
102
+ しまいます。inherit はその面が持つ対になる前景色をそのまま使う唯一のメンバーです。
103
+ これを入れる前、--primary の上の Text は --foreground を塗って 2.45:1
104
+ でした(gh#643)。
105
+ </CardDescription>
106
+ </CardHeader>
107
+ <CardContent>
108
+ <Flex direction="col" gap="md">
109
+ {/* ui-audit-disable-next-line no-hand-rolled-surface no-utility-layout no-utility-spacing — the COLOURED SURFACE is the subject here, exactly as in docs/foundation/colors.tsx */}
110
+ <div className="bg-primary text-primary-foreground rounded-md p-4">
111
+ <Flex direction="col" gap="xs">
112
+ <Heading level={3} tone="inherit">
113
+ 請求サマリー
114
+ </Heading>
115
+ <Text tone="inherit">今月の請求は締め処理まで完了しています。</Text>
116
+ <Text size="2xs" mono tone="inherit">
117
+ {`tone="inherit"`}
118
+ </Text>
119
+ </Flex>
120
+ </div>
121
+ <Text size="xs" tone="muted">
122
+ 同じ二行を tone=&quot;default&quot; で置くと、面ではなくページの文字色を塗ります。
123
+ </Text>
124
+ </Flex>
125
+ </CardContent>
126
+ </Card>
127
+
97
128
  <Card>
98
129
  <CardHeader>
99
130
  <CardTitle level={2}>Text sizes</CardTitle>
@@ -1,9 +1,15 @@
1
1
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
2
+ import { Logo } from "@godxjp/ui/general";
2
3
  import { AuthAccountSummary, AuthIdentity, AuthStack, PageContainer } from "@godxjp/ui/layout";
3
4
 
4
5
  /**
5
6
  * AuthIdentity — canonical identity title with optional relying-party request context. The
6
7
  * requester line is consumer data and may be omitted for first-party authentication.
8
+ *
9
+ * `brand` is the product's own artwork, in the MARK's place: the h1 stays painted and keeps naming
10
+ * the block, and the artwork is decorative whatever fills it — so a lockup that spells "GoDX | ID"
11
+ * is not announced on top of the heading. When the lockup already carries the product name, make
12
+ * `title` the SCREEN'S purpose ("サインイン") rather than repeating the brand.
7
13
  */
8
14
  export default function Demo() {
9
15
  return (
@@ -31,6 +37,34 @@ export default function Demo() {
31
37
  </AuthStack>
32
38
  </CardContent>
33
39
  </Card>
40
+
41
+ <Card>
42
+ <CardHeader>
43
+ <CardTitle level={2}>brand · 製品ロックアップ</CardTitle>
44
+ <CardDescription>
45
+ brandを渡すとパッケージのマークが製品のロックアップに置き換わります。h1はtitleを読み上げ名
46
+ として保持し、ロックアップは装飾扱いになるため製品名は一度だけ読まれます。ロックアップが製品名
47
+ を含む場合、titleには画面の目的を入れてください。
48
+ </CardDescription>
49
+ </CardHeader>
50
+ <CardContent>
51
+ <AuthStack>
52
+ <AuthIdentity
53
+ title="サインイン"
54
+ brand={<Logo mark="godx-lockup" productSuffix="ID" />}
55
+ />
56
+ <AuthIdentity
57
+ title="サインイン"
58
+ brand={<Logo mark="godx-lockup" productSuffix="ID" size="lg" />}
59
+ requester="勤怠管理が認証を要求しています"
60
+ />
61
+ <AuthIdentity
62
+ title="サインイン"
63
+ brand={<Logo glyph="a" wordmark="Acme" productSuffix="ID" />}
64
+ />
65
+ </AuthStack>
66
+ </CardContent>
67
+ </Card>
34
68
  </PageContainer>
35
69
  );
36
70
  }
@@ -80,7 +80,9 @@ const THEME = `
80
80
  [data-tenant="acme-web"] .tx-h2 { font-family: var(--font-family-display); font-weight: 800; font-size: 2.25rem;
81
81
  line-height: 1.15; letter-spacing: -0.02em; margin: 0.75rem 0 0.625rem; color: hsl(var(--foreground)); }
82
82
  [data-tenant="acme-web"] .tx-lead { font-size: 1.1875rem; line-height: 1.6; color: hsl(var(--muted-foreground)); max-width: 480px; }
83
- [data-tenant="acme-web"] .tx-gold { color: hsl(var(--primary)); }
83
+ /* --text-brand, not --primary (gh#643): --primary is a FILL role, and as ink it measured
84
+ 2.01:1 here. The AA-safe text tier already exists and .tx-eyebrow above already reads it. */
85
+ [data-tenant="acme-web"] .tx-gold { color: hsl(var(--text-brand)); }
84
86
  [data-tenant="acme-web"] .tx-stat { font-family: var(--font-family-display); font-weight: 800; font-size: 1.75rem; color: hsl(var(--primary)); }
85
87
  [data-tenant="acme-web"] .tx-hero-grid { display: grid; gap: 3rem; align-items: center; padding-block: 6rem; }
86
88
  @media (min-width: 1024px) { [data-tenant="acme-web"] .tx-hero-grid { grid-template-columns: 1.1fr 0.9fr; } }
@@ -283,11 +283,11 @@ export default function LoginShowcase() {
283
283
 
284
284
  <Text as="p" size="2xs" tone="muted" align="center" className="leading-relaxed">
285
285
  ログインすると{" "}
286
- <Button variant="link" size="sm" className="text-[var(--font-size-2xs)]">
286
+ <Button variant="link" size="sm" className="text-[length:var(--font-size-2xs)]">
287
287
  利用規約
288
288
  </Button>{" "}
289
289
  ·{" "}
290
- <Button variant="link" size="sm" className="text-[var(--font-size-2xs)]">
290
+ <Button variant="link" size="sm" className="text-[length:var(--font-size-2xs)]">
291
291
  プライバシーポリシー
292
292
  </Button>{" "}
293
293
  に同意したものとみなされます。
@@ -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" />