@godxjp/ui 28.12.0 → 29.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 (178) hide show
  1. package/agent/START-HERE.md +29 -10
  2. package/agent/components/Anchor.json +6 -1
  3. package/agent/components/AppLauncher.json +10 -0
  4. package/agent/components/AppShell.json +1 -1
  5. package/agent/components/AreaChart.json +19 -1
  6. package/agent/components/Attachments.json +26 -1
  7. package/agent/components/BarChart.json +11 -1
  8. package/agent/components/BranchScopePicker.json +10 -0
  9. package/agent/components/Cascader.json +6 -1
  10. package/agent/components/Checkbox.json +6 -0
  11. package/agent/components/CompactBarTrend.json +1 -1
  12. package/agent/components/CredentialReveal.json +21 -0
  13. package/agent/components/DataState.json +1 -1
  14. package/agent/components/DataTable.json +4 -4
  15. package/agent/components/FormField.json +1 -1
  16. package/agent/components/InfiniteQueryState.json +1 -1
  17. package/agent/components/Input.json +1 -1
  18. package/agent/components/InputOTP.json +35 -0
  19. package/agent/components/LineChart.json +20 -2
  20. package/agent/components/ListRow.json +1 -1
  21. package/agent/components/Masonry.json +1 -1
  22. package/agent/components/MasterDetail.json +1 -1
  23. package/agent/components/PasswordStrength.json +1 -1
  24. package/agent/components/PermissionMatrix.json +6 -1
  25. package/agent/components/SearchInput.json +5 -0
  26. package/agent/components/Select.json +1 -0
  27. package/agent/components/ServiceRolePanel.json +5 -0
  28. package/agent/components/Sidebar.json +1 -1
  29. package/agent/components/Switch.json +6 -0
  30. package/agent/components/Table.json +8 -3
  31. package/agent/components/Tabs.json +10 -0
  32. package/agent/components/ThemeScope.json +49 -0
  33. package/agent/components/TimeRangePicker.json +5 -0
  34. package/agent/components/Topbar.json +1 -0
  35. package/agent/components/TopbarItem.json +2 -1
  36. package/agent/components/Transfer.json +6 -1
  37. package/agent/components/TreeSelect.json +1 -1
  38. package/agent/components/Upload.json +5 -0
  39. package/agent/components/UploadCropDialog.json +1 -1
  40. package/agent/components/formatDate.json +1 -1
  41. package/agent/components-index.json +5 -0
  42. package/agent/components.json +297 -31
  43. package/agent/index.json +19 -9
  44. package/agent/llms.txt +10 -10
  45. package/agent/patterns/tenant-brand-color.json +28 -0
  46. package/agent/patterns-index.json +27 -0
  47. package/agent/patterns.json +28 -0
  48. package/agent/rules.json +15 -0
  49. package/agent/tokens.json +4965 -970
  50. package/dist/app/index.d.ts +3 -0
  51. package/dist/app/index.js +3 -0
  52. package/dist/app/tenant-theme.d.ts +80 -0
  53. package/dist/app/tenant-theme.js +154 -0
  54. package/dist/app/theme-axes.d.ts +14 -1
  55. package/dist/app/theme-axes.js +24 -31
  56. package/dist/components/charts/chart-cartesian.d.ts +5 -1
  57. package/dist/components/charts/chart-cartesian.js +15 -8
  58. package/dist/components/data-display/badge.d.ts +1 -1
  59. package/dist/components/data-display/badge.js +20 -2
  60. package/dist/components/data-display/carousel.js +4 -4
  61. package/dist/components/data-display/data-table.js +13 -2
  62. package/dist/components/data-display/permission-matrix.js +1 -1
  63. package/dist/components/data-display/table.d.ts +11 -2
  64. package/dist/components/data-display/table.js +18 -2
  65. package/dist/components/data-entry/control-appearance.d.ts +12 -6
  66. package/dist/components/data-entry/control-appearance.js +1 -1
  67. package/dist/components/data-entry/select.js +4 -3
  68. package/dist/components/feedback/dialog.js +6 -3
  69. package/dist/components/feedback/overlay-header-tone.d.ts +7 -0
  70. package/dist/components/feedback/overlay-header-tone.js +4 -4
  71. package/dist/components/feedback/sheet.d.ts +1 -1
  72. package/dist/components/feedback/sheet.js +6 -9
  73. package/dist/components/feedback/sonner.js +16 -3
  74. package/dist/components/general/button.js +22 -5
  75. package/dist/components/layout/affix.js +15 -1
  76. package/dist/components/layout/sidebar.js +7 -1
  77. package/dist/components/navigation/anchor.d.ts +1 -1
  78. package/dist/components/navigation/anchor.js +5 -4
  79. package/dist/components/navigation/app-setting-picker.js +1 -1
  80. package/dist/components/navigation/pagination.js +1 -1
  81. package/dist/components/navigation/tabs.js +15 -2
  82. package/dist/components/query/infinite-query-state.d.ts +22 -6
  83. package/dist/contracts/measurement.json +1 -1
  84. package/dist/i18n/messages/en.json +0 -697
  85. package/dist/i18n/messages/ja.json +0 -691
  86. package/dist/i18n/messages/vi.json +0 -691
  87. package/dist/lib/control-styles.d.ts +31 -11
  88. package/dist/lib/control-styles.js +6 -6
  89. package/dist/lib/overlay-portal.d.ts +20 -0
  90. package/dist/lib/overlay-portal.js +93 -0
  91. package/dist/props/components/app.prop.d.ts +12 -0
  92. package/dist/props/components/charts.prop.d.ts +30 -0
  93. package/dist/props/components/index.d.ts +1 -1
  94. package/dist/props/components/navigation.prop.d.ts +21 -2
  95. package/dist/props/components/query.prop.d.ts +36 -2
  96. package/dist/props/registry.d.ts +46 -1
  97. package/dist/props/registry.js +38 -3
  98. package/dist/styles/alert-layout.css +34 -14
  99. package/dist/styles/badge-layout.css +10 -6
  100. package/dist/styles/base.css +14 -5
  101. package/dist/styles/card-layout.css +19 -8
  102. package/dist/styles/chart-layout.css +22 -3
  103. package/dist/styles/control.css +165 -59
  104. package/dist/styles/data-display-layout.css +129 -36
  105. package/dist/styles/data-entry-layout.css +23 -87
  106. package/dist/styles/dialog-layout.css +49 -19
  107. package/dist/styles/float-button-layout.css +5 -5
  108. package/dist/styles/focus-ring.css +9 -5
  109. package/dist/styles/layout.css +42 -15
  110. package/dist/styles/logo-layout.css +1 -1
  111. package/dist/styles/motion.css +1 -1
  112. package/dist/styles/navigation-layout.css +90 -29
  113. package/dist/styles/shell-layout.css +63 -40
  114. package/dist/styles/table-layout.css +56 -17
  115. package/dist/styles/text-layout.css +13 -4
  116. package/dist/styles/toggle.css +8 -2
  117. package/dist/tokens/components/actions.css +1 -1
  118. package/dist/tokens/components/attachments.css +4 -4
  119. package/dist/tokens/components/badge.css +4 -4
  120. package/dist/tokens/components/callout.css +1 -1
  121. package/dist/tokens/components/card.css +9 -4
  122. package/dist/tokens/components/chart.css +10 -1
  123. package/dist/tokens/components/chat-bubble.css +1 -1
  124. package/dist/tokens/components/control.css +28 -10
  125. package/dist/tokens/components/conversations.css +2 -1
  126. package/dist/tokens/components/data-display.css +12 -7
  127. package/dist/tokens/components/descriptions.css +1 -1
  128. package/dist/tokens/components/draggable-panel.css +1 -1
  129. package/dist/tokens/components/feedback.css +28 -8
  130. package/dist/tokens/components/float-button.css +1 -1
  131. package/dist/tokens/components/legal-document.css +1 -1
  132. package/dist/tokens/components/logo.css +1 -1
  133. package/dist/tokens/components/mega-menu.css +5 -3
  134. package/dist/tokens/components/navigation.css +21 -7
  135. package/dist/tokens/components/segmented.css +8 -3
  136. package/dist/tokens/components/shell.css +19 -5
  137. package/dist/tokens/components/table.css +9 -1
  138. package/dist/tokens/components/thought-chain.css +1 -1
  139. package/dist/tokens/components/toggle.css +2 -0
  140. package/dist/tokens/components/tree.css +3 -1
  141. package/dist/tokens/components/upload.css +6 -6
  142. package/dist/tokens/components/welcome.css +1 -1
  143. package/dist/tokens/foundation.css +28 -1
  144. package/docs/COMPOSITION-VS-COMPONENT.md +31 -0
  145. package/docs/CUSTOMER-THEMING.md +637 -1
  146. package/docs/DESIGN-AUTHORITY.md +13 -0
  147. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  148. package/docs/GLASSMORPHISM-STANDARD.md +196 -0
  149. package/docs/THEME-API-COVERAGE.md +538 -0
  150. package/docs/TOKEN-RESOLUTION.md +195 -0
  151. package/docs/TOKENS.md +63 -24
  152. package/docs/asset-modules.d.ts +7 -0
  153. package/docs/data-display/charts.tsx +80 -0
  154. package/docs/data-display/data-table/index.tsx +30 -0
  155. package/docs/data-display/popover.tsx +1 -1
  156. package/docs/data-display/table.tsx +52 -0
  157. package/docs/feedback/sheet.tsx +10 -10
  158. package/docs/foundation/density.tsx +4 -4
  159. package/docs/i18n/messages/en.json +1201 -0
  160. package/docs/i18n/messages/ja.json +1195 -0
  161. package/docs/i18n/messages/vi.json +1195 -0
  162. package/docs/layout/account-chip.tsx +2 -2
  163. package/docs/layout/responsive-grid.tsx +1 -1
  164. package/docs/navigation/toolbar.tsx +20 -12
  165. package/docs/providers/theme-scope.tsx +186 -0
  166. package/docs/showcase/caimono-price-comparison.tsx +911 -0
  167. package/docs/showcase/case4-login.tsx +2 -2
  168. package/docs/showcase/marketing-page.tsx +3 -2
  169. package/docs/showcase/permission-matrix.tsx +13 -5
  170. package/docs/showcase/table-pagination.tsx +2 -1
  171. package/docs/showcase/tenant-brand-color.tsx +338 -0
  172. package/docs/showcase/theme-customization.tsx +2 -1
  173. package/docs/showcase/theme-lab.tsx +2125 -0
  174. package/docs/themes/flat.css +462 -0
  175. package/docs/themes/glassmorphism.css +958 -0
  176. package/docs/themes/index.ts +200 -0
  177. package/package.json +4 -3
  178. package/scripts/explain-token.mjs +382 -0
