@godxjp/ui 19.6.0 → 20.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 (137) hide show
  1. package/dist/components/data-display/card.d.ts +2 -2
  2. package/dist/components/data-display/collapsible.d.ts +23 -4
  3. package/dist/components/data-display/collapsible.js +119 -4
  4. package/dist/components/data-display/index.d.ts +3 -1
  5. package/dist/components/data-display/index.js +2 -0
  6. package/dist/components/data-display/legend.d.ts +15 -0
  7. package/dist/components/data-display/legend.js +11 -0
  8. package/dist/components/data-display/list-row.js +1 -1
  9. package/dist/components/data-display/popover.d.ts +58 -5
  10. package/dist/components/data-display/popover.js +206 -27
  11. package/dist/components/data-display/progress.d.ts +50 -2
  12. package/dist/components/data-display/progress.js +57 -9
  13. package/dist/components/data-entry/calendar.d.ts +1 -1
  14. package/dist/components/data-entry/calendar.js +67 -32
  15. package/dist/components/data-entry/checkbox.d.ts +19 -6
  16. package/dist/components/data-entry/checkbox.js +55 -16
  17. package/dist/components/data-entry/choice-option.d.ts +1 -1
  18. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  19. package/dist/components/data-entry/field.js +0 -1
  20. package/dist/components/data-entry/form.js +1 -1
  21. package/dist/components/data-entry/label.d.ts +3 -2
  22. package/dist/components/data-entry/label.js +23 -10
  23. package/dist/components/data-entry/month-picker.d.ts +1 -1
  24. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  25. package/dist/components/data-entry/transfer.js +16 -6
  26. package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
  27. package/dist/components/data-entry/tree-select.js +29 -12
  28. package/dist/components/data-entry/tree-utils.d.ts +1 -1
  29. package/dist/components/data-entry/upload.js +7 -2
  30. package/dist/components/feedback/dialog.d.ts +105 -38
  31. package/dist/components/feedback/dialog.js +272 -194
  32. package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
  33. package/dist/components/feedback/overlay-close-focus.js +31 -0
  34. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  35. package/dist/components/feedback/sheet.d.ts +53 -11
  36. package/dist/components/feedback/sheet.js +150 -81
  37. package/dist/components/feedback/tooltip.d.ts +51 -7
  38. package/dist/components/feedback/tooltip.js +107 -25
  39. package/dist/components/general/button.js +2 -2
  40. package/dist/components/general/logo.d.ts +17 -0
  41. package/dist/components/general/logo.js +22 -16
  42. package/dist/components/layout/app-shell.d.ts +1 -1
  43. package/dist/components/layout/app-shell.js +52 -8
  44. package/dist/components/layout/aspect-ratio.js +0 -1
  45. package/dist/components/layout/auth-divider.js +0 -1
  46. package/dist/components/layout/flex.d.ts +1 -1
  47. package/dist/components/layout/flex.js +19 -2
  48. package/dist/components/layout/nav-surface.d.ts +26 -0
  49. package/dist/components/layout/nav-surface.js +17 -0
  50. package/dist/components/layout/org-switcher.d.ts +5 -1
  51. package/dist/components/layout/org-switcher.js +5 -3
  52. package/dist/components/layout/separator.js +0 -1
  53. package/dist/components/layout/sidebar.js +4 -1
  54. package/dist/components/layout/split-pane.d.ts +14 -1
  55. package/dist/components/layout/split-pane.js +26 -13
  56. package/dist/components/layout/topbar-item.js +1 -2
  57. package/dist/components/navigation/app-setting-picker.js +21 -2
  58. package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
  59. package/dist/components/navigation/app-setting-toggle.js +96 -0
  60. package/dist/components/navigation/dropdown-menu.d.ts +199 -18
  61. package/dist/components/navigation/dropdown-menu.js +344 -117
  62. package/dist/components/navigation/index.d.ts +2 -0
  63. package/dist/components/navigation/index.js +2 -0
  64. package/dist/components/navigation/pagination-utils.d.ts +2 -1
  65. package/dist/components/navigation/tabs.d.ts +47 -6
  66. package/dist/components/navigation/tabs.js +152 -90
  67. package/dist/components/ui/accordion.d.ts +50 -5
  68. package/dist/components/ui/accordion.js +239 -33
  69. package/dist/components/ui/aspect-ratio.d.ts +23 -2
  70. package/dist/components/ui/aspect-ratio.js +15 -13
  71. package/dist/components/ui/avatar.d.ts +29 -4
  72. package/dist/components/ui/avatar.js +111 -25
  73. package/dist/components/ui/hover-card.d.ts +42 -4
  74. package/dist/components/ui/hover-card.js +183 -27
  75. package/dist/components/ui/label.js +0 -1
  76. package/dist/components/ui/segmented.d.ts +3 -3
  77. package/dist/components/ui/separator.d.ts +8 -2
  78. package/dist/components/ui/separator.js +17 -9
  79. package/dist/components/ui/toggle-group.d.ts +50 -5
  80. package/dist/components/ui/toggle-group.js +79 -20
  81. package/dist/components/ui/toggle.d.ts +31 -5
  82. package/dist/components/ui/toggle.js +42 -3
  83. package/dist/i18n/messages/en.json +8 -0
  84. package/dist/i18n/messages/ja.json +8 -0
  85. package/dist/i18n/messages/vi.json +8 -0
  86. package/dist/lib/control-styles.d.ts +1 -1
  87. package/dist/lib/control-styles.js +1 -1
  88. package/dist/lib/slot.d.ts +32 -0
  89. package/dist/lib/slot.js +22 -0
  90. package/dist/lib/variants.d.ts +22 -3
  91. package/dist/lib/variants.js +56 -1
  92. package/dist/props/components/app.prop.d.ts +25 -1
  93. package/dist/props/components/data-display.prop.d.ts +17 -0
  94. package/dist/props/components/data-entry.prop.d.ts +32 -4
  95. package/dist/props/components/index.d.ts +1 -1
  96. package/dist/props/components/layout.prop.d.ts +118 -5
  97. package/dist/props/components/navigation.prop.d.ts +1 -1
  98. package/dist/props/registry.d.ts +22 -2
  99. package/dist/props/registry.js +33 -2
  100. package/dist/props/vocabulary/content.prop.d.ts +1 -1
  101. package/dist/props/vocabulary/index.d.ts +2 -2
  102. package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
  103. package/dist/props/vocabulary/layout.prop.d.ts +57 -1
  104. package/dist/styles/control.css +142 -1
  105. package/dist/styles/core.css +5 -2
  106. package/dist/styles/data-display-layout.css +72 -0
  107. package/dist/styles/focus-ring.css +4 -0
  108. package/dist/styles/index.css +5 -2
  109. package/dist/styles/layout.css +106 -0
  110. package/dist/styles/shell-layout.css +140 -8
  111. package/dist/styles/text-layout.css +5 -4
  112. package/dist/tokens/base.css +1 -1
  113. package/dist/tokens/components/data-display.css +11 -0
  114. package/dist/tokens/components/shell.css +3 -1
  115. package/dist/tokens/foundation.css +2 -0
  116. package/docs/COMPONENTS.md +9 -3
  117. package/docs/DESIGN-AUTHORITY.md +96 -71
  118. package/docs/FRAME-COVERAGE-REPORT.md +5 -3
  119. package/docs/README.md +14 -14
  120. package/docs/WHAT-BELONGS-HERE.md +179 -0
  121. package/docs/data-display/legend.tsx +145 -0
  122. package/docs/data-display/progress.tsx +32 -0
  123. package/docs/data-entry/segmented.tsx +1 -1
  124. package/docs/data-entry/select.tsx +2 -2
  125. package/docs/feedback/sheet.tsx +1 -1
  126. package/docs/layout/app-shell-arrangements.tsx +225 -0
  127. package/docs/layout/aspect-ratio.tsx +1 -1
  128. package/docs/navigation/app-setting-picker.tsx +26 -1
  129. package/docs/navigation/app-setting-toggle.tsx +111 -0
  130. package/package.json +11 -8
  131. package/scripts/_agent-setup.mjs +165 -3
  132. package/scripts/consumer-rule.md +98 -0
  133. package/scripts/guinea-pig-skill.md +322 -0
  134. package/scripts/init-guinea-pig.mjs +62 -0
  135. package/scripts/postinstall.mjs +13 -2
  136. package/scripts/ui-audit.mjs +115 -14
  137. /package/dist/tokens/{antd.generated.css → derived.css} +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  @import "./foundation.css";
4
4
 
5
- @import "./antd.generated.css";
5
+ @import "./derived.css";
6
6
  @import "./semantic/layout.css";
7
7
  @import "./axes.css";
8
8
  @import "./components/control.css";
@@ -64,6 +64,17 @@
64
64
  --progress-track-background: initial;
65
65
  --progress-fill-background: initial;
66
66
 
67
+ --progress-breakdown-block-size: 1.375rem;
68
+ --progress-breakdown-radius: var(--radius);
69
+
70
+ --legend-gap: 0.875rem;
71
+ --legend-item-gap: 0.3125rem;
72
+ --legend-swatch-size: 0.625rem;
73
+ --legend-swatch-radius: 0.125rem;
74
+ --legend-font-size: var(--font-size-xs);
75
+
76
+ --legend-color: initial;
77
+
67
78
  --timeline-dot-done-background: initial;
68
79
  --timeline-dot-current-background: initial;
69
80
  --timeline-line-completed-background: initial;
@@ -42,7 +42,9 @@
42
42
  --app-shell-bar-gap: var(--space-3);
43
43
 
44
44
  --app-shell-sidebar-width: 16rem;
45
- --app-shell-rail-width: 4rem;
45
+ --app-shell-sidebar-collapsed-width: 4rem;
46
+
47
+ --app-shell-nav-rail-width: 3.5rem;
46
48
  --app-shell-page-max-width: 80rem;
47
49
  --app-shell-main-background: hsl(var(--muted) / 0.4);
48
50
  --app-shell-mobile-nav-width: 22.5rem;
@@ -273,6 +273,8 @@
273
273
  --space-inline-sm: var(--space-2);
274
274
  --space-inline-md: var(--space-3);
275
275
  --space-inline-lg: var(--space-4);
276
+
277
+ --space-inline-xl: var(--space-6);
276
278
  --space-chrome-x: var(--space-6);
277
279
  --space-chrome-y: var(--space-4);
278
280
  --space-chrome-gap: var(--space-2);
@@ -48,9 +48,13 @@ All UI is **mobile-first**: base layout targets ~320–428px viewport; `sm` (640
48
48
 
49
49
  Default `direction="row"`; use `direction="col"` for vertical rhythm.
50
50
 
51
- | Component | Prop | Key props |
52
- | --------- | ---------- | ----------------------------------------------------------------------- |
53
- | `Flex` | `FlexProp` | `direction: "row" \| "col"`, `gap: GapProp`, `align`, `justify`, `wrap` |
51
+ | Component | Prop | Key props |
52
+ | --------- | ---------- | ---------------------------------------------------------------------------------------------------- |
53
+ | `Flex` | `FlexProp` | `direction: "row" \| "col"`, `gap: GapProp`, `gapRaw`, `pad`, `padRaw`, `fill`, `width`, `align`, `justify`, `wrap` |
54
+
55
+ `fill` / `width` are the `fixed | elastic | fixed` row: `width` pins a column (number = px, and
56
+ leaves `data-width-raw` on the DOM so the measurement stays countable), `fill` takes what is left
57
+ and sets `min-inline-size: 0` so a truncating child ellipses instead of widening the row.
54
58
 
55
59
  ---
56
60
 
@@ -92,6 +96,8 @@ Default `direction="row"`; use `direction="col"` for vertical rhythm.
92
96
  | `Descriptions` | `DescriptionsProp` | `items[]` |
93
97
  | `Badge` | `BadgeProp` | `status`, `tone: ToneProp` |
94
98
  | `Badge` | `BadgeProp` | shadcn badge variants |
99
+ | `Progress` | `ProgressProp` | `value` (meter) \| `segments` (breakdown) |
100
+ | `Legend` | `LegendProp` | `items[]` — the key for a tone |
95
101
  | `Popover` | Radix primitives | floating content panel |
96
102
  | `ScrollArea` | Radix primitives | scrollable regions |
97
103
  | `Collapsible` | Radix primitives | expand/collapse sections |
@@ -17,9 +17,9 @@ It changes no code by itself. It is the tie-breaker a reviewer points at.
17
17
  | Interaction semantics, keyboard, ARIA | **WAI-ARIA APG** | already followed — 33 references in `src/` |
18
18
  | Behaviour primitives | **Radix** | already the implementation — 193 references |
19
19
  | Component composition shape | **shadcn** | already the structural convention — 23 references |
20
- | Component taxonomy / grouping | **Ant Design** groups | already the catalog shape: `data-entry`, `data-display`, `layout`, `feedback`, `navigation`, `general` |
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
21
  | Colour foundation | **SmartHR** | already the palette source — `--primary` = SmartHR MAIN `#0071bd`, `--foreground` = TEXT_BLACK, `--border` = BORDER |
22
- | **Derived colour — every step computed from a seed** | **Ant Design (the algorithm itself)** | **NEW this decision.** antd is no longer a shape reference here: `scripts/gen-antd-tokens.mjs` runs `theme.getDesignToken()` at build time and emits the derived map |
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 |
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** |
@@ -187,70 +187,94 @@ Recorded rather than silently fixed, because each is a real decision:
187
187
  strokes blur far more visibly than Latin letterforms. Another reason to prefer Carbon's integer
188
188
  steps if the scale is ever revisited.
189
189
 
190
- ## Derived colour is GENERATED, not authored
190
+ ## Derived colour is AUTHORED, and MEASUREMENT is what makes it authoritative
191
191
 
192
- **The problem this fixes.** The library hand-authored roughly a thousand tokens and reasoned about
193
- each one. A hover step was chosen by eye, an active step was chosen by eye, a focus-halo alpha was
194
- chosen by eye and then justified in a paragraph. Every one of those is a decision with no outside
195
- authority behind it, and every one has to be re-argued the next time somebody looks at it.
192
+ **Twenty values, in `src/tokens/derived.css`:** `--primary-hover`, `--primary-active`,
193
+ `--primary-border`, `--destructive-hover`, `--destructive-active`, `--control-outline` and its
194
+ alpha, `--control-outline-error` and its alpha, and `--ring` each in both themes. They are the
195
+ interaction states that hang off the five authored seeds per theme (SmartHR MAIN plus four 和色,
196
+ and the lifted dark ramp), which stay in `src/tokens/foundation.css`.
196
197
 
197
- **Ant Design already solves this exactly once**: a SEED colour goes in, an ALGORITHM runs, and the
198
- whole derived map falls out. This document already named antd as the authority for component
199
- taxonomy and focus shape; that is now extended to make antd the **generator** of derived colour.
198
+ **These twenty were once generated.** A colour algorithm ran at build time, took the seeds and
199
+ emitted the derived map; the authority behind each value was "the algorithm said so". That
200
+ generator has been removed and its dependency with it. The values did not change but the reason
201
+ to trust them had to.
200
202
 
201
- `scripts/gen-antd-tokens.mjs` reads the seeds out of `src/tokens/foundation.css`, runs antd 6.6.2's
202
- own `theme.getDesignToken()` (`defaultAlgorithm` for light, `darkAlgorithm` for dark) and writes
203
- `src/tokens/antd.generated.css`. `pnpm check:antd-tokens` fails CI if that file drifts from the
204
- algorithm. **antd is a devDependency and a build-time tool only** — `pnpm check:no-antd-runtime`
205
- asserts it never reaches `dependencies`, `src/`, or `dist/`.
203
+ **The authority is now the measurement, not the derivation.** Four suites read `derived.css`
204
+ directly and hold every value in it to a threshold this repo has already committed to:
206
205
 
207
- **What is authored:** five brand colours per theme (SmartHR MAIN plus four 和色, and the lifted dark
208
- ramp). **What is computed:** `--primary-hover`, `--primary-active`, `--primary-border`,
209
- `--destructive-hover`, `--destructive-active`, `--control-outline*` and `--ring`.
206
+ | suite | what it holds |
207
+ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
208
+ | `src/tokens/__tests__/focus-ring-contrast.test.ts` | the focus mark in both switch positions — ≥3:1 (WCAG 2.2 SC 1.4.11) on every surface a control sits on, and the halo proven to be decoration rather than the indicator |
209
+ | `src/tokens/__tests__/interactive-fill-contrast.test.ts` | an interactive fill must clear **4.5:1** against the label sitting on it |
210
+ | `src/tokens/__tests__/destructive-contrast.test.ts` | `--destructive-hover` / `--destructive-active` against the same bar |
211
+ | `src/lib/__tests__/theme-tokens-css.test.ts` | the tier is actually loaded, and complete in both themes |
210
212
 
211
- **What is deliberately NOT taken from antd, and why each a measurement, not a preference:**
213
+ The first three also pin each value as a literal, so an edit to `derived.css` alone turns CI red
214
+ rather than quietly retinting the library. **That is a stronger claim than the generator made, not
215
+ a weaker one.** An algorithm guarantees a value is _consistent_; this repo had to override it four
216
+ times (below) precisely because consistent is not the same as _accessible_. A threshold guarantees
217
+ the property actually being sold.
212
218
 
213
- - **The neutral spine.** Colour foundation stays SmartHR's (see the table above); the neutrals are
214
- not derived from the brand seed. `colorBorder` is also the only role antd offers for a control
215
- boundary and it measures **1.43:1** on the page, where `--input` is held to 3:1 by SC 1.4.11 and
216
- by `input-boundary-contrast.test.ts`.
217
- - **The text ramp.** antd's is alpha-based (`rgba(0,0,0,0.88)`), which cannot enter this library's
218
- opaque `H S% L%` triple without choosing a surface to composite against — lossy by construction.
219
- - **The dark `--primary` itself.** `darkAlgorithm` MOVES the seed (antd's own `#1677ff` becomes
220
- `#1668dc`). Feeding this library's light seed in gives `#0363a4` at **2.81:1** on the dark spine,
221
- which `primary-text-contrast.test.ts` rejects outright; antd's transform of the committed dark
222
- seed gives `#3794d3` at 5.36:1 where the seed itself measures 7.07:1. The dark theme therefore
223
- keeps its seed and everything derived from it is antd's. This is the one structural divergence,
224
- recorded in `DIVERGENCES` in the generator.
225
-
226
- **Geometry antd owns was already correct.** `lineWidth` 1, `controlOutlineWidth` 2,
227
- `lineWidthFocus` 3, `borderRadius` 6, `controlHeight` 32 and `fontSize` 14 all match the named
228
- scales this library already ships (`--stroke-*`, `--radius`, `--band-height-md`,
229
- `--font-size-base`). The generator asserts that agreement rather than emitting a second copy, so
230
- the two cannot drift apart silently.
231
-
232
- ## Focus appearance — Ant Design owns the shape, and the indicator SHIPS OFF
233
-
234
- ### The two forms, read from antd's source
219
+ **To change a derived value:** change it, run `pnpm test`, and if a threshold breaks the value is
220
+ wrong. Do not relax the threshold.
235
221
 
236
- **SmartHR** (`smarthr-ui@99.6.0`) draws focus as an opaque ring held off the control by a white
237
- spacer. **Ant Design** (`antd@6.6.2`) has two forms instead:
222
+ **The four overrides that existed even while a generator did** the clearest evidence the
223
+ algorithm was never the real authority. Stepping an interactive fill towards the label sitting on
224
+ it lands under 4.5:1, so those four states take the same ramp at the same step size in the
225
+ opposite direction:
238
226
 
239
- - **Field** `borderColor: colorPrimary` at the unchanged `lineWidth`, plus
240
- `boxShadow: 0 0 0 ${controlOutlineWidth}px ${controlOutline}`
241
- (`es/input/style/token.js:48-50`). `es/select/style/select-input.js:32` emits the same
242
- declarations, which is why an antd Select focuses exactly like an antd Input.
243
- - **Non-field** `genFocusOutline` (`es/style/index.js:60-64`):
244
- `outline: ${lineWidthFocus}px solid ${colorPrimaryBorder}; outline-offset: 1`.
227
+ | token | conventional step | shipped |
228
+ | ------------------------ | ----------------- | ----------------- |
229
+ | light `--primary-hover` | #208bc9 · 3.69:1 | #005596 · 7.53:1 |
230
+ | light `--primary-active` | #005596 · 7.53:1 | #003c70 · 10.97:1 |
231
+ | dark `--primary-hover` | #61b6e8 · 7.92:1 | #61b6e8 · 7.92:1 |
232
+ | dark `--primary-active` | #2f76a6 · 3.60:1 | #8bd0f3 · 10.50:1 |
245
233
 
246
- **v5 v6 changed none of this.** Both majors were unpacked and their own `formatToken` run side by
247
- side; every value is identical to the digit in `antd@5.29.3`.
234
+ **What was deliberately never derived, and why each a measurement, not a preference:**
248
235
 
249
- **Ant Design wins the shape.** The library shipped an opaque brand ring drawn immediately outside an
250
- untouched grey border two outlines of different colours claiming the same edge and a Select that
251
- could not be told to agree with an Input. antd's rule resolves both. SmartHR still owns the hue: the
252
- focus colour is `--ring`, which the generated tier declares as `var(--primary)` because antd has no
253
- separate focus-colour token at all.
236
+ - **The neutral spine.** Colour foundation stays SmartHR's (see the table above); the neutrals are
237
+ not derived from the brand seed. The one derived role on offer for a control boundary measures
238
+ **1.43:1** on the page, where `--input` is held to 3:1 by SC 1.4.11 and by
239
+ `input-boundary-contrast.test.ts`.
240
+ - **The text ramp.** The derived one is alpha-based (`rgba(0,0,0,0.88)`), which cannot enter this
241
+ library's opaque `H S% L%` triple without choosing a surface to composite against — lossy by
242
+ construction.
243
+ - **The dark `--primary` itself.** A mechanical dark derivation MOVES the seed. Deriving from this
244
+ library's light seed gives `#0363a4` at **2.81:1** on the dark spine, which
245
+ `primary-text-contrast.test.ts` rejects outright; deriving from the committed dark seed gives
246
+ `#3794d3` at 5.36:1, where the seed itself measures 7.07:1. The dark theme therefore keeps its
247
+ own seed — recorded, with the measurement, in `focus-ring-contrast.test.ts`.
248
+
249
+ **The focus geometry was already on the named scale.** Border 1, halo width 2, heavy outline 3,
250
+ radius 6, control height 32 and font size 14 all match scales this library already ships
251
+ (`--stroke-*`, `--radius`, `--band-height-md`, `--font-size-base`), so the focus tokens bind to
252
+ those steps instead of restating a second copy of the number.
253
+
254
+ **antd itself is gone from this repository**, and `pnpm check:no-antd-runtime` is what keeps it
255
+ gone: it fails if antd or any `@ant-design/*` package appears in any manifest field, is imported
256
+ anywhere in `src/`, or leaves a trace in `dist/`. Deleting that gate re-opens the door it closes.
257
+
258
+ ## Focus appearance — a two-form convention, and the indicator SHIPS OFF
259
+
260
+ ### The two forms
261
+
262
+ **SmartHR** (`smarthr-ui@99.6.0`) draws focus as an opaque ring held off the control by a white
263
+ spacer. This library takes a different, two-form convention instead — widely used in enterprise
264
+ component libraries, and surveyed across several before it was adopted here:
265
+
266
+ - **Field** — the boundary RECOLOURS to the primary at its unchanged hairline width, plus
267
+ `box-shadow: 0 0 0 var(--control-outline-width) var(--control-outline)`. A Select emits the same
268
+ declarations as an Input, which is why the two focus identically.
269
+ - **Non-field** — an outline outside the box model:
270
+ `outline: var(--focus-outline-weight) solid var(--primary-border); outline-offset: 1px`.
271
+
272
+ **Why this shape wins.** The library previously shipped an opaque brand ring drawn immediately
273
+ outside an untouched grey border — two outlines of different colours claiming the same edge — and a
274
+ Select that could not be told to agree with an Input. Recolouring the existing boundary rather than
275
+ adding a second one resolves both. SmartHR still owns the hue: the focus colour is `--ring`, which
276
+ the derived tier declares as `var(--primary)`, because a focused field takes the primary rather than
277
+ a focus colour of its own.
254
278
 
255
279
  ### The indicator ships OFF. That forfeits WCAG 2.4.7 and a JIS X 8341-3 AA claim.
256
280
 
@@ -272,15 +296,15 @@ owner chose to ship it off and let whoever needs it turn it on. What that costs,
272
296
  <html data-focus-outline="on">
273
297
  ```
274
298
 
275
- **The switch is antd's own mechanism, not one invented here.** antd 6.6.2 carries a `focusOutline`
276
- seed flag (`es/theme/themes/seed.js:67`) and consumes it in `es/theme/util/alias.js:71`:
299
+ **The switch is a single multiplier, not a scatter of overrides.** `--focus-outline` is one flag,
300
+ and every painted focus length multiplies by it:
277
301
 
278
- ```js
279
- lineWidthFocus: mergedToken.focusOutline === false ? 0 : mergedToken.lineWidth * 3
302
+ ```css
303
+ --focus-ring-width: calc(var(--focus-ring-weight) * var(--focus-outline));
280
304
  ```
281
305
 
282
- The flag zeroes the focus line width. `--focus-outline` is that flag in CSS: every painted length
283
- multiplies by it, so no component rebind can bring the mark back while it is `0`. The two paints
306
+ Setting it to `0` zeroes every focus length at once, so no component rebind can bring the mark back
307
+ while it is off which is the property a scatter of per-component overrides could never give. The two paints
284
308
  that are not lengths — the halo and a field's recoloured boundary — are scoped to the same attribute
285
309
  in `styles/focus-ring.css`. **Every `:focus-visible` selector stays exactly where it is**; only the
286
310
  painted result disappears.
@@ -288,8 +312,8 @@ painted result disappears.
288
312
  ### The ON state is the LIGHT one
289
313
 
290
314
  The complaint was weight, not existence, so the on-position is not the old mark restored. It paints
291
- antd's **field** indicator on every control — one `lineWidth` (1px) in the focus hue, plus antd's
292
- `controlOutline` halo — rather than antd's 3px `genFocusOutline`. Measured, in Chromium, on
315
+ the **field** indicator on every control — one hairline (1px) in the focus hue, plus the
316
+ `--control-outline` halo — rather than the heavy 3px outline form. Measured, in Chromium, on
293
317
  `ql.test` after the transition settles:
294
318
 
295
319
  | control | switch off | switch on |
@@ -298,8 +322,8 @@ antd's **field** indicator on every control — one `lineWidth` (1px) in the foc
298
322
  | Button (primary) | outline `0px`, resting shadow intact | `outline: 1px solid rgb(0,113,189)` @ `0px` + same halo |
299
323
  | Sidebar nav row / list row | outline `0px` | `outline: 1px solid rgb(0,113,189)` @ **`-1px`** (inset into the row) |
300
324
 
301
- The field pair is **byte-identical to antd's own `activeBorderColor` + `activeShadow`** for this
302
- seed. The nav row insets its mark into its own shape rather than wrapping an already-shaded surface,
325
+ The field pair is the recoloured boundary plus the halo, exactly as the convention specifies for
326
+ this seed. The nav row insets its mark into its own shape rather than wrapping an already-shaded surface,
303
327
  which is the specific stacking that read as heavy.
304
328
 
305
329
  **No control's box moves when it is focused**, measured with `getBoundingClientRect()` before and
@@ -308,11 +332,12 @@ Input, Save and Delete all stay at 32.00px.
308
332
 
309
333
  **Two criteria, and only one is met by thickness alone.** SC 1.4.11 (AA, non-text contrast) is about
310
334
  COLOUR — the 1px mark measures 5.05:1 light and 7.07:1 dark on every surface a control sits on, so
311
- the light weight costs nothing there. antd's own `colorPrimaryBorder` (#6dc0e3) measures **2.00:1
312
- light / 1.66:1 dark** and could not have satisfied it at any thickness, which is why the on-state
313
- takes the focus hue instead — the single place it departs from `genFocusOutline`. SC 2.4.13 Focus
314
- Appearance (AAA) additionally wants a 2px perimeter; the on-state does not target it, and
315
- `--focus-outline-weight: var(--stroke-lg)` restores antd's 3px if a customer needs the area clause.
335
+ the light weight costs nothing there. `--primary-border` (#6dc0e3) measures **2.00:1 light /
336
+ 1.66:1 dark** and could not have satisfied it at any thickness, which is why the on-state takes the
337
+ focus hue instead — the single place it departs from the outline form. SC 2.4.13 Focus Appearance
338
+ (AAA) additionally wants a 2px perimeter; the on-state does not target it, and
339
+ `--focus-outline-weight: var(--stroke-lg)` restores the 3px weight if a customer needs the area
340
+ clause.
316
341
 
317
342
  Both positions of the switch are gated in `src/tokens/__tests__/focus-ring-contrast.test.ts`: with
318
343
  it off nothing paints and no rebind can route around it, with it on the geometry and the ≥3:1
@@ -3,8 +3,8 @@
3
3
  > Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
4
4
  > Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
5
5
 
6
- - Public components: **129**
7
- - With ≥1 frame: **129** · zero-frame: **0**
6
+ - Public components: **131**
7
+ - With ≥1 frame: **131** · zero-frame: **0**
8
8
  - Fully covered (every contract axis declared): **0**
9
9
 
10
10
  ## Per-component contract axes
@@ -41,6 +41,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
41
41
  | StatCard | ✓ | N/A | N/A | · | · | · | · | · | · | · |
42
42
  | ServiceLauncherCard | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
43
43
  | Badge | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
44
+ | Legend | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
44
45
  | ListRow | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
45
46
  | CredentialReveal | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
46
47
  | QrCode | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
@@ -106,7 +107,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
106
107
  | Avatar | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
107
108
  | Separator | ✓ | · | N/A | N/A | · | · | · | · | · | · |
108
109
  | Skeleton | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
109
- | Toggle | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
110
+ | Toggle | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
110
111
  | ToggleGroup | ✓ | ✓ | · | · | · | · | · | · | · | · |
111
112
  | AspectRatio | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
112
113
  | Accordion | ✓ | N/A | · | · | · | · | · | · | · | · |
@@ -123,6 +124,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
123
124
  | ResizablePanel | ✓ | N/A | · | N/A | · | · | · | · | · | · |
124
125
  | Carousel | ✓ | N/A | N/A | · | · | · | · | · | · | · |
125
126
  | AppSettingPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
127
+ | AppSettingToggle | ✓ | N/A | · | · | · | · | · | · | · | · |
126
128
  | Field | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
127
129
  | LineChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
128
130
  | BarChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
package/docs/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # @godxjp/ui Documentation
2
2
 
3
- | Doc | Purpose |
4
- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5
- | [CONSUMER-RULES.md](./CONSUMER-RULES.md) | **Start here if you build an app with @godxjp/ui** — the ten rules the audit enforces (styles entry, layout owners, real primitives, tokens) |
6
- | [DEVELOPMENT.md](./DEVELOPMENT.md) | **Contributor guideline** — role/boundary, architecture, how to add/extend, verify |
7
- | [COMPONENTS.md](./COMPONENTS.md) | Component catalog by Ant Design group |
8
- | [PROPS-VOCABULARY.md](./PROPS-VOCABULARY.md) | Atomic prop types (`*Prop` suffix) |
9
- | [PROPS-REGISTRY.md](./PROPS-REGISTRY.md) | Machine-readable registry + forbidden aliases |
10
- | [DATETIME.md](./DATETIME.md) | **Mandatory** `formatDate` — all date/time display |
11
- | [DESIGN-AUTHORITY.md](./DESIGN-AUTHORITY.md) | **Which external standard owns which decision** — APG, Radix, shadcn, Ant Design groups, SmartHR colour, Carbon geometry. Read before inventing a spacing/density/layout answer |
12
- | [SPACING.md](./SPACING.md) | Golden ratio (φ) macro spacing — PageContainer sections, Flex gap, Card rhythm |
13
- | [FORMS.md](./FORMS.md) | **Mandatory** react-hook-form + Zod 4 |
14
- | [TESTING.md](./TESTING.md) | **Mandatory** Vitest per component |
15
- | [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) | Per-frame axe a11y + geometry + coverage CI gates — how to run/regenerate locally |
16
- | [../README.md](../README.md) | Setup, workspace wiring, theme |
3
+ | Doc | Purpose |
4
+ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5
+ | [CONSUMER-RULES.md](./CONSUMER-RULES.md) | **Start here if you build an app with @godxjp/ui** — the ten rules the audit enforces (styles entry, layout owners, real primitives, tokens) |
6
+ | [DEVELOPMENT.md](./DEVELOPMENT.md) | **Contributor guideline** — role/boundary, architecture, how to add/extend, verify |
7
+ | [COMPONENTS.md](./COMPONENTS.md) | Component catalog by taxonomy group |
8
+ | [PROPS-VOCABULARY.md](./PROPS-VOCABULARY.md) | Atomic prop types (`*Prop` suffix) |
9
+ | [PROPS-REGISTRY.md](./PROPS-REGISTRY.md) | Machine-readable registry + forbidden aliases |
10
+ | [DATETIME.md](./DATETIME.md) | **Mandatory** `formatDate` — all date/time display |
11
+ | [DESIGN-AUTHORITY.md](./DESIGN-AUTHORITY.md) | **Which external standard owns which decision** — APG, Radix, shadcn, Ant Design groups, SmartHR colour, Carbon geometry, measured contrast for derived colour. Read before inventing a spacing/density/layout answer |
12
+ | [SPACING.md](./SPACING.md) | Golden ratio (φ) macro spacing — PageContainer sections, Flex gap, Card rhythm |
13
+ | [FORMS.md](./FORMS.md) | **Mandatory** react-hook-form + Zod 4 |
14
+ | [TESTING.md](./TESTING.md) | **Mandatory** Vitest per component |
15
+ | [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) | Per-frame axe a11y + geometry + coverage CI gates — how to run/regenerate locally |
16
+ | [../README.md](../README.md) | Setup, workspace wiring, theme |
17
17
 
18
18
  ## Preview
19
19
 
@@ -0,0 +1,179 @@
1
+ # Cái gì đáng đưa vào @godxjp/ui
2
+
3
+ Một design system hỏng theo hai hướng ngược nhau, và cả hai đều hỏng thật:
4
+
5
+ - **Quá hẹp** — thiếu prop, nên consumer buộc phải lách bằng class hoặc mã màu
6
+ gõ tay. Đây là hướng đang hỏng: một lượt kiểm trên godx-chat (08/09/2026) ra
7
+ **51 lỗi, 42 trong đó đến từ ĐÚNG MỘT prop còn thiếu** (đệm trên primitive bố
8
+ cục).
9
+ - **Quá rộng** — nhận mọi yêu cầu, thành một đống prop không ai nhớ, và mỗi
10
+ prop là một mảnh DOM nội bộ bị đóng băng thành API công khai.
11
+
12
+ Tài liệu này là bộ lọc giữa hai hướng đó. Nó cố tình viết thành câu hỏi **trả
13
+ lời được bằng có/không**, không phải nguyên tắc để cảm nhận.
14
+
15
+ ---
16
+
17
+ ## Ba câu hỏi — phải ĐỦ CẢ BA
18
+
19
+ ### 1. Có phải consumer KHÔNG CÓ nước đi hợp lệ nào không?
20
+
21
+ Không phải "bất tiện". Là **bất khả**: mọi prop và token đang có đều không nói
22
+ được điều cần nói, và mọi đường còn lại đều bị `ui-audit` chặn.
23
+
24
+ - ✅ Thiết kế cần 12px giữa hai phần tử trên một HÀNG. Thang bậc tên đọc trục
25
+ dọc nên `md` = 16px; làm tròn thì lệch bố cục; viết `gap: 12px` thì
26
+ `no-arbitrary-spacing` chặn. **Không có nước đi.** → gh#401, đã vá.
27
+ - ❌ "Viết `<Flex gap={3}>` dài hơn `gap-3`." Có nước đi, chỉ là dài hơn.
28
+
29
+ Cách kiểm: viết ra đoạn mã bạn _muốn_ viết, chạy `ui-audit` lên nó. Nếu nó
30
+ xanh, bạn đã có nước đi.
31
+
32
+ ### 2. Nó thuộc về HÌNH DẠNG của component, hay thuộc về NỘI DUNG của một màn?
33
+
34
+ Design system sở hữu hình dạng. Màn hình sở hữu nội dung.
35
+
36
+ - ✅ `Flex` không có đệm. Mọi hàng/cột trong mọi ứng dụng đều có thể cần đệm.
37
+ - ✅ `TableHead` không có trục căn lề. Mọi bảng có cột số đều cần.
38
+ - ❌ "Bảng quản trị thành viên cần cột vai trò rộng 8rem." Đó là màn hình ấy.
39
+ - ❌ "Dashboard cần bốn thẻ chỉ số xếp ngang." Đó là bố cục của một trang.
40
+
41
+ Câu hỏi phụ khi phân vân: **consumer thứ hai có gặp không?** Nếu câu trả lời là
42
+ "chắc là không" thì gần như luôn là nội dung, không phải hình dạng.
43
+
44
+ ### 3. Diễn đạt được thành một TRỤC CÓ TÊN không?
45
+
46
+ Một prop phải gọi được **ý định**, không chỉ mở một lỗ.
47
+
48
+ - ✅ `pad={{ blockStart: 3 }}` — trục logic, bậc thang, đọc ra nghĩa.
49
+ - ❌ `styles={{ body: {...}, header: {...} }}` — không phải một trục; nó là một
50
+ lỗ tự do, và nó đóng băng tên khe DOM nội bộ thành API công khai. Xem mục
51
+ "Không đáng" bên dưới.
52
+
53
+ ---
54
+
55
+ ## Bốn dấu hiệu MẠNH — một cái là đủ để ưu tiên làm ngay
56
+
57
+ Ba câu trên quyết định _có thuộc về đây không_. Bốn dấu hiệu này quyết định
58
+ _làm trước hay sau_.
59
+
60
+ ### a. Nó chặn một yêu cầu TIẾP CẬN
61
+
62
+ Không thương lượng, làm trước.
63
+ Ví dụ đã gặp: vòng tiêu điểm của Input là 1px (hạ xuống có chủ ý), không
64
+ đạt WCAG 2.4.11 vốn đòi vùng chỉ báo tương đương đường 2px. Và `FormRoot` không
65
+ có móc `onInvalid`, nên không đưa được tiêu điểm về trường lỗi đầu tiên — WCAG
66
+ 3.3.1 / 2.4.3.
67
+
68
+ ### b. Nó hỏng IM LẶNG
69
+
70
+ Không lỗi biên dịch, không test đỏ, không cảnh báo — chỉ sai.
71
+ Ví dụ đã gặp: `AppShell` nhận cả `logo` lẫn `topbar` nhưng `resolvedTopbar` trả
72
+ thẳng `topbar`, nên `logo` **không bao giờ được vẽ**. godx-chat truyền cả hai
73
+ suốt nhiều tháng và không ai phát hiện.
74
+
75
+ Lớp lỗi này đắt gấp nhiều lần lớp lỗi ồn ào, vì nó không có ai báo.
76
+
77
+ ### c. Đếm được
78
+
79
+ Một khoảng trống đẻ ra N lỗi audit là một khoảng trống có số đo, không phải một
80
+ ý kiến. 42 lỗi từ một prop thiếu là một ưu tiên; một lỗi từ một prop thiếu thì
81
+ chưa chắc.
82
+
83
+ ### d. Nó BẤT ĐỐI XỨNG với thứ đã có
84
+
85
+ Nếu trục dọc có `xl` mà trục ngang không có, khoảng trống ấy gần như luôn là
86
+ **quên**, không phải quyết định. Sửa cho cân là rẻ và không tranh cãi.
87
+
88
+ ---
89
+
90
+ ## Cái KHÔNG đáng vào — và vì sao
91
+
92
+ **Bố cục của một màn hình cụ thể.** Nó thuộc về màn hình đó. Nếu ba màn cùng
93
+ cần, lúc ấy nó đã thành hình dạng và quay lại câu hỏi 2.
94
+
95
+ **Một giá trị dùng đúng một lần.** Đừng đẻ prop cho nó — dùng cửa thoát
96
+ (`gapRaw`, `padRaw`). Cửa thoát để lại `data-*-raw` trên DOM nên đếm được; khi
97
+ số đếm ấy lớn lên, ĐÓ mới là lúc nó thành một trục đáng có tên.
98
+
99
+ **Lỗ kiểu dáng tự do** (`styles={{ slot }}`, `classNames={{ slot }}`). Nó đóng
100
+ băng cấu trúc DOM nội bộ thành API công khai, và audit **không đếm được** nó
101
+ (một object style dựng ở đâu cũng được rồi spread vào). Nó cũng ngược hẳn với
102
+ `style: 0/289` — lập trường nhất quán hiện nay là cho prop ý định, không cho lỗ
103
+ kiểu dáng.
104
+
105
+ **Gu thẩm mỹ.** "Dùng nhiều khoảng trắng", "hai màu nhấn là đủ", "tránh lưới
106
+ bento". Không kiểm chứng được, không cổng nào canh, và khi nó tới tay agent
107
+ TRƯỚC luật cứng thì kết quả là mã đẹp mà sai hợp đồng.
108
+
109
+ **Thứ chỉ tiết kiệm vài dòng ở call site.** Nếu đường hiện tại đã hợp lệ và
110
+ đọc được, việc rút ngắn nó không phải việc của design system.
111
+
112
+ ---
113
+
114
+ ## Thêm rồi thì NỢ gì
115
+
116
+ Một prop mới chưa xong khi nó biên dịch được. Ba việc bắt buộc, và cả ba đều đã
117
+ có cổng CI:
118
+
119
+ 1. **Vào catalog.** Chạy `node scripts/gen-component-api-manifest.mjs`.
120
+ `check:component-api-manifest` canh việc này. **Không vào catalog thì agent
121
+ không biết nó tồn tại** — và đó không phải giả thuyết: `pad`/`padRaw` được
122
+ thêm vào gói nhưng catalog phát hành chưa có, nên một agent tra MCP đúng quy
123
+ trình vẫn kết luận "Flex chỉ có gap" và bỏ cuộc trước 21 lỗi mà nó có thể
124
+ sửa.
125
+
126
+ 2. **Có bằng chứng phủ từng nhánh giá trị.** `component-case-evidence.json` +
127
+ `check:frame-coverage-ledger`. Bằng chứng phải trỏ vào test THẬT SỰ chạy qua
128
+ nhánh ấy, không phải một tệp cho có.
129
+
130
+ 3. **Có ví dụ BIÊN DỊCH ĐƯỢC.** Hậu quả đo được khi không kiểm: pattern
131
+ `confirm-destructive` truyền cho `Dialog` một prop `mode` không tồn tại,
132
+ hướng dẫn DataTable dùng `variant="success"` (chính thứ luật
133
+ `status-tone-not-variant` cấm), và ví dụ `Input` gọi một prop
134
+ `onValueChange` không có thật. Agent chép chúng và viết ra mã hỏng.
135
+
136
+ Ba chỗ ấy đã sửa, và `check:doc-prop-existence` nay bắt được lớp lỗi đó.
137
+ Lưu ý khi viết tài liệu: cổng ấy đọc mọi đoạn JSX trong `docs/**` như mã
138
+ THẬT, nên **trích dẫn một API sai để làm ví dụ cũng bị bắt**. Mô tả bằng lời
139
+ như đoạn trên, đừng dán một thẻ JSX hoàn chỉnh vào.
140
+
141
+ ---
142
+
143
+ ## Một lợi ích ẩn của việc rời Radix, đo được
144
+
145
+ `check:doc-prop-existence` canh chiều "mọi prop dùng trong ví dụ đều phải tồn
146
+ tại" — chính lớp lỗi khiến agent chép ví dụ rồi viết ra mã hỏng (prop `mode`
147
+ trên `Dialog`, prop `onValueChange` trên `Input`). Nhưng nó **bỏ qua mọi
148
+ component bọc primitive bên thứ ba**, vì với chúng manifest chỉ là cận dưới:
149
+ prop thật nằm trong types của Radix, generator không mở ra được.
150
+
151
+ Số đo (08/09/2026):
152
+
153
+ | | component KHÔNG kiểm được |
154
+ | --------------------------------------------- | ------------------------- |
155
+ | `main` | **139 / 289** |
156
+ | `feat/v20-react-aria` (6 primitive đã chuyển) | **126 / 289** |
157
+
158
+ Mỗi primitive rời Radix là `declaredIn` chuyển từ `node_modules` vào `src/`, và
159
+ component đó **bước vào vùng kiểm được**. Sáu primitive đầu đã gỡ rào cho 13
160
+ component.
161
+
162
+ Nên đợt đổi nền không chỉ là thay thư viện. Nó là cách duy nhất làm cho 126
163
+ component còn lại có thể được canh — và trong số đó có `Dialog`, `Input`,
164
+ `Badge`, tức đúng những component mà ví dụ sai đã lọt qua.
165
+
166
+ ## Khi câu trả lời là "không thuộc về đây"
167
+
168
+ Nói ra ở chỗ gặp nó, bằng mã:
169
+
170
+ ```tsx
171
+ /*
172
+ * KHÔNG thuộc về design system: bề rộng này là số đo của riêng màn hình
173
+ * quản trị thành viên, không phải một trục của Table. Nếu màn thứ hai cần
174
+ * cùng thứ, lúc đó mở issue.
175
+ */
176
+ ```
177
+
178
+ Một dòng như thế đáng giá hơn một issue mở rồi bỏ đấy — nó nói cho người sau
179
+ biết ai đã cân nhắc, và cân nhắc ra sao.