@godxjp/ui 20.0.0 → 20.2.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 (273) hide show
  1. package/README.md +8 -2
  2. package/dist/app/app-provider.js +37 -21
  3. package/dist/app/index.d.ts +2 -0
  4. package/dist/app/index.js +2 -0
  5. package/dist/app/storage.d.ts +17 -0
  6. package/dist/app/storage.js +28 -0
  7. package/dist/app/theme-axes.d.ts +2 -0
  8. package/dist/app/theme-axes.js +45 -0
  9. package/dist/components/charts/chart-cartesian.d.ts +2 -1
  10. package/dist/components/charts/chart-cartesian.js +52 -3
  11. package/dist/components/charts/chart-category-axis.d.ts +55 -0
  12. package/dist/components/charts/chart-category-axis.js +93 -0
  13. package/dist/components/charts/chart-frame.d.ts +10 -1
  14. package/dist/components/charts/chart-frame.js +19 -3
  15. package/dist/components/charts/compact-bar-trend.d.ts +1 -1
  16. package/dist/components/charts/compact-bar-trend.js +2 -0
  17. package/dist/components/charts/pie-chart.d.ts +1 -1
  18. package/dist/components/charts/pie-chart.js +12 -1
  19. package/dist/components/charts/recharts-peer.d.ts +49 -0
  20. package/dist/components/charts/recharts-peer.js +45 -0
  21. package/dist/components/data-display/card.d.ts +12 -1
  22. package/dist/components/data-display/card.js +22 -4
  23. package/dist/components/data-display/code-block.js +6 -2
  24. package/dist/components/data-display/data-table.d.ts +39 -4
  25. package/dist/components/data-display/data-table.js +802 -270
  26. package/dist/components/data-display/descriptions.d.ts +27 -6
  27. package/dist/components/data-display/descriptions.js +58 -17
  28. package/dist/components/data-display/index.d.ts +2 -0
  29. package/dist/components/data-display/index.js +2 -0
  30. package/dist/components/data-display/list-row.d.ts +10 -1
  31. package/dist/components/data-display/list-row.js +7 -1
  32. package/dist/components/data-display/popover.d.ts +9 -3
  33. package/dist/components/data-display/popover.js +65 -11
  34. package/dist/components/data-display/range-timeline.d.ts +38 -0
  35. package/dist/components/data-display/range-timeline.js +161 -0
  36. package/dist/components/data-display/scroll-area.js +13 -2
  37. package/dist/components/data-display/service-launcher-card.js +14 -12
  38. package/dist/components/data-display/table.d.ts +71 -3
  39. package/dist/components/data-display/table.js +56 -18
  40. package/dist/components/data-display/tree-list.js +4 -9
  41. package/dist/components/data-entry/calendar.d.ts +2 -2
  42. package/dist/components/data-entry/calendar.js +23 -3
  43. package/dist/components/data-entry/cascader.d.ts +1 -1
  44. package/dist/components/data-entry/cascader.js +188 -24
  45. package/dist/components/data-entry/color-picker.d.ts +1 -1
  46. package/dist/components/data-entry/color-picker.js +17 -5
  47. package/dist/components/data-entry/control-appearance.d.ts +64 -0
  48. package/dist/components/data-entry/control-appearance.js +39 -0
  49. package/dist/components/data-entry/control-surface.d.ts +61 -0
  50. package/dist/components/data-entry/control-surface.js +39 -0
  51. package/dist/components/data-entry/date-picker.d.ts +1 -1
  52. package/dist/components/data-entry/date-picker.js +344 -113
  53. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  54. package/dist/components/data-entry/date-range-picker.js +278 -140
  55. package/dist/components/data-entry/form-field.d.ts +1 -1
  56. package/dist/components/data-entry/form-field.js +39 -4
  57. package/dist/components/data-entry/form.d.ts +4 -0
  58. package/dist/components/data-entry/form.js +4 -2
  59. package/dist/components/data-entry/index.d.ts +7 -3
  60. package/dist/components/data-entry/index.js +10 -1
  61. package/dist/components/data-entry/input-otp.d.ts +1 -1
  62. package/dist/components/data-entry/input.d.ts +12 -34
  63. package/dist/components/data-entry/input.js +92 -24
  64. package/dist/components/data-entry/month-picker.d.ts +1 -1
  65. package/dist/components/data-entry/month-picker.js +47 -10
  66. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  67. package/dist/components/data-entry/month-range-picker.js +51 -11
  68. package/dist/components/data-entry/number-input.d.ts +7 -0
  69. package/dist/components/data-entry/number-input.js +147 -98
  70. package/dist/components/data-entry/password-input.d.ts +1 -1
  71. package/dist/components/data-entry/radio.d.ts +1 -1
  72. package/dist/components/data-entry/radio.js +61 -13
  73. package/dist/components/data-entry/search-input.d.ts +1 -1
  74. package/dist/components/data-entry/search-input.js +16 -2
  75. package/dist/components/data-entry/search-select.d.ts +2 -2
  76. package/dist/components/data-entry/search-select.js +209 -78
  77. package/dist/components/data-entry/select.d.ts +22 -4
  78. package/dist/components/data-entry/select.js +215 -163
  79. package/dist/components/data-entry/slider.d.ts +9 -1
  80. package/dist/components/data-entry/slider.js +87 -9
  81. package/dist/components/data-entry/switch.d.ts +3 -0
  82. package/dist/components/data-entry/switch.js +29 -3
  83. package/dist/components/data-entry/textarea.d.ts +14 -56
  84. package/dist/components/data-entry/textarea.js +80 -33
  85. package/dist/components/data-entry/time-picker.d.ts +2 -2
  86. package/dist/components/data-entry/time-picker.js +333 -109
  87. package/dist/components/data-entry/time-range-picker.d.ts +5 -0
  88. package/dist/components/data-entry/time-range-picker.js +89 -0
  89. package/dist/components/data-entry/transfer.d.ts +1 -1
  90. package/dist/components/data-entry/transfer.js +86 -28
  91. package/dist/components/data-entry/tree-select.d.ts +1 -1
  92. package/dist/components/data-entry/tree-select.js +201 -113
  93. package/dist/components/data-entry/tree-utils.js +7 -14
  94. package/dist/components/data-entry/upload-files.d.ts +2 -0
  95. package/dist/components/data-entry/upload-files.js +31 -0
  96. package/dist/components/data-entry/upload-request.d.ts +4 -0
  97. package/dist/components/data-entry/upload-request.js +53 -0
  98. package/dist/components/data-entry/upload-types.d.ts +28 -0
  99. package/dist/components/data-entry/upload-types.js +2 -0
  100. package/dist/components/data-entry/upload.d.ts +2 -2
  101. package/dist/components/data-entry/upload.js +475 -115
  102. package/dist/components/feedback/dialog.js +19 -19
  103. package/dist/components/feedback/sheet.js +3 -0
  104. package/dist/components/feedback/tooltip.js +3 -0
  105. package/dist/components/general/button.d.ts +2 -1
  106. package/dist/components/general/button.js +7 -1
  107. package/dist/components/general/index.d.ts +1 -0
  108. package/dist/components/general/index.js +2 -0
  109. package/dist/components/general/typography.d.ts +3 -0
  110. package/dist/components/general/typography.js +15 -1
  111. package/dist/components/general/visually-hidden.d.ts +3 -0
  112. package/dist/components/general/visually-hidden.js +9 -0
  113. package/dist/components/layout/app-launcher.d.ts +34 -0
  114. package/dist/components/layout/app-launcher.js +261 -0
  115. package/dist/components/layout/app-shell.d.ts +3 -1
  116. package/dist/components/layout/app-shell.js +34 -14
  117. package/dist/components/layout/auth-shell.d.ts +1 -1
  118. package/dist/components/layout/auth-shell.js +2 -0
  119. package/dist/components/layout/breadcrumb.d.ts +14 -2
  120. package/dist/components/layout/breadcrumb.js +46 -4
  121. package/dist/components/layout/flex.d.ts +1 -1
  122. package/dist/components/layout/flex.js +23 -2
  123. package/dist/components/layout/index.d.ts +3 -0
  124. package/dist/components/layout/index.js +5 -1
  125. package/dist/components/layout/org-switcher.js +20 -1
  126. package/dist/components/layout/page-container.d.ts +1 -1
  127. package/dist/components/layout/page-container.js +5 -3
  128. package/dist/components/layout/responsive-grid.d.ts +16 -2
  129. package/dist/components/layout/responsive-grid.js +29 -2
  130. package/dist/components/layout/sidebar.js +2 -1
  131. package/dist/components/layout/topbar-item.d.ts +3 -0
  132. package/dist/components/layout/topbar-item.js +18 -3
  133. package/dist/components/layout/topbar.d.ts +1 -1
  134. package/dist/components/layout/topbar.js +27 -6
  135. package/dist/components/navigation/dropdown-menu.d.ts +30 -2
  136. package/dist/components/navigation/dropdown-menu.js +73 -11
  137. package/dist/components/navigation/pagination.d.ts +2 -2
  138. package/dist/components/navigation/pagination.js +155 -83
  139. package/dist/components/navigation/steps.d.ts +2 -2
  140. package/dist/components/navigation/steps.js +29 -4
  141. package/dist/components/navigation/tabs.d.ts +5 -33
  142. package/dist/components/navigation/tabs.js +232 -64
  143. package/dist/components/ui/hover-card.js +3 -0
  144. package/dist/components/ui/input-otp.d.ts +26 -28
  145. package/dist/components/ui/input-otp.js +50 -7
  146. package/dist/components/ui/password-input.d.ts +36 -2
  147. package/dist/components/ui/password-input.js +27 -7
  148. package/dist/components/ui/rating.d.ts +25 -0
  149. package/dist/components/ui/rating.js +67 -27
  150. package/dist/components/ui/segmented.d.ts +9 -0
  151. package/dist/components/ui/segmented.js +17 -2
  152. package/dist/components/ui/separator.d.ts +4 -0
  153. package/dist/components/ui/separator.js +13 -0
  154. package/dist/components/ui/tag-input.d.ts +45 -0
  155. package/dist/components/ui/tag-input.js +109 -27
  156. package/dist/form/form-context.d.ts +29 -0
  157. package/dist/form/form-context.js +44 -4
  158. package/dist/form/form-field-array.d.ts +22 -0
  159. package/dist/form/form-field-array.js +50 -0
  160. package/dist/form/form-field-control.d.ts +2 -1
  161. package/dist/form/form-field-control.js +116 -40
  162. package/dist/form/form-root.d.ts +2 -1
  163. package/dist/form/form-root.js +142 -23
  164. package/dist/form/index.d.ts +3 -1
  165. package/dist/form/index.js +12 -1
  166. package/dist/i18n/index.d.ts +1 -1
  167. package/dist/i18n/index.js +3 -1
  168. package/dist/i18n/messages/en.json +59 -6
  169. package/dist/i18n/messages/ja.json +59 -6
  170. package/dist/i18n/messages/vi.json +59 -6
  171. package/dist/i18n/translate.d.ts +19 -0
  172. package/dist/i18n/translate.js +24 -0
  173. package/dist/inertia/index.d.ts +20 -0
  174. package/dist/inertia/index.js +60 -1
  175. package/dist/lib/control-styles.d.ts +25 -3
  176. package/dist/lib/control-styles.js +9 -3
  177. package/dist/lib/datetime/picker-format.d.ts +8 -0
  178. package/dist/lib/datetime/picker-format.js +54 -0
  179. package/dist/lib/overlay-portal.d.ts +18 -0
  180. package/dist/lib/overlay-portal.js +17 -0
  181. package/dist/props/components/app.prop.d.ts +13 -2
  182. package/dist/props/components/charts.prop.d.ts +21 -0
  183. package/dist/props/components/data-display.prop.d.ts +55 -4
  184. package/dist/props/components/data-entry.prop.d.ts +687 -43
  185. package/dist/props/components/form.prop.d.ts +142 -4
  186. package/dist/props/components/general.prop.d.ts +30 -1
  187. package/dist/props/components/index.d.ts +2 -2
  188. package/dist/props/components/layout.prop.d.ts +255 -8
  189. package/dist/props/components/navigation.prop.d.ts +113 -3
  190. package/dist/props/registry.d.ts +376 -10
  191. package/dist/props/registry.js +493 -9
  192. package/dist/props/vocabulary/data.prop.d.ts +179 -1
  193. package/dist/props/vocabulary/index.d.ts +4 -4
  194. package/dist/props/vocabulary/interaction.prop.d.ts +51 -2
  195. package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
  196. package/dist/props/vocabulary/shared.prop.d.ts +33 -0
  197. package/dist/styles/badge-layout.css +2 -1
  198. package/dist/styles/card-layout.css +36 -5
  199. package/dist/styles/chart-layout.css +17 -1
  200. package/dist/styles/control.css +520 -14
  201. package/dist/styles/data-display-layout.css +248 -7
  202. package/dist/styles/data-entry-layout.css +13 -0
  203. package/dist/styles/layout.css +221 -4
  204. package/dist/styles/navigation-layout.css +230 -1
  205. package/dist/styles/shell-layout.css +690 -21
  206. package/dist/styles/table-layout.css +95 -6
  207. package/dist/styles/text-layout.css +5 -0
  208. package/dist/tokens/base.css +1 -0
  209. package/dist/tokens/components/badge.css +1 -0
  210. package/dist/tokens/components/chart.css +6 -0
  211. package/dist/tokens/components/control.css +81 -3
  212. package/dist/tokens/components/data-display.css +9 -0
  213. package/dist/tokens/components/descriptions.css +11 -0
  214. package/dist/tokens/components/flex.css +8 -0
  215. package/dist/tokens/components/navigation.css +55 -0
  216. package/dist/tokens/components/shell.css +100 -5
  217. package/dist/tokens/components/table.css +14 -0
  218. package/dist/tokens/foundation.css +2 -0
  219. package/dist/tokens/semantic/layout.css +9 -0
  220. package/docs/CONSUMER-RULES.md +13 -0
  221. package/docs/DESIGN-AUTHORITY.md +43 -6
  222. package/docs/DEVELOPMENT.md +4 -3
  223. package/docs/FORMS.md +151 -6
  224. package/docs/FRAME-COVERAGE-REPORT.md +29 -16
  225. package/docs/README.md +1 -1
  226. package/docs/STANDARDS-vocabulary-tokens.md +1 -1
  227. package/docs/TESTING.md +15 -6
  228. package/docs/charts/cjk-category-axis.tsx +81 -0
  229. package/docs/data-display/card/index.tsx +22 -0
  230. package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
  231. package/docs/data-display/data-table/index.tsx +23 -0
  232. package/docs/data-display/descriptions.tsx +41 -0
  233. package/docs/data-display/list-row.tsx +6 -6
  234. package/docs/data-display/scroll-area.tsx +31 -25
  235. package/docs/data-display/table.tsx +104 -45
  236. package/docs/data-display/timeline.tsx +41 -0
  237. package/docs/data-entry/date-picker.tsx +85 -0
  238. package/docs/data-entry/date-range-picker.tsx +26 -0
  239. package/docs/data-entry/form-dynamic-fields.tsx +199 -0
  240. package/docs/data-entry/form.tsx +74 -4
  241. package/docs/data-entry/input-otp.tsx +24 -0
  242. package/docs/data-entry/input.tsx +46 -1
  243. package/docs/data-entry/month-range-picker.tsx +1 -1
  244. package/docs/data-entry/select.tsx +27 -0
  245. package/docs/data-entry/switch.tsx +41 -0
  246. package/docs/data-entry/textarea.tsx +38 -0
  247. package/docs/data-entry/time-picker.tsx +85 -1
  248. package/docs/data-entry/time-range-picker.tsx +55 -0
  249. package/docs/data-entry/transfer.tsx +17 -0
  250. package/docs/data-entry/upload.tsx +56 -12
  251. package/docs/feedback/tooltip.tsx +1 -1
  252. package/docs/general/activity.tsx +2 -2
  253. package/docs/general/button/index.tsx +14 -1
  254. package/docs/general/typography.tsx +14 -1
  255. package/docs/layout/app-launcher.tsx +195 -0
  256. package/docs/layout/app-shell-arrangements.tsx +5 -5
  257. package/docs/layout/app-shell.tsx +11 -0
  258. package/docs/layout/flex.tsx +50 -0
  259. package/docs/layout/responsive-grid.tsx +21 -1
  260. package/docs/layout/topbar.tsx +5 -9
  261. package/docs/navigation/app-setting-picker.tsx +11 -0
  262. package/docs/navigation/breadcrumb.tsx +48 -0
  263. package/docs/navigation/dropdown-menu.tsx +21 -0
  264. package/docs/navigation/pagination.tsx +52 -0
  265. package/docs/navigation/steps.tsx +37 -0
  266. package/docs/navigation/tabs.tsx +97 -1
  267. package/docs/query/button-refetch.tsx +1 -0
  268. package/docs/showcase/settings-security-mfa.tsx +3 -3
  269. package/package.json +24 -4
  270. package/scripts/_agent-setup.mjs +76 -9
  271. package/scripts/init-guinea-pig.mjs +26 -4
  272. package/scripts/ui-audit.mjs +243 -30
  273. package/scripts/visual-audit.mjs +45 -7
@@ -18,6 +18,8 @@
18
18
  --overlay-z-index: 50;
19
19
 
20
20
  --master-detail-rail-compact: 18.75rem;
21
+ --master-detail-rail-narrow: 12rem;
22
+ --master-detail-rail-wide: 24rem;
21
23
  --master-detail-rail-standard: 20rem;
22
24
  --master-detail-gap: var(--space-stack-md);
23
25
 
@@ -37,6 +39,8 @@
37
39
 
38
40
  --overlay-background: rgb(0 0 0 / 0.5);
39
41
 
42
+ --surface-recessed: hsl(var(--muted) / 0.4);
43
+
40
44
  --page-footer-pad-block-end: var(--space-stack-md);
41
45
  --page-header-divider: none;
42
46
 
@@ -80,6 +84,11 @@
80
84
  --field-label-gap: var(--space-2);
81
85
  }
82
86
 
87
+ .dark,
88
+ :root[data-theme="dark"] {
89
+ --surface-recessed: hsl(var(--background));
90
+ }
91
+
83
92
  :root {
84
93
  --admin-collection-control-height: var(--control-height-default);
85
94
  --admin-collection-section-gap: var(--space-stack-md);
@@ -12,3 +12,16 @@ Read this once; the audit enforces it. Everything else in `docs/` is for contrib
12
12
  8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
13
13
  9. Logical directions (`ms-`, `me-`, `start-`, `end-`) when a utility is unavoidable; never `ml-` / `left-`.
14
14
  10. Run `node node_modules/@godxjp/ui/scripts/ui-audit.mjs <dir>` before every review; then `visual-audit.mjs <url>` on the running app. Zero errors is the bar.
15
+
16
+ **Opting a deliberate exception out.** Name the rule; the block form also has to say why.
17
+
18
+ ```tsx
19
+ // ui-audit-disable-line no-physical-direction
20
+ // ui-audit-disable-next-line no-physical-direction
21
+
22
+ // ui-audit-disable-begin no-utility-spacing — vendor widget ships its own grid, gh#123
23
+ const legacyClasses = ["gap-3", "p-2"];
24
+ // ui-audit-disable-end no-utility-spacing
25
+ ```
26
+
27
+ A block with no reason (or under 12 characters of it) is ignored and the finding stands; an unclosed block runs to the end of the file. The class-shaped rules (`gap-*`, `bg-red-500`, `w-[37px]`, `pr-*`, `dark:*`) only read class expressions — a `className`/`class` attribute, a class-named binding (`baseClass`, `statusStyles`, `badgeVariants`) or a `cn()`/`clsx()`/`cva()` call — so product copy or an i18n value that happens to spell a utility is never a finding.
@@ -187,6 +187,43 @@ 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
+ ## The PROP SURFACE of a component is antd's too, not just its group name
191
+
192
+ antd was already the authority for which components exist and which group they live in. That
193
+ answered "is there a Table" and never answered "what does a Table take". So every prop was decided
194
+ here, one at a time, and the answer drifted per component: `DataTable` grew `pin: "end"` where antd
195
+ has `fixed`, `sortable: true` where antd has `sorter`, and no answer at all for filters, expandable
196
+ rows or a totals row — which is how a consumer ends up hand-rolling a `<tfoot>` and a sticky column
197
+ in page CSS.
198
+
199
+ **The rule: where antd names a capability, this library takes antd's name and antd's semantics.**
200
+ The gap is read out of the INSTALLED types (`antd/es/table/interface.d.ts`,
201
+ `antd/es/table/InternalTable.d.ts` and the `@rc-component/table` interface they extend) — never
202
+ from memory, because antd's own names move between majors (`fixed: 'left'` is deprecated in favour
203
+ of `start` inside rc-table itself).
204
+
205
+ > Đọc sau 20.0.0: bản major ấy đã **gỡ `antd` khỏi devDependencies** cùng máy sinh màu của nó, và
206
+ > `check:no-antd-runtime` canh cho nó không quay lại. Câu trên mô tả cách bề mặt prop này ĐƯỢC ĐỌC
207
+ > lúc antd còn cài, không phải một lời mời cài lại. Lần sau muốn đối chiếu, đọc type ở một checkout
208
+ > riêng rồi ghi số hiệu bản vào đây — đừng thêm dependency.
209
+
210
+ **Three things override antd's spelling, each for a stated reason:**
211
+
212
+ - **Logical over physical.** antd's `fixed: 'left' | 'right'` cannot mirror for an RTL locale, so
213
+ only `start` / `end` are published. Same rule that makes `check:rtl` a gate.
214
+ - **This library's controlled vocabulary wins on values.** antd's `SortOrder` is
215
+ `'ascend' | 'descend'`; here it stays `SortDirectionProp` (`asc` / `desc`), because that type
216
+ already exists and a second spelling of the same axis is exactly what `check:prop-vocabulary`
217
+ exists to prevent.
218
+ - **A capability this library already has keeps its own name.** antd's `size`
219
+ (`small | middle | large`) IS `density` (`compact | default | comfortable`); antd's `locale` IS
220
+ the `t()` layer. Adding the antd spelling as an alias would be duplication, not parity.
221
+
222
+ **A knob that only a fork could reach is not parity either.** antd's `components`,
223
+ `filterDropdown`, `classNames`/`styles` semantic maps and `prefixCls` all exist to let a consumer
224
+ replace the rendered markup. This library answers that layer with tokens (cardinal rule #45), so
225
+ those are deliberately NOT adopted — adopting them would re-open the hole the token tiers close.
226
+
190
227
  ## Derived colour is AUTHORED, and MEASUREMENT is what makes it authoritative
191
228
 
192
229
  **Twenty values, in `src/tokens/derived.css`:** `--primary-hover`, `--primary-active`,
@@ -293,7 +330,7 @@ owner chose to ship it off and let whoever needs it turn it on. What that costs,
293
330
  **Turning it back on is one attribute, on the root element, with no code change:**
294
331
 
295
332
  ```html
296
- <html data-focus-outline="on">
333
+ <html data-focus-outline="on"></html>
297
334
  ```
298
335
 
299
336
  **The switch is a single multiplier, not a scatter of overrides.** `--focus-outline` is one flag,
@@ -316,11 +353,11 @@ the **field** indicator on every control — one hairline (1px) in the focus hue
316
353
  `--control-outline` halo — rather than the heavy 3px outline form. Measured, in Chromium, on
317
354
  `ql.test` after the transition settles:
318
355
 
319
- | control | switch off | switch on |
320
- | --- | --- | --- |
321
- | 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` |
322
- | Button (primary) | outline `0px`, resting shadow intact | `outline: 1px solid rgb(0,113,189)` @ `0px` + same halo |
323
- | Sidebar nav row / list row | outline `0px` | `outline: 1px solid rgb(0,113,189)` @ **`-1px`** (inset into the row) |
356
+ | control | switch off | switch on |
357
+ | -------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
358
+ | 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` |
359
+ | Button (primary) | outline `0px`, resting shadow intact | `outline: 1px solid rgb(0,113,189)` @ `0px` + same halo |
360
+ | Sidebar nav row / list row | outline `0px` | `outline: 1px solid rgb(0,113,189)` @ **`-1px`** (inset into the row) |
324
361
 
325
362
  The field pair is the recoloured boundary plus the halo, exactly as the convention specifies for
326
363
  this seed. The nav row insets its mark into its own shape rather than wrapping an already-shaded surface,
@@ -117,13 +117,14 @@ A change isn't done until its documentation reflects it:
117
117
  ```bash
118
118
  pnpm lint # eslint — self-contained flat config
119
119
  pnpm typecheck # tsc --noEmit
120
- pnpm test # vitest per component
121
- pnpm preview:build # integration test: examples + docs must build
120
+ pnpm vitest run src/components/<group>/__tests__ --maxWorkers=2 # ONLY what you touched
121
+ pnpm test # FULL suite CI only, never from an agent loop
122
+ pnpm preview:build # integration test: examples + docs must build — at most once, pre-PR
122
123
  pnpm audit # godxjp-ui-audit — 0 errors for touched files
123
124
  pnpm check:mcp-sync # MCP registry ↔ library export drift guard
124
125
  ```
125
126
 
126
- `pnpm verify` and `pnpm verify:release` run these together (verify:release also builds). It needs `pnpm exec playwright install chromium` once locally; see [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) for how to run/scope it, read the evidence, and regenerate its baseline after an accessibility fix.
127
+ `pnpm verify` and `pnpm verify:release` run these together (verify:release also builds) — **both include the full suite, so both belong to CI.** Locally, run them at most once immediately before opening a PR, never inside an edit loop and never while other agents are working on the same machine. It needs `pnpm exec playwright install chromium` once locally; see [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) for how to run/scope it, read the evidence, and regenerate its baseline after an accessibility fix.
127
128
 
128
129
  All gates are **self-contained** — no internal/external tooling package required. The eslint, prettier, and vitest setup live in the package (`eslint.config.js`, `prettier.config.mjs`, `vitest.config.ts`, `src/test/`), so a fresh checkout can lint/type-check/test without anything beyond the declared devDependencies.
129
130
 
package/docs/FORMS.md CHANGED
@@ -44,11 +44,127 @@ export function CreateCustomerForm({ onSubmit }: { onSubmit: (v: Values) => void
44
44
 
45
45
  ## API
46
46
 
47
- | Export | Prop type | Purpose |
48
- | ------------------ | ----------------------------------------- | -------------------------------------- |
49
- | `useZodForm` | `ZodSchemaProp` + `UseZodFormOptionsProp` | Hook — injects zodResolver |
50
- | `FormRoot` | `FormRootProp` | FormProvider + `<form onSubmit>` |
51
- | `FormFieldControl` | `FormFieldControlProp` | Controller + FormField + error display |
47
+ | Export | Prop type | Purpose |
48
+ | ------------------- | ----------------------------------------- | --------------------------------------------- |
49
+ | `useZodForm` | `ZodSchemaProp` + `UseZodFormOptionsProp` | Hook — injects zodResolver |
50
+ | `FormRoot` | `FormRootProp` | FormProvider + `<form onSubmit>` + layout |
51
+ | `FormFieldControl` | `FormFieldControlProp` | Controller + FormField + error display |
52
+ | `FormFieldArray` | `FormFieldArrayProp` | Repeating rows on RHF's `useFieldArray` |
53
+ | `useFormWatch` | — | Subscribe to one field (antd `Form.useWatch`) |
54
+ | `useFormInstance` | — | The RHF instance, without prop-drilling |
55
+ | `useFormSubmitting` | — | Submit in flight (both paths) |
56
+ | `useFormDisabled` | — | Form-level `disabled`, for the action row |
57
+
58
+ ### `FormRoot` — form-level knobs
59
+
60
+ `onSubmit` runs only after validation passes (antd `onFinish`). Everything else is optional:
61
+
62
+ | Prop | antd | Notes |
63
+ | ----------------------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------- |
64
+ | `onSubmitFailed(errors)` | `onFinishFailed` | RHF error tree; the first invalid field is focused regardless |
65
+ | `onSubmitError(error)` | — | Optional error callback; rejected submissions show localized feedback and retain values |
66
+ | `onReset` | — | A native `<Button type="reset">` restores `defaultValues`; this runs after |
67
+ | `scrollToFirstError` | `scrollToFirstError` | Default `true`; centres the field row, honours `prefers-reduced-motion` |
68
+ | `disabled` | Form `disabled` | Disables fields and submission; values remain in state. Buttons read `useFormDisabled()` |
69
+ | `layout` `labelWidth` `controlWidth` `labelAlign` `collapseBelow` | `layout` `labelCol` … | Renders the `Form` layout shell (see the spacing note below) |
70
+ | `density` / `requiredMark` | `size` / `requiredMark` | Forwarded to the same shell |
71
+ | `errors` | — | Server error bag; fields claim their keys, `<FormErrors />` shows the rest |
72
+
73
+ > **Spacing.** A bare `FormRoot` stacks its children with `ui-stack-md`. Passing ANY layout prop
74
+ > switches it to the `Form` shell, whose `--form-block-gap` owns the spacing instead — the two are
75
+ > never combined, so a form does not silently gain double gaps when a `layout` is added.
76
+
77
+ `columns` renders a responsive grid inside the form. Duplicate submissions are ignored until the current attempt settles. Reset returns the form to its defaults.
78
+
79
+ ### `FormFieldControl` — field-level knobs
80
+
81
+ | Prop | antd | Notes |
82
+ | ----------------------------------------- | ------------------- | -------------------------------------------------------------------- |
83
+ | `disabled` | Item `disabled` | Defaults to `FormRoot disabled`; `false` re-enables one field |
84
+ | `dependencies` | `dependencies` | Re-validate this field when another changes (after the first submit) |
85
+ | `getValueFromEvent` | `getValueFromEvent` | Replaces the built-in DOM-event / raw-value detection |
86
+ | `normalize` | `normalize` | Transform before storing; return `previousValue` to REJECT a change |
87
+ | `help` | `help` | Shown instead of the resolved validation message |
88
+ | `validateStatus` `hasFeedback` `feedback` | same | Forwarded to `FormField` — the in-flight remote-check state |
89
+ | `preserve` | `preserve` | `false` unregisters the value when the field unmounts |
90
+
91
+ The binding includes both native `onChange` and godx-ui `onValueChange`; spreading it onto Input, Textarea or Select updates the same store. Native fields do not emit the shared callback twice. IDs are unique across sibling forms. Per-field layout, label/control width, label addons and column spans are forwarded to FormField.
92
+
93
+ **There is no `rules` prop, by design.** react-hook-form ignores per-field register rules whenever a
94
+ `resolver` is set, and `useZodForm` always sets one — a `rules` prop would silently do nothing.
95
+ antd's `rules` is the Zod schema here, including the cross-field case:
96
+
97
+ ```tsx
98
+ const schema = z
99
+ .object({ password: z.string().min(8), confirm: z.string() })
100
+ .refine((v) => v.confirm === v.password, { path: ["confirm"], message: "Mật khẩu không khớp" });
101
+ ```
102
+
103
+ Pair that refinement with `dependencies={["password"]}` on the `confirm` field so editing the
104
+ password re-runs it.
105
+
106
+ ## Dynamic rows — `FormFieldArray` (antd `Form.List`)
107
+
108
+ `FormFieldArray` is react-hook-form's `useFieldArray`, so rows keep their identity across
109
+ insert/remove/move instead of being re-keyed by index. Each row hands back its dotted path prefix;
110
+ build the child field name from it and nested validation lands on the right row.
111
+
112
+ ```tsx
113
+ import { FormFieldArray, FormFieldControl, FormRoot, useZodForm } from "@godxjp/ui/form";
114
+
115
+ const schema = z.object({
116
+ contacts: z.array(z.object({ email: z.string().email() })).min(1, "Cần ít nhất một liên hệ"),
117
+ });
118
+ type Values = z.infer<typeof schema>;
119
+
120
+ <FormFieldArray<Values, "contacts"> name="contacts">
121
+ {({ fields, append, remove, move, error, disabled }) => (
122
+ <>
123
+ {error && (
124
+ <Alert tone="destructive">
125
+ <AlertDescription>{error}</AlertDescription>
126
+ </Alert>
127
+ )}
128
+ {fields.map((row, position) => (
129
+ <Card key={row.key} variant="outline">
130
+ <CardContent>
131
+ <FormFieldControl<Values> name={`${row.name}.email`} label={`Liên hệ ${position + 1}`}>
132
+ {(field) => <Input {...field} value={String(field.value ?? "")} />}
133
+ </FormFieldControl>
134
+ <Button type="button" variant="destructive" onClick={() => remove(row.index)}>
135
+ Xoá
136
+ </Button>
137
+ </CardContent>
138
+ </Card>
139
+ ))}
140
+ <Button
141
+ type="button"
142
+ variant="outline"
143
+ disabled={disabled}
144
+ onClick={() => append({ email: "" })}
145
+ >
146
+ Thêm liên hệ
147
+ </Button>
148
+ </>
149
+ )}
150
+ </FormFieldArray>;
151
+ ```
152
+
153
+ - `row.key` is RHF's stable id — use it as the React key, **never** the index.
154
+ - `row.index` is the live position; pass it to `remove`/`move`, not the render position.
155
+ - `error` is the ARRAY-level message (a `.min(1)` on the array itself), not a row's.
156
+ - Also available: `prepend`, `insert`, `swap`, `replace`.
157
+ - react-hook-form path only — a server-driven `adapter` owns its own collection state.
158
+
159
+ ## Watching a value — `useFormWatch`
160
+
161
+ ```tsx
162
+ const plan = useFormWatch<Values>("plan"); // one field
163
+ const all = useFormWatch<Values>(); // every value
164
+ ```
165
+
166
+ Returns `unknown` (the same contract as the render-prop `value`) — narrow at the call site. It
167
+ works on BOTH paths: react-hook-form's `useWatch`, or the adapter's `getValue`/`getValues`.
52
168
 
53
169
  ## Forbidden
54
170
 
@@ -108,7 +224,9 @@ const form = useForm({ customer_nm: "", action_mode: "regist" });
108
224
  - Never hand-filter the bag per page; that except-list is exactly what this mechanism removes.
109
225
  - `FormFieldControl` forwards its `name`, so `FormRoot` + adapter fields claim their keys too;
110
226
  give `<FormErrors errors={form.errors} />` the bag explicitly when there is no surrounding
111
- `Form errors`.
227
+ `Form errors` — or pass the bag straight to `FormRoot errors`, which provides the same registry.
228
+ - An adapter may implement the optional `reset()`, which `FormRoot` calls for a native
229
+ `<Button type="reset">`, so the reset button behaves identically on both paths.
112
230
 
113
231
  ### Sibling Forms — one bag over several Card+Form sections
114
232
 
@@ -141,3 +259,30 @@ import { Form, FormErrors, FormErrorsProvider, FormField, Input } from "@godxjp/
141
259
 
142
260
  A nested Form **with** its own `errors` starts a new registry that shadows the provider — its
143
261
  claims and messages stay inside it (use this for an embedded sub-form with a separate bag).
262
+
263
+ ### Multipart uploads and Inertia visits
264
+
265
+ For a form that submits files together, use `<Upload name="attachments[]" pastable />`.
266
+ The uploader appends staged local files during the native `formdata` event, including
267
+ files selected by dropping or pasting. Completed media uploads remain the responsibility
268
+ of `collectUploadCommitActions`.
269
+
270
+ For an endpoint that redirects back after each uploaded file, connect the reusable
271
+ Inertia bridge. Errors retain the local file for retry; removing or cancelling a file
272
+ cancels its visit. Each request runs asynchronously so selecting another file does not
273
+ silently cancel an unrelated upload.
274
+
275
+ ```tsx
276
+ import { inertiaUpload } from "@godxjp/ui/inertia";
277
+ import { Upload } from "@godxjp/ui/data-entry";
278
+
279
+ <Upload
280
+ value={items}
281
+ onValueChange={(next) => setItems(next.filter((item) => item.status !== "done"))}
282
+ onUpload={inertiaUpload(
283
+ (file, callbacks) => router.post(uploadUrl, { file }, { ...callbacks, preserveScroll: true }),
284
+ t("upload.failed"),
285
+ )}
286
+ pastable
287
+ />;
288
+ ```
@@ -3,16 +3,28 @@
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: **131**
7
- - With ≥1 frame: **131** · zero-frame: **0**
6
+ - Public components: **137**
7
+ - With ≥1 frame: **133** · zero-frame: **4**
8
8
  - Fully covered (every contract axis declared): **0**
9
9
 
10
+ ## UNTESTED — no `/frame/**` route
11
+
12
+ - [ ] `inertiaUpload`
13
+ - [ ] `CardBar`
14
+ - [ ] `VisuallyHidden`
15
+ - [ ] `RangeTimeline`
16
+
10
17
  ## Per-component contract axes
11
18
 
12
19
  Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
13
20
 
14
21
  | Component | Frame | visua | state | owner | conte | respo | inter | acces | prefe | async |
15
22
  | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | inertiaUpload | **✗** | · | · | · | · | · | · | · | · | · |
24
+ | CardBar | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
25
+ | TimeRangePicker | ✓ | · | · | · | · | · | · | · | · | · |
26
+ | VisuallyHidden | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
27
+ | RangeTimeline | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
16
28
  | PageContainer | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
17
29
  | Flex | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
18
30
  | ResponsiveGrid | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
@@ -55,21 +67,21 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
55
67
  | Table | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
56
68
  | DataState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
57
69
  | InfiniteQueryState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
58
- | Form | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
70
+ | Form | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
59
71
  | FormField | ✓ | N/A | · | N/A | · | · | · | · | · | · |
60
72
  | FormErrors | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
61
73
  | Input | ✓ | ✓ | · | · | · | · | · | · | · | · |
62
74
  | NumberInput | ✓ | ✓ | · | · | · | · | · | · | · | · |
63
- | SearchInput | ✓ | N/A | · | · | · | · | · | · | · | · |
75
+ | SearchInput | ✓ | | · | · | · | · | · | · | · | · |
64
76
  | Select | ✓ | N/A | · | · | · | · | · | · | · | · |
65
77
  | Switch | ✓ | ✓ | · | · | · | · | · | · | · | · |
66
78
  | Textarea | ✓ | ✓ | · | · | · | · | · | · | · | · |
67
79
  | Label | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
68
80
  | Checkbox | ✓ | N/A | · | · | · | · | · | · | · | · |
69
81
  | RadioGroup | ✓ | N/A | · | · | · | · | · | · | · | · |
70
- | MonthPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
71
- | MonthRangePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
72
- | DatePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
82
+ | MonthPicker | ✓ | · | · | · | · | · | · | · | · | · |
83
+ | MonthRangePicker | ✓ | · | · | · | · | · | · | · | · | · |
84
+ | DatePicker | ✓ | · | · | · | · | · | · | · | · | · |
73
85
  | Dialog | ✓ | N/A | N/A | · | · | · | · | · | · | · |
74
86
  | AlertDialog | ✓ | ✓ | · | · | · | · | · | · | · | · |
75
87
  | AlertDialogRoot | ✓ | N/A | N/A | · | · | · | · | · | · | · |
@@ -79,16 +91,16 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
79
91
  | SkeletonTable | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
80
92
  | Toaster | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
81
93
  | Tabs | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
82
- | Pagination | ✓ | N/A | · | · | · | · | · | · | · | · |
94
+ | Pagination | ✓ | · | · | · | · | · | · | · | · | · |
83
95
  | DropdownMenu | ✓ | N/A | N/A | · | · | · | · | · | · | · |
84
96
  | Steps | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
85
97
  | Toolbar | ✓ | N/A | · | N/A | · | · | · | · | · | · |
86
98
  | AppProvider | ✓ | · | · | · | · | · | · | · | · | · |
87
99
  | formatDate | ✓ | · | · | · | · | · | · | · | · | · |
88
- | TimePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
89
- | DateRangePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
90
- | Cascader | ✓ | N/A | · | · | · | · | · | · | · | · |
91
- | TreeSelect | ✓ | N/A | · | · | · | · | · | · | · | · |
100
+ | TimePicker | ✓ | · | · | · | · | · | · | · | · | · |
101
+ | DateRangePicker | ✓ | · | · | · | · | · | · | · | · | · |
102
+ | Cascader | ✓ | | · | · | · | · | · | · | · | · |
103
+ | TreeSelect | ✓ | | · | · | · | · | · | · | · | · |
92
104
  | Transfer | ✓ | N/A | · | · | · | · | · | · | · | · |
93
105
  | Upload | ✓ | ✓ | · | · | · | · | · | · | · | · |
94
106
  | UploadCropDialog | ✓ | · | · | · | · | · | · | · | · | · |
@@ -112,12 +124,12 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
112
124
  | AspectRatio | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
113
125
  | Accordion | ✓ | N/A | · | · | · | · | · | · | · | · |
114
126
  | HoverCard | ✓ | N/A | N/A | · | · | · | · | · | · | · |
115
- | PasswordInput | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
127
+ | PasswordInput | ✓ | ✓ | · | · | · | · | · | · | · | · |
116
128
  | PasswordStrength | ✓ | N/A | N/A | · | · | · | · | · | · | · |
117
- | InputOTP | ✓ | N/A | · | N/A | · | · | · | · | · | · |
129
+ | InputOTP | ✓ | · | · | · | · | · | · | · | · | · |
118
130
  | Rating | ✓ | N/A | · | · | · | · | · | · | · | · |
119
- | Segmented | ✓ | N/A | · | · | · | · | · | · | · | · |
120
- | TagInput | ✓ | N/A | · | · | · | · | · | · | · | · |
131
+ | Segmented | ✓ | | · | · | · | · | · | · | · | · |
132
+ | TagInput | ✓ | | · | · | · | · | · | · | · | · |
121
133
  | ContextMenu | ✓ | N/A | N/A | · | · | · | · | · | · | · |
122
134
  | Menubar | ✓ | N/A | N/A | · | · | · | · | · | · | · |
123
135
  | NavigationMenu | ✓ | N/A | N/A | · | · | · | · | · | · | · |
@@ -140,6 +152,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
140
152
  | AuthAccountSummary | ✓ | N/A | · | N/A | · | · | · | · | · | · |
141
153
  | AuthStack | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
142
154
  | OrgSwitcher | ✓ | N/A | · | · | · | · | · | · | · | · |
155
+ | AppLauncher | ✓ | N/A | · | · | · | · | · | · | · | · |
143
156
  | FilterBar | ✓ | N/A | · | N/A | · | · | · | · | · | · |
144
157
  | PermissionMatrix | ✓ | N/A | · | N/A | · | · | · | · | · | · |
145
158
  | BranchScopePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
package/docs/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  ```bash
21
21
  cd packages/ui
22
22
  pnpm preview # http://localhost:6008
23
- pnpm preview:build # static site → preview/dist/
23
+ pnpm preview:build # static site → preview/dist/ — heavy; CI's job, run at most once pre-PR
24
24
  ```
25
25
 
26
26
  ### Preview doc format
@@ -1,6 +1,6 @@
1
1
  # Prop Vocabulary + Design Token Standards
2
2
 
3
- These rules are normative for @godxjp/ui 8.0.0. They mirror `debate/props-vocab-token-consistency/04-Decision.md`.
3
+ These rules are normative for @godxjp/ui. They came out of the props-vocabulary / token-consistency debate, whose record lived in `debate/` until that directory was removed — this file is now the normative source, not a mirror of one.
4
4
 
5
5
  1. Prop vocabulary: every exported `*Prop` type in `src/props/components/` MUST have exactly one `COMPONENT_PROP_REGISTRY` entry.
6
6
  2. Prop vocabulary: every public property in an exported component prop type MUST map to one `VOCABULARY_REGISTRY` entry or to a `local: true` registry record with a non-empty `reason`.
package/docs/TESTING.md CHANGED
@@ -5,10 +5,16 @@ Every component **must** ship with a Vitest suite before merge.
5
5
  ## Run
6
6
 
7
7
  ```bash
8
- cd packages/ui
9
- pnpm test # single run
10
- pnpm test:watch # watch mode
11
- pnpm test:coverage # v8 coverage report
8
+ # While working — ONLY the tests for what you touched. Tests sit beside the
9
+ # component in `src/components/<group>/__tests__/`, so scope to that folder.
10
+ pnpm vitest run src/components/<group>/__tests__ --maxWorkers=2
11
+ pnpm vitest watch src/components/<group>/__tests__ # watch one component
12
+
13
+ # CI only — 506 files / 3700+ tests. Do NOT run these from an agent loop or with
14
+ # other agents on the machine: measured 70 vitest workers at load 90, an overheating
15
+ # laptop and a burnt monthly API budget.
16
+ pnpm test # single run — CI
17
+ pnpm test:coverage # v8 coverage report — CI
12
18
  ```
13
19
 
14
20
  ## File location
@@ -68,7 +74,7 @@ import "@testing-library/jest-dom/vitest";
68
74
  | `react-hooks/set-state-in-effect` | sync setState in effects |
69
75
  | `RenderLoopGuard` in tests | runaway re-renders during Vitest |
70
76
 
71
- Run `pnpm lint` before `pnpm test` — official React Compiler rules align with test guard.
77
+ Run `pnpm lint` before your scoped test run — official React Compiler rules align with test guard.
72
78
 
73
79
  ## Forms testing
74
80
 
@@ -83,7 +89,10 @@ Never test forms with raw `useState` — mirror production `FormRoot` + `FormFie
83
89
 
84
90
  ## CI gate
85
91
 
86
- `pnpm test` in `packages/ui` must pass with zero failures before any UI PR merges.
92
+ The full suite must pass with zero failures before any UI PR merges — **and that run belongs
93
+ to CI, not to you.** Locally you run only the tests for the files you changed; CI runs
94
+ everything on the PR. `tal --help` states the same rule for the agent loop: *FULL SUITE KHÔNG
95
+ THUỘC VỀ VÒNG LẶP.*
87
96
 
88
97
  ## When tests fail
89
98
 
@@ -0,0 +1,81 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
2
+ import { Flex, PageContainer } from "@godxjp/ui/layout";
3
+ import { BarChart } from "@godxjp/ui/charts";
4
+
5
+ /**
6
+ * 横棒グラフの分類軸と、カードの見出しとの重複 — gh#409 · 1 と 2。
7
+ *
8
+ * 監理団体の実習実施者(受入企業)名は「株式会社」を含む 8〜10 文字の全角で、幅は 100〜125px に
9
+ * なります。分類軸の幅を固定にすると、名前の**先頭**、つまり識別に必要な部分から切り落とされます。
10
+ * BarChart は実際に描画される字形で各目盛りを実測し、いちばん広い目盛りに合わせて軸を確保します。
11
+ * 上限はトークン `--chart-category-axis-max-fraction` が持ち、これを超える名前だけが**末尾**で
12
+ * 省略され、全文は目盛りの `<title>` と図の代替テキストに残ります。
13
+ *
14
+ * 下段のカードは `showCaption={false}`。`CardTitle` が見出しを担うので、キャプションは
15
+ * `sr-only` として DOM に残り、`role="img"` の名前は失われません。
16
+ */
17
+
18
+ /** 実習実施者ごとの受入人数。企業名は 8〜10 文字の全角で、固定幅の軸では先頭が消えていました。 */
19
+ const acceptances = [
20
+ { company: "株式会社山田製作所", trainees: 42 },
21
+ { company: "佐藤食品株式会社", trainees: 35 },
22
+ { company: "株式会社中村建設", trainees: 28 },
23
+ { company: "東海精密工業株式会社", trainees: 24 },
24
+ { company: "みどり農産株式会社", trainees: 18 },
25
+ { company: "大和金属加工株式会社", trainees: 12 },
26
+ ];
27
+
28
+ export default function Demo() {
29
+ return (
30
+ <PageContainer
31
+ title="CJK category axis"
32
+ subtitle="横棒グラフの分類軸を実測で確保し、見出しの重複をなくす"
33
+ >
34
+ <Flex direction="col" gap="lg">
35
+ <Card>
36
+ <CardHeader>
37
+ <CardTitle level={2}>横棒 BarChart · 分類軸は目盛りの実測で決まる</CardTitle>
38
+ <CardDescription>
39
+ いちばん広い目盛りに合わせて軸幅を確保します。キャプションは既定どおり表示。
40
+ </CardDescription>
41
+ </CardHeader>
42
+ <CardContent>
43
+ <BarChart
44
+ id="cjk-axis-chart"
45
+ label="実習実施者別 受入人数"
46
+ data={acceptances}
47
+ categoryKey="company"
48
+ series={[{ dataKey: "trainees", label: "受入人数" }]}
49
+ showLegend={false}
50
+ size="md"
51
+ horizontal
52
+ />
53
+ </CardContent>
54
+ </Card>
55
+
56
+ <Card>
57
+ <CardHeader>
58
+ <CardTitle level={2}>実習実施者別 受入人数</CardTitle>
59
+ <CardDescription>
60
+ `showCaption={"{false}"}` — 見出しは CardTitle が担い、キャプションは sr-only
61
+ として残るのでアクセシブルな名前は保たれます。
62
+ </CardDescription>
63
+ </CardHeader>
64
+ <CardContent>
65
+ <BarChart
66
+ id="cjk-axis-chart-no-caption"
67
+ label="実習実施者別 受入人数"
68
+ data={acceptances}
69
+ categoryKey="company"
70
+ series={[{ dataKey: "trainees", label: "受入人数" }]}
71
+ showLegend={false}
72
+ showCaption={false}
73
+ size="md"
74
+ horizontal
75
+ />
76
+ </CardContent>
77
+ </Card>
78
+ </Flex>
79
+ </PageContainer>
80
+ );
81
+ }
@@ -281,6 +281,28 @@ export default function Demo() {
281
281
  </Card>
282
282
  </ResponsiveGrid>
283
283
  </Flex>
284
+
285
+ <Card>
286
+ <CardHeader>
287
+ <CardTitle level={2}>連続したツールバー</CardTitle>
288
+ </CardHeader>
289
+ <CardContent>
290
+ <Text>本文</Text>
291
+ </CardContent>
292
+ <CardBar pad={{ block: 1, inline: 3 }} surface="muted" border="block-start" gap="sm">
293
+ <Button variant="ghost" size="sm">
294
+ 添付
295
+ </Button>
296
+ </CardBar>
297
+ <CardBar
298
+ pad={{ block: 2, inline: 3 }}
299
+ border="block-start"
300
+ gap="sm"
301
+ extra={<Button>送信</Button>}
302
+ >
303
+ <Text tone="muted">下書きを保存しました</Text>
304
+ </CardBar>
305
+ </Card>
284
306
  </Flex>
285
307
  </PageContainer>
286
308
  );