@@ -0,0 +1,195 @@
1
+ # Token resolution — who overrides whom, and how to prove it
2
+
3
+ This document exists because of one complaint: _"rất nhiều chỗ cứ đè cấu hình lung tung làm ảnh
4
+ hưởng component này sang component khác"_ — configuration overriding configuration until changing
5
+ one component moves another. That is a real failure mode and prose does not settle it, so this
6
+ page is **an order, a rule, and a tool**. The tool is `scripts/explain-token.mjs`.
7
+
8
+ Nothing here is invented. Ant Design v5 settled this problem years ago and its vocabulary is used
9
+ throughout; where this package lacks one of Ant Design's layers, that is said plainly rather than
10
+ papered over with a new word.
11
+
12
+ ---
13
+
14
+ ## 1. The order
15
+
16
+ **Yes, the chain is what you said.** For a value at a given element, strongest first:
17
+
18
+ | # | layer | in this package | Ant Design equivalent |
19
+ | --- | ----------------- | -------------------------------------------------------------- | ------------------------------------------ |
20
+ | 1 | **instance** | `style={{ "--card-radius": "…" }}` on the element | a component prop / `<Component style>` |
21
+ | 2 | **nearest scope** | `[data-tenant]`, `.dark`, `[data-density]`, any region wrapper | the **nearest** `ConfigProvider theme` |
22
+ | 3 | **global** | `:root` in the app's own `theme.css` | the outermost `ConfigProvider theme.token` |
23
+ | 4 | **default** | `:root` in `src/tokens/**` | `theme.defaultAlgorithm` seed |
24
+
25
+ There is no bespoke machinery here: a CSS custom property is resolved by the ordinary cascade, so
26
+ "nearest scope wins" is just inheritance, and "the app's `theme.css` beats the package" is just the
27
+ layer contract — **unlayered CSS outranks every `@layer`**, and everything this package ships is
28
+ layered (`docs/TOKENS.md` · The layer contract).
29
+
30
+ Between 2 and 3 there is no ambiguity to resolve: an override on a wrapper is _nearer_ to the
31
+ element than `:root`, so it wins by inheritance, not by specificity. Two scopes on the same element
32
+ are ordinary cascade — higher specificity, then later.
33
+
34
+ ## 2. The four tiers — Ant Design's model, and the one layer we do not have
35
+
36
+ Ant Design v5: **Seed → Map → Alias → Component**. This package:
37
+
38
+ | Ant Design | here | file | count |
39
+ | ------------------------------------ | ----------- | ----------------------------- | ----- |
40
+ | **Seed Token** | foundation | `src/tokens/foundation.css` | 201 |
41
+ | **Map Token** (derived by algorithm) | — _partial_ | `src/tokens/derived.css` | — |
42
+ | **Alias Token** | semantic | `src/tokens/semantic/*.css` | 92 |
43
+ | **Component Token** | component | `src/tokens/components/*.css` | 1688 |
44
+
45
+ **The Map layer is PARTIAL, not missing** — an earlier draft of this page said "missing" and Codex
46
+ was right to reject it. `derived.css` really is a Map layer for the brand ramp: `--ring`,
47
+ `--primary-hover` and `--primary-active` are computed from `--primary` with relative colour, which
48
+ is derivation by algorithm in the sense Ant Design means. Foundation mixes seed and derived too —
49
+ the spacing steps are `calc(<n> * var(--scaling))` (`foundation.css:673`) and the shadows derive
50
+ from `--shadow-color` (`foundation.css:290`).
51
+
52
+ What is genuinely thin is its **reach**: the hover knobs are themselves `initial`, the destructive
53
+ states are authored literals (`derived.css:136`), and the `@supports not (color: hsl(from …))`
54
+ branch hands older engines literals that stop following the seed altogether (`derived.css:188`).
55
+ That last one is why `tenantTheme()` (gh#868) computes hover/pressed in JavaScript — not because
56
+ there was no algorithm layer, but because the one that exists does not survive an engine without
57
+ relative colour.
58
+
59
+ A previous draft also claimed the thin Map layer is _why_ the component tier needs 1688 tokens.
60
+ That is unsupported and has been removed: deriving defaults does not remove the need for
61
+ independently overridable component knobs, which is what most of those 1688 are.
62
+
63
+ ## 3. The rule that makes the order work — and silently breaks it
64
+
65
+ `var()` substitutes **where it is declared**, not where it is read.
66
+
67
+ ```css
68
+ /* BROKEN — the chain is dead at step 2 */
69
+ :root {
70
+ --card-border-color: var(--border);
71
+ }
72
+ .ui-card {
73
+ border-color: var(--card-border-color);
74
+ }
75
+ ```
76
+
77
+ `--card-border-color` resolves against the **root's** `--border`, once. A `[data-tenant]` below
78
+ root that sets its own `--border` can never reach it: the binding already happened higher up. Step
79
+ 2 of the chain exists, and for that token it does nothing.
80
+
81
+ ```css
82
+ /* CORRECT — knob is `initial`, formula at the CALL SITE */
83
+ :root {
84
+ --card-border-color: initial;
85
+ }
86
+ .ui-card {
87
+ border-color: var(--card-border-color, hsl(var(--border)));
88
+ }
89
+ ```
90
+
91
+ Now the fallback is evaluated **at `.ui-card`**, where the scope is in effect, so the tenant's
92
+ `--border` is the one that arrives — and an explicit `--card-border-color` still overrides it.
93
+
94
+ This package has paid for that distinction seven times: gh#687, gh#843, gh#848, gh#866 and others.
95
+ It is the single most common cause of "I overrode the token and nothing happened."
96
+
97
+ ## 4. The tool
98
+
99
+ ```
100
+ node node_modules/@godxjp/ui/scripts/explain-token.mjs --card-radius # one token: every declaration, every read
101
+ node node_modules/@godxjp/ui/scripts/explain-token.mjs --table # a whole family
102
+ node node_modules/@godxjp/ui/scripts/explain-token.mjs --audit # every freeze, orphan and unpublished token
103
+ ```
104
+
105
+ (From inside this repo's own checkout, drop the `node_modules/@godxjp/ui/` prefix.)
106
+
107
+ For one token it prints every declaration site — marked `root-only` or `below root` — with its
108
+ selector, its value, and whether it is a freeze; then every read and whether that read carries a
109
+ call-site fallback. If two components move together, run it on the token they share and the shared
110
+ declaration is on the screen.
111
+
112
+ **It does not compute a winner, and says so.** The first version ranked selectors into four
113
+ "cascade" buckets by regex and printed them strongest-last. Codex found that `@theme inline` and
114
+ `[dir="rtl"] .ui-actions[data-fade-in-inline]` both scored top precedence on the substring
115
+ `inline`, that `:root[data-brand="crm"]` was filed as a descendant scope, and that "strongest last"
116
+ sorted by alphabetical filename. A tool meant to settle override disputes that invents precedence
117
+ is worse than no tool. `root-only` is the one property it can prove, and it is the only one the
118
+ freeze test needs. `--audit` ends with the four things it cannot see; read them before treating a
119
+ clean run as proof.
120
+
121
+ ### What `--audit` reports today
122
+
123
+ ```
124
+ 2090 declared · 1973 published · 641 frozen · 6 orphan reads
125
+ ```
126
+
127
+ - **frozen (641)** — a root-only binding whose source is restated somewhere below root. The naive
128
+ test ("any `:root` binding that reads a token") reports **1028**, and a list that long is one
129
+ nobody reads; `--actions-gap: var(--space-1)` only matters because `--space-1` really is
130
+ restated. The scoped set is derived on every run, never hand-listed — a hand-kept list is what
131
+ went blind in gh#854. It counts **any** declaration below root, not just theme-looking scopes:
132
+ `.ui-page-container` inside `@media (max-width: 720px)` restates `--space-section-active`
133
+ (`layout.css:992`) while `--card-space-inset` binds it at `:root` (`card.css:6`), so below 720px
134
+ a Card keeps the root's inset. An earlier, narrower version of this test missed exactly that.
135
+ - **orphan reads (6)** — `var(--x)` where `--x` is declared nowhere and no fallback is given.
136
+ - **unpublished (117)** — declared in CSS but absent from `agent/tokens.json`, so a consumer cannot
137
+ discover them. From the consumer's point of view these are hard-coded. `--card-accent-color` is
138
+ one: six call-site declarations, zero documentation.
139
+
140
+ ## 5. Rules for anyone adding or changing a token
141
+
142
+ 1. **Declare it in the tier it belongs to.** Component tokens go in `src/tokens/components/<name>.css`,
143
+ named `--{component}-{part}-{property}`; `check:token-tiers` enforces the shape.
144
+ 2. **A knob that mirrors a role is `initial` + a call-site fallback.** Never a `:root` binding.
145
+ See §3. If you are unsure whether it mirrors a role, run `explain-token.mjs` on the role and see
146
+ whether any scope restates it.
147
+ 3. **Do not write another component's token from your own rule — with one qualified exception.**
148
+ Writing `--badge-*` from a Card rule is the literal shape of "changing one component moves
149
+ another," and that is the default answer.
150
+
151
+ The exception is a **composition default**: a child whose geometry legitimately differs when it
152
+ sits inside a particular parent. `control.css:313` does exactly this — a count Badge inside a
153
+ boxed Button gets `--badge-space-y: 0` and a tighter radius, because a chip in a button is not a
154
+ standalone status chip. That is correct, and an earlier draft of this rule would have forbidden
155
+ it.
156
+
157
+ To qualify, all three must hold: the boundary is **documented** at the rule; the child's tokens
158
+ are **public**, so the relationship is inspectable; and a per-instance override on the child
159
+ still **wins**. The descendant selector deserves scrutiny of its own — `control.css:313` reaches
160
+ every nested Badge, not just a direct child — but that is a scoping question, not grounds to ban
161
+ composition.
162
+
163
+ 4. **Publish it.** A token absent from `agent/tokens.json` is not part of the API, whatever the
164
+ stylesheet says. `pnpm regen` does this; `check:agent-catalog` verifies it.
165
+ 5. **A consumer sets tokens, never selectors.** App CSS targeting `[data-slot]`, `[data-priority]`
166
+ or `.ui-*` is unlayered and therefore outranks every package layer at every width, including the
167
+ responsive re-points — which is how a page-local fix becomes a library-wide regression.
168
+ 6. **A theme that makes a surface translucent owns its own `prefers-reduced-transparency` and
169
+ `@supports not (backdrop-filter)` fallbacks.** The library cannot write them on the theme's
170
+ behalf — it does not know what opaque colour the theme wants, and guessing at one role's own
171
+ colour is not safe: `docs/themes/glassmorphism.css` sets `--card: 0 0% 100% / 42%`, so a fallback
172
+ that fell back to `hsl(var(--card))` would still be 42% translucent. Both branches stay
173
+ custom-property overrides on the theme's own selector, same shape as rule 5.
174
+
175
+ 7. **A scoped theme cannot re-ink text it does not own the `color` of.** `src/styles/base.css`
176
+ sets `color: hsl(var(--foreground))` on `body`, above every scope, so that `var()` substitutes
177
+ once against the root and everything below inherits the resolved colour — the §3 freeze rule
178
+ applied to a PROPERTY rather than a token, and worse there, because a token can be given a knob
179
+ and `color` on `body` has none to give. `ThemeScope` re-states it on itself and on the
180
+ body-level overlay host, so a region wrapped in one re-inks correctly (gh#881). **A plain
181
+ wrapper themed by a stylesheet must state `color: hsl(var(--foreground))` on its own element
182
+ too** — that is the consumer's own element, not a selector into this package, so rule 5 permits
183
+ it.
184
+
185
+ ## 6. What is not yet true
186
+
187
+ The owner's second question was whether **every smallest element** is configurable. It is not, and
188
+ the measurement is in `docs/THEME-API-COVERAGE.md`. Two known shapes of failure:
189
+
190
+ - a painted property written as a literal, with no token at all;
191
+ - a property written as `var(--x)` where `--x` is unpublished — reachable in principle, invisible
192
+ in practice.
193
+
194
+ Both are counted there per component. This page describes how resolution _works_; that one says how
195
+ far it currently _reaches_.
package/docs/TOKENS.md CHANGED
@@ -44,17 +44,18 @@ src/styles/
44
44
 
45
45
  Default brand tokens use the GodX Agent Portal palette: navy primary, 朱 orange focus/accent, warm neutral surfaces. App or customer identity colors belong in the consuming app theme, not in package tokens.
46
46
 
47
- #### The three tone tiers — FILL, TEXT, MARK
47
+ #### The four tone tiers — FILL, TEXT, MARK, SURFACE
48
48
 
49
- A status tone can be painted three ways, and each way is judged against a different thing. Reading
49
+ A status tone can be painted four ways, and each way is judged against a different thing. Reading
50
50
  the wrong tier is this palette's most expensive recurring bug, because nothing about it looks wrong
51
51
  in the source.
52
52
 
53
- | Tier | Tokens | What it paints | Contrast bar |
54
- | -------- | ------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- |
55
- | **FILL** | `--success`, `--warning`, `--info`, `--destructive` | A solid chip, band or bar with a label ON it | AA **4.5:1** against its own `*-foreground` label |
56
- | **TEXT** | `--text-success`, `--text-warning`, `--text-info`, `--text-error` | Small coloured type — a StatCard delta, an outline badge label, a field's error line | AA **4.5:1** against the surface BEHIND it |
57
- | **MARK** | `--mark-success`, `--mark-warning`, `--mark-info`, `--mark-destructive`, `--mark-primary`, `--mark-attention` | A thin shape carrying meaning with nothing written on it — a `Card accent` rail, a `DataTable rowTone` rail | SC 1.4.11 **3:1** against the surface it sits on |
53
+ | Tier | Tokens | What it paints | Contrast bar |
54
+ | ----------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
55
+ | **FILL** | `--success`, `--warning`, `--info`, `--destructive` | A solid chip, band or bar with a label ON it | AA **4.5:1** against its own `*-foreground` label |
56
+ | **TEXT** | `--text-success`, `--text-warning`, `--text-info`, `--text-error` | Small coloured type — a StatCard delta, an outline badge label, a field's error line | AA **4.5:1** against the surface BEHIND it |
57
+ | **MARK** | `--mark-success`, `--mark-warning`, `--mark-info`, `--mark-destructive`, `--mark-primary`, `--mark-attention` | A thin shape carrying meaning with nothing written on it — a `Card accent` rail, a `DataTable rowTone` rail | SC 1.4.11 **3:1** against the surface it sits on |
58
+ | **SURFACE** | `--surface-success`, `--surface-warning`, `--surface-info`, `--surface-destructive` | The pale GROUND a toned surface paints — an `Alert` tint, a status `Badge` chip, a toned `DataTable` row, a toast, a `ChatBubble`, a toned Dialog/Sheet header, an `EmptyState` medallion | whatever sits ON it must clear AA **4.5:1** — see below |
58
59
 
59
60
  The MARK tier exists because both rails were reading FILL, and two of them were effectively
60
61
  invisible. Measured in Chromium on the light card: `Card accent="warning"` drew its 6px rail at
@@ -66,6 +67,44 @@ place and both follow — and since 3:1 is looser than 4.5:1, anything legible a
66
67
  a mark by construction. `--mark-attention` is the exception: there is no `--text-attention`, and
67
68
  the fill already clears the floor in both themes (3.32 / 6.75).
68
69
 
70
+ **The SURFACE tier is `initial` and has no default of its own (gh#866).** The other three tiers
71
+ hold a colour; this one holds a brand's ANSWER, and there is no single derived value it could
72
+ carry — Alert washes at 5%, Badge at 10%, the row tone at 6%, the medallion at 12%, the toast
73
+ `color-mix`es into `--popover`. So each surface keeps its own formula AT ITS CALL SITE, behind the
74
+ role:
75
+
76
+ ```css
77
+ background-color: var(--surface-success, hsl(var(--success) / var(--alert-bg-alpha)));
78
+ ```
79
+
80
+ Unset, every surface paints exactly what it painted before (measured in Chromium: 30/30 computed
81
+ values byte-identical). Set on a scope — `[data-tenant] { --surface-success: #E8F5EF }` — all nine
82
+ follow, because the formula lives where the paint happens rather than at `:root`; the same probe
83
+ measured 0/30 before the roles existed and 30/30 after.
84
+
85
+ The tier exists because DERIVATION TIES THE GROUND'S HUE TO THE INK. A brand kit that pairs
86
+ `--success #126342` with `--success-soft #E8F5EF` is not describing a derivation: #E8F5EF is a
87
+ separately chosen mint, not that green at any alpha, and the only move the system offered was to
88
+ lighten the success TEXT until its wash matched — damaging the ink's contrast to fix the colour of
89
+ the ground.
90
+
91
+ **A brand's ground is the brand's contrast problem, and these are the numbers to check.** Everything
92
+ this library paints ON a status surface reads the TEXT tier — the Alert icon and title, the Badge
93
+ label (`text-*-strong`), the toast ink — and on the kit's four grounds that measures success
94
+ **6.22:1** · warning **5.48:1** · error **6.64:1** · info **7.34:1**, with body ink
95
+ (`--foreground`) at 13.9–14.4:1. All four clear AA, so the role is safe to hand a brand.
96
+
97
+ The one thing that does NOT is the Callout RAIL, which is the FILL tier by design (a 4px rail at
98
+ the border alpha reads as a smudge): against the brand grounds it measures success **1.99:1** and
99
+ warning **1.62:1**, under SC 1.4.11's 3:1. That is inherited, not introduced — on today's derived
100
+ tint the same rail measures **2.12:1** and **1.69:1** — and the repair is the MARK tier, not a
101
+ lighter ground.
102
+
103
+ `primary` and `attention` have no SURFACE entry on purpose: a brand ships "soft" pairs for the four
104
+ statuses, and two more roles nobody asked for make the vocabulary harder to learn. Borders stay
105
+ derived too (`--alert-border-alpha`, `--chat-bubble-tone-border-alpha`) — the kit supplies no border
106
+ colours.
107
+
69
108
  Two guards, because one was not enough:
70
109
  `src/tokens/__tests__/tone-mark-contrast.test.ts` recomputes every tone × ground × theme off the
71
110
  committed tokens and reads the alias out of the CSS (so repointing a mark back at FILL fails the
@@ -75,20 +114,20 @@ gate had ever looked at one.
75
114
 
76
115
  **Progress and Legend joined the tier, and the wa-iro question is settled.** `.ui-legend-swatch`,
77
116
  `.ui-progress-bar` and `.ui-progress-segment` all read FILL as standalone graphics and all failed
78
- the same floor. The floor applies: nothing is written on a progress fill, so *where the colour
79
- stops* is the entire datum, and a slice of a partition carries its share of the whole with no words
117
+ the same floor. The floor applies: nothing is written on a progress fill, so _where the colour
118
+ stops_ is the entire datum, and a slice of a partition carries its share of the whole with no words
80
119
  on it — that is precisely a "graphical object required to understand the content". The hue loses.
81
120
  The two moved together because a swatch is a SAMPLE of the bar beside it; a key that is not the
82
121
  colour it is a key to is not a key.
83
122
 
84
- | surface | tone | ground | FILL (before) | MARK (after) |
85
- | --- | --- | --- | --- | --- |
86
- | `.ui-legend-swatch` | warning | light card | **1.74** | **5.90** |
87
- | `.ui-legend-swatch` | success | light card | **2.18** | **6.84** |
88
- | `.ui-legend-swatch` | destructive | dark card | **2.95** | **5.52** |
89
- | `.ui-progress-segment` | warning | light track | **1.60** | **5.41** |
90
- | `.ui-progress-segment` | success | light track | **2.00** | **6.28** |
91
- | `.ui-progress-segment` | destructive | dark track | **2.42** | **4.52** |
123
+ | surface | tone | ground | FILL (before) | MARK (after) |
124
+ | ---------------------- | ----------- | ----------- | ------------- | ------------ |
125
+ | `.ui-legend-swatch` | warning | light card | **1.74** | **5.90** |
126
+ | `.ui-legend-swatch` | success | light card | **2.18** | **6.84** |
127
+ | `.ui-legend-swatch` | destructive | dark card | **2.95** | **5.52** |
128
+ | `.ui-progress-segment` | warning | light track | **1.60** | **5.41** |
129
+ | `.ui-progress-segment` | success | light track | **2.00** | **6.28** |
130
+ | `.ui-progress-segment` | destructive | dark track | **2.42** | **4.52** |
92
131
 
93
132
  Worst case anywhere on the two routes after the move: **4.52:1**. `.ui-progress-bar` (meter and
94
133
  over-capacity) reads the same tokens as the slice, so a `tone="warning"` meter and a `warning`
@@ -101,10 +140,10 @@ failure — was on `text-destructive`, the FILL utility. So the same `tone` was
101
140
  `Alert` and near-invisible one line below it, in the field that caused it. Measured in Chromium on
102
141
  `/isolate/layout-auth-recovery-examples-mfa-challenge`:
103
142
 
104
- | surface | ground | FILL (before) | TEXT (after) |
105
- | --- | --- | --- | --- |
106
- | `.ui-form-field-note[role="alert"]` | dark card | **2.95** | **5.52** |
107
- | `.ui-form-field-note[role="alert"]` | light card | 6.16 | **7.21** |
143
+ | surface | ground | FILL (before) | TEXT (after) |
144
+ | ----------------------------------- | ---------- | ------------- | ------------ |
145
+ | `.ui-form-field-note[role="alert"]` | dark card | **2.95** | **5.52** |
146
+ | `.ui-form-field-note[role="alert"]` | light card | 6.16 | **7.21** |
108
147
 
109
148
  Only the DARK branch failed, and the light one passing is why it survived: the fill is tuned for a
110
149
  white label ON it, so it darkens on light grounds and lightens on dark ones — the opposite of what
@@ -114,13 +153,13 @@ two `ui-auth-shell` routes it had never loaded.
114
153
 
115
154
  **A theme that repoints `--secondary` owes `--progress-track-background`.** The track defaults to
116
155
  `hsl(var(--secondary))`, which is a pale neutral in the stock palette. `docs/showcase/acme-portal`
117
- repurposes `--secondary` as a navy *button* colour, so its bars were drawn on a near-black track
156
+ repurposes `--secondary` as a navy _button_ colour, so its bars were drawn on a near-black track
118
157
  and the mark fills measured 2.50 (success) / 2.90 (warning) on it. Naming the track explicitly is
119
158
  the fix — 6.49 / 5.59 after — not dragging the tier back.
120
159
 
121
160
  Three guards now, and each sees something the others cannot: the ratio tests in
122
161
  `tone-mark-contrast.test.ts` (rails against card/background, progress marks against the track);
123
- the *same file's* CSS-alias assertions, which fail if a rule is repointed at the fill tier and
162
+ the _same file's_ CSS-alias assertions, which fail if a rule is repointed at the fill tier and
124
163
  which also pin the swatch and the slice to the **same** token per tone; and `check:contrast`'s
125
164
  **thin fill** pass — added because adding `/isolate/data-display-progress` to that gate's route
126
165
  list on its own changed nothing at all. The graphic pass wants ≤24px on both axes and the rail
@@ -209,7 +248,7 @@ A system with only tier 1 turns every exception into a hack (`!important`, a glo
209
248
 
210
249
  An inline custom property wins by **inheritance proximity**, not specificity, so it beats the `:root` default without any weight games. What keeps the route open is that every icon rule in `src/styles` reads its token through `var()` with no baked literal — `src/tokens/__tests__/icon-size-scale.test.ts` asserts exactly that, and carries a shrink-only list of the rules that still bake a literal and are therefore unreachable from an app.
211
250
 
212
- The three left need tokens in `components/control.css` (`.ui-otp-separator-icon`) and `components/shell.css` (`.tb-icon-btn svg`, `.tb-chip-icon`). Note that `.tb-chip-icon`'s `1.125rem` is **not** a snap case even though 18px is off the scale: it is a whole pixel, and the box is a letter medallion (`display: grid`, `place-items: center`, a radius, `color: white`), not a stroked glyph — so it wants a `scale-exempt:` marker, not the nearest step. Off-scale and off-grid are different findings; decide each on what the icon actually is.
251
+ The two left need tokens in `components/control.css` (`.ui-otp-separator-icon`) and `components/shell.css` (`.tb-icon-btn svg`).
213
252
 
214
253
  **Before you add a step:** if a value is wanted in two places it belongs on the scale, and if it is wanted in one it does not. Adding a tenth step to serve a single call site is how a scale stops meaning anything.
215
254
 
@@ -44,3 +44,10 @@ declare module "*.gif" {
44
44
  const src: string;
45
45
  export default src;
46
46
  }
47
+
48
+ /**
49
+ * A docs-owned theme stylesheet, imported for its side effect (e.g. `docs/themes/*.css`) —
50
+ * exactly like `preview/src/*.css`. There is nothing to type; the import exists only so the
51
+ * bundler includes the file.
52
+ */
53
+ declare module "*.css";
@@ -1,6 +1,8 @@
1
+ import type * as React from "react";
1
2
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
2
3
  import { Flex, PageContainer, ResponsiveGrid } from "@godxjp/ui/layout";
3
4
  import { AreaChart, BarChart, LineChart, PieChart } from "@godxjp/ui/charts";
5
+ import { useTranslation } from "@godxjp/ui/i18n";
4
6
 
5
7
  /**
6
8
  * Charts — tree-shaken `@godxjp/ui/charts` entry (needs the `recharts` optional
@@ -42,7 +44,18 @@ const sparseRevenue = [
42
44
 
43
45
  const jpy = { style: "currency", currency: "JPY" } as const;
44
46
 
47
+ /** 生鮮品の相場推移 — 値そのものではなく「形」が意味を持つ連続値 (gh#865)。 */
48
+ const priceHistory = [
49
+ { week: "第1週", price: 25400 },
50
+ { week: "第2週", price: 26800 },
51
+ { week: "第3週", price: 26100 },
52
+ { week: "第4週", price: 28300 },
53
+ { week: "第5週", price: 27600 },
54
+ { week: "第6週", price: 29900 },
55
+ ];
56
+
45
57
  export default function Demo() {
58
+ const { t } = useTranslation();
46
59
  return (
47
60
  <PageContainer
48
61
  title="Charts"
@@ -192,6 +205,73 @@ export default function Demo() {
192
205
  </CardContent>
193
206
  </Card>
194
207
  </ResponsiveGrid>
208
+
209
+ <ResponsiveGrid columns={2}>
210
+ <Card>
211
+ <CardHeader>
212
+ <CardTitle level={2}>{t("chartsDocs.areaSplitTitle")}</CardTitle>
213
+ <CardDescription>{t("chartsDocs.areaSplitBody")}</CardDescription>
214
+ </CardHeader>
215
+ <CardContent>
216
+ <AreaChart
217
+ label={t("chartsDocs.areaSplitLabel")}
218
+ description={t("chartsDocs.areaSplitDescription")}
219
+ data={priceHistory}
220
+ categoryKey="week"
221
+ series={[
222
+ {
223
+ dataKey: "price",
224
+ label: t("chartsDocs.priceSeries"),
225
+ color: "var(--chart-5)",
226
+ fillColor: "var(--chart-3)",
227
+ },
228
+ ]}
229
+ valueDomain={[24000, 31000]}
230
+ valueTicks={[24000, 26000, 28000, 30000]}
231
+ numberFormat={jpy}
232
+ showLegend={false}
233
+ showDots
234
+ curved
235
+ />
236
+ </CardContent>
237
+ </Card>
238
+
239
+ <Card>
240
+ <CardHeader>
241
+ <CardTitle level={2}>{t("chartsDocs.tokenScopeTitle")}</CardTitle>
242
+ <CardDescription>{t("chartsDocs.tokenScopeBody")}</CardDescription>
243
+ </CardHeader>
244
+ <CardContent>
245
+ <Flex
246
+ direction="col"
247
+ style={
248
+ {
249
+ "--chart-series-stroke-width": "3",
250
+ "--chart-area-fill-alpha": "0.07",
251
+ "--chart-grid-line-dash": "none",
252
+ } as React.CSSProperties
253
+ }
254
+ >
255
+ <AreaChart
256
+ label={t("chartsDocs.tokenScopeLabel")}
257
+ data={priceHistory}
258
+ categoryKey="week"
259
+ series={[
260
+ {
261
+ dataKey: "price",
262
+ label: t("chartsDocs.priceSeries"),
263
+ color: "var(--chart-5)",
264
+ },
265
+ ]}
266
+ valueDomain={[24000, 31000]}
267
+ numberFormat={jpy}
268
+ showLegend={false}
269
+ curved
270
+ />
271
+ </Flex>
272
+ </CardContent>
273
+ </Card>
274
+ </ResponsiveGrid>
195
275
  </Flex>
196
276
  </PageContainer>
197
277
  );
@@ -3,6 +3,7 @@ import { useMemo, useState } from "react";
3
3
  import { AppProvider } from "@godxjp/ui/app";
4
4
  import { Badge, Card, CardContent, DataTable, type ColumnDef } from "@godxjp/ui/data-display";
5
5
  import { Button, Text } from "@godxjp/ui/general";
6
+ import { useTranslation } from "@godxjp/ui/i18n";
6
7
  import { Flex, PageContainer } from "@godxjp/ui/layout";
7
8
  import {
8
9
  DropdownMenu,
@@ -110,6 +111,7 @@ export default function Demo() {
110
111
  direction: "desc",
111
112
  });
112
113
  const [density, setDensity] = useState<DensityProp>("comfortable");
114
+ const { t } = useTranslation();
113
115
 
114
116
  const rows = useMemo(() => {
115
117
  if (!sort) return invoices;
@@ -166,6 +168,34 @@ export default function Demo() {
166
168
  getRowId={(row) => row.id}
167
169
  />
168
170
  </Flex>
171
+ {/* The RECORD collection — a wide, heterogeneous record set. The fold is a CONTAINER
172
+ query, not a viewport one, so the same table folds by the width it is GIVEN: the two
173
+ blocks below are one component and one column list, rendered wide and narrow.
174
+
175
+ Below `collapseBelow` the header row hides and every `<tr>` becomes a bordered
176
+ key-value card. The per-cell labels are DERIVED from the same `ColumnDef.header` the
177
+ `<th>` uses (gh#864) — one declaration, so the card can never drift from the table it
178
+ collapsed out of, and the value that loses its header keeps its name. */}
179
+ <Flex direction="col" gap="sm" id="stacked-record-collection">
180
+ <Text weight="medium">{t("dataTableDocs.stackedRecordWide")}</Text>
181
+ <DataTable
182
+ preset="stacked-record-collection"
183
+ collapseBelow="sm"
184
+ data={invoices}
185
+ columns={columns}
186
+ getRowId={(row) => row.id}
187
+ />
188
+ </Flex>
189
+ <Flex direction="col" gap="sm" id="stacked-record-collection-folded" className="max-w-xs">
190
+ <Text weight="medium">{t("dataTableDocs.stackedRecordFolded")}</Text>
191
+ <DataTable
192
+ preset="stacked-record-collection"
193
+ collapseBelow="sm"
194
+ data={invoices}
195
+ columns={columns}
196
+ getRowId={(row) => row.id}
197
+ />
198
+ </Flex>
169
199
  {/* Row TONE — the leading-edge rail + wash for a row in a named state. The status Badge
170
200
  stays in its own cell: the rail makes the row findable, it does not carry the meaning
171
201
  (WCAG 1.4.1). */}
@@ -78,7 +78,7 @@ export default function Demo() {
78
78
  </PopoverTrigger>
79
79
  <PopoverContent aria-label="ワークスペース">
80
80
  <Select aria-label="ワークスペース" defaultOpen defaultValue="tokyo">
81
- <SelectTrigger aria-label="ワークスペース" data-picker-trigger="">
81
+ <SelectTrigger data-picker-trigger="">
82
82
  <SelectValue />
83
83
  </SelectTrigger>
84
84
  <SelectContent>
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Badge,
2
3
  Card,
3
4
  CardContent,
4
5
  CardDescription,
@@ -13,6 +14,7 @@ import {
13
14
  TableRow,
14
15
  } from "@godxjp/ui/data-display";
15
16
  import { Text } from "@godxjp/ui/general";
17
+ import { useTranslation } from "@godxjp/ui/i18n";
16
18
  import { Flex, PageContainer } from "@godxjp/ui/layout";
17
19
 
18
20
  /**
@@ -21,6 +23,7 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
21
23
  * for a custom one-off table. Composed only from real @godxjp/ui components.
22
24
  */
23
25
  export default function Demo() {
26
+ const { t } = useTranslation();
24
27
  return (
25
28
  <PageContainer
26
29
  title="Table"
@@ -290,6 +293,55 @@ export default function Demo() {
290
293
  </CardContent>
291
294
  </Card>
292
295
 
296
+ {/* ── gh#876 — TableRow tone, a typed route to the same paint DataTable's own
297
+ `rowTone` writes onto `data-tone` (wash + leading rail + `--surface-*` grounding,
298
+ gh#866). A hand-composed row discovers it from types now; `data-tone` written by
299
+ hand keeps working unchanged — this is an addition, not a migration. */}
300
+ <Card>
301
+ <CardHeader>
302
+ <CardTitle level={2}>{t("tableDocs.title")}</CardTitle>
303
+ <CardDescription>
304
+ `TableRow tone` writes the same `data-tone` attribute `DataTable`&apos;s `rowTone`
305
+ already writes for you — no more reaching for the raw attribute to discover the row
306
+ can be toned at all.
307
+ </CardDescription>
308
+ </CardHeader>
309
+ <CardContent flush>
310
+ <Table>
311
+ <TableHeader>
312
+ <TableRow>
313
+ <TableHead>{t("tableDocs.item")}</TableHead>
314
+ <TableHead align="end">{t("tableDocs.amount")}</TableHead>
315
+ <TableHead>{t("tableDocs.status")}</TableHead>
316
+ </TableRow>
317
+ </TableHeader>
318
+ <TableBody>
319
+ <TableRow>
320
+ <TableCell>{t("tableDocs.fee")}</TableCell>
321
+ <TableCell numeric>¥440</TableCell>
322
+ <TableCell>
323
+ <Badge tone="neutral">{t("tableDocs.done")}</Badge>
324
+ </TableCell>
325
+ </TableRow>
326
+ <TableRow tone="warning">
327
+ <TableCell>{t("tableDocs.overdue")}</TableCell>
328
+ <TableCell numeric>¥128,000</TableCell>
329
+ <TableCell>
330
+ <Badge tone="warning">{t("tableDocs.check")}</Badge>
331
+ </TableCell>
332
+ </TableRow>
333
+ <TableRow tone="destructive">
334
+ <TableCell>{t("tableDocs.failed")}</TableCell>
335
+ <TableCell numeric>¥52,300</TableCell>
336
+ <TableCell>
337
+ <Badge tone="destructive">{t("tableDocs.failedBadge")}</Badge>
338
+ </TableCell>
339
+ </TableRow>
340
+ </TableBody>
341
+ </Table>
342
+ </CardContent>
343
+ </Card>
344
+
293
345
  <Card>
294
346
  <CardHeader>
295
347
  <CardTitle level={2}>関連ファイル · flush, no table</CardTitle>
@@ -141,8 +141,8 @@ export default function Demo() {
141
141
  <SheetBody>
142
142
  <Flex direction="col" gap="md">
143
143
  <FormField id="filter-account" label="勘定科目">
144
- <Select value={account} onValueChange={setAccount}>
145
- <SelectTrigger id="filter-account" aria-label="勘定科目">
144
+ <Select aria-label="勘定科目" value={account} onValueChange={setAccount}>
145
+ <SelectTrigger id="filter-account">
146
146
  <SelectValue placeholder="すべての勘定科目" />
147
147
  </SelectTrigger>
148
148
  <SelectContent>
@@ -154,8 +154,8 @@ export default function Demo() {
154
154
  </Select>
155
155
  </FormField>
156
156
  <FormField id="filter-status" label="ステータス">
157
- <Select value={status} onValueChange={setStatus}>
158
- <SelectTrigger id="filter-status" aria-label="ステータス">
157
+ <Select aria-label="ステータス" value={status} onValueChange={setStatus}>
158
+ <SelectTrigger id="filter-status">
159
159
  <SelectValue placeholder="すべてのステータス" />
160
160
  </SelectTrigger>
161
161
  <SelectContent>
@@ -166,8 +166,8 @@ export default function Demo() {
166
166
  </Select>
167
167
  </FormField>
168
168
  <FormField id="filter-source" label="ソース">
169
- <Select value={source} onValueChange={setSource}>
170
- <SelectTrigger id="filter-source" aria-label="ソース">
169
+ <Select aria-label="ソース" value={source} onValueChange={setSource}>
170
+ <SelectTrigger id="filter-source">
171
171
  <SelectValue placeholder="すべてのソース" />
172
172
  </SelectTrigger>
173
173
  <SelectContent>
@@ -444,10 +444,10 @@ export default function Demo() {
444
444
  modal=&#123;false&#125; on Sheet renders a non-modal panel, a pattern WAI-ARIA APG
445
445
  allows. The page behind keeps working: no scrim, no scroll lock, nothing hidden from
446
446
  assistive tech, and a press outside does not close the panel. Open the order summary
447
- (side=&quot;left&quot;, so the quantity controls stay uncovered), then change a quantity
448
- below. The total inside the panel follows. Focus moves into the
449
- panel on open, Tab can leave it, Escape closes it while focus is inside, and focus
450
- returns to the trigger.
447
+ (side=&quot;left&quot;, so the quantity controls stay uncovered), then change a
448
+ quantity below. The total inside the panel follows. Focus moves into the panel on
449
+ open, Tab can leave it, Escape closes it while focus is inside, and focus returns to
450
+ the trigger.
451
451
  </CardDescription>
452
452
  </CardHeader>
453
453
  <CardContent>