@godxjp/ui 19.6.0 → 20.1.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 (326) hide show
  1. package/README.md +8 -2
  2. package/dist/app/theme-axes.d.ts +2 -0
  3. package/dist/app/theme-axes.js +45 -0
  4. package/dist/components/charts/chart-cartesian.d.ts +2 -1
  5. package/dist/components/charts/chart-cartesian.js +52 -3
  6. package/dist/components/charts/chart-category-axis.d.ts +55 -0
  7. package/dist/components/charts/chart-category-axis.js +93 -0
  8. package/dist/components/charts/chart-frame.d.ts +10 -1
  9. package/dist/components/charts/chart-frame.js +19 -3
  10. package/dist/components/charts/compact-bar-trend.d.ts +1 -1
  11. package/dist/components/charts/compact-bar-trend.js +2 -0
  12. package/dist/components/charts/pie-chart.d.ts +1 -1
  13. package/dist/components/charts/pie-chart.js +12 -1
  14. package/dist/components/charts/recharts-peer.d.ts +49 -0
  15. package/dist/components/charts/recharts-peer.js +45 -0
  16. package/dist/components/data-display/card.d.ts +14 -3
  17. package/dist/components/data-display/card.js +22 -4
  18. package/dist/components/data-display/code-block.js +6 -2
  19. package/dist/components/data-display/collapsible.d.ts +23 -4
  20. package/dist/components/data-display/collapsible.js +119 -4
  21. package/dist/components/data-display/data-table.d.ts +39 -4
  22. package/dist/components/data-display/data-table.js +802 -270
  23. package/dist/components/data-display/descriptions.d.ts +27 -6
  24. package/dist/components/data-display/descriptions.js +58 -17
  25. package/dist/components/data-display/index.d.ts +5 -1
  26. package/dist/components/data-display/index.js +4 -0
  27. package/dist/components/data-display/legend.d.ts +15 -0
  28. package/dist/components/data-display/legend.js +11 -0
  29. package/dist/components/data-display/list-row.d.ts +10 -1
  30. package/dist/components/data-display/list-row.js +8 -2
  31. package/dist/components/data-display/popover.d.ts +60 -7
  32. package/dist/components/data-display/popover.js +268 -39
  33. package/dist/components/data-display/progress.d.ts +50 -2
  34. package/dist/components/data-display/progress.js +57 -9
  35. package/dist/components/data-display/range-timeline.d.ts +38 -0
  36. package/dist/components/data-display/range-timeline.js +161 -0
  37. package/dist/components/data-display/scroll-area.js +13 -2
  38. package/dist/components/data-display/service-launcher-card.js +10 -10
  39. package/dist/components/data-display/table.d.ts +11 -3
  40. package/dist/components/data-display/table.js +48 -18
  41. package/dist/components/data-display/tree-list.js +4 -9
  42. package/dist/components/data-entry/calendar.d.ts +2 -2
  43. package/dist/components/data-entry/calendar.js +79 -33
  44. package/dist/components/data-entry/cascader.d.ts +1 -1
  45. package/dist/components/data-entry/cascader.js +188 -24
  46. package/dist/components/data-entry/checkbox.d.ts +19 -6
  47. package/dist/components/data-entry/checkbox.js +55 -16
  48. package/dist/components/data-entry/choice-option.d.ts +1 -1
  49. package/dist/components/data-entry/color-picker.d.ts +1 -1
  50. package/dist/components/data-entry/color-picker.js +17 -5
  51. package/dist/components/data-entry/control-appearance.d.ts +64 -0
  52. package/dist/components/data-entry/control-appearance.js +39 -0
  53. package/dist/components/data-entry/control-surface.d.ts +61 -0
  54. package/dist/components/data-entry/control-surface.js +39 -0
  55. package/dist/components/data-entry/date-picker.d.ts +1 -1
  56. package/dist/components/data-entry/date-picker.js +344 -113
  57. package/dist/components/data-entry/date-range-picker.d.ts +2 -2
  58. package/dist/components/data-entry/date-range-picker.js +278 -140
  59. package/dist/components/data-entry/field.js +0 -1
  60. package/dist/components/data-entry/form-field.d.ts +1 -1
  61. package/dist/components/data-entry/form-field.js +39 -4
  62. package/dist/components/data-entry/form.d.ts +4 -0
  63. package/dist/components/data-entry/form.js +5 -3
  64. package/dist/components/data-entry/index.d.ts +7 -3
  65. package/dist/components/data-entry/index.js +10 -1
  66. package/dist/components/data-entry/input-otp.d.ts +1 -1
  67. package/dist/components/data-entry/input.d.ts +12 -34
  68. package/dist/components/data-entry/input.js +92 -24
  69. package/dist/components/data-entry/label.d.ts +3 -2
  70. package/dist/components/data-entry/label.js +23 -10
  71. package/dist/components/data-entry/month-picker.d.ts +2 -2
  72. package/dist/components/data-entry/month-picker.js +47 -10
  73. package/dist/components/data-entry/month-range-picker.d.ts +2 -2
  74. package/dist/components/data-entry/month-range-picker.js +51 -11
  75. package/dist/components/data-entry/number-input.d.ts +7 -0
  76. package/dist/components/data-entry/number-input.js +147 -98
  77. package/dist/components/data-entry/password-input.d.ts +1 -1
  78. package/dist/components/data-entry/radio.d.ts +1 -1
  79. package/dist/components/data-entry/radio.js +61 -13
  80. package/dist/components/data-entry/search-input.d.ts +1 -1
  81. package/dist/components/data-entry/search-input.js +16 -2
  82. package/dist/components/data-entry/search-select.d.ts +2 -2
  83. package/dist/components/data-entry/search-select.js +209 -78
  84. package/dist/components/data-entry/select.d.ts +22 -4
  85. package/dist/components/data-entry/select.js +215 -163
  86. package/dist/components/data-entry/slider.d.ts +9 -1
  87. package/dist/components/data-entry/slider.js +87 -9
  88. package/dist/components/data-entry/switch.d.ts +3 -0
  89. package/dist/components/data-entry/switch.js +29 -3
  90. package/dist/components/data-entry/textarea.d.ts +14 -56
  91. package/dist/components/data-entry/textarea.js +80 -33
  92. package/dist/components/data-entry/time-picker.d.ts +2 -2
  93. package/dist/components/data-entry/time-picker.js +333 -109
  94. package/dist/components/data-entry/time-range-picker.d.ts +5 -0
  95. package/dist/components/data-entry/time-range-picker.js +89 -0
  96. package/dist/components/data-entry/transfer.d.ts +1 -1
  97. package/dist/components/data-entry/transfer.js +99 -31
  98. package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
  99. package/dist/components/data-entry/tree-select.d.ts +1 -1
  100. package/dist/components/data-entry/tree-select.js +197 -92
  101. package/dist/components/data-entry/tree-utils.d.ts +1 -1
  102. package/dist/components/data-entry/tree-utils.js +7 -14
  103. package/dist/components/data-entry/upload-files.d.ts +2 -0
  104. package/dist/components/data-entry/upload-files.js +31 -0
  105. package/dist/components/data-entry/upload-request.d.ts +4 -0
  106. package/dist/components/data-entry/upload-request.js +53 -0
  107. package/dist/components/data-entry/upload-types.d.ts +28 -0
  108. package/dist/components/data-entry/upload-types.js +2 -0
  109. package/dist/components/data-entry/upload.d.ts +2 -2
  110. package/dist/components/data-entry/upload.js +482 -117
  111. package/dist/components/feedback/dialog.d.ts +105 -38
  112. package/dist/components/feedback/dialog.js +268 -193
  113. package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
  114. package/dist/components/feedback/overlay-close-focus.js +31 -0
  115. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  116. package/dist/components/feedback/sheet.d.ts +53 -11
  117. package/dist/components/feedback/sheet.js +150 -81
  118. package/dist/components/feedback/tooltip.d.ts +51 -7
  119. package/dist/components/feedback/tooltip.js +107 -25
  120. package/dist/components/general/button.d.ts +1 -1
  121. package/dist/components/general/button.js +7 -3
  122. package/dist/components/general/index.d.ts +1 -0
  123. package/dist/components/general/index.js +2 -0
  124. package/dist/components/general/logo.d.ts +17 -0
  125. package/dist/components/general/logo.js +22 -16
  126. package/dist/components/general/typography.d.ts +3 -0
  127. package/dist/components/general/typography.js +15 -1
  128. package/dist/components/general/visually-hidden.d.ts +3 -0
  129. package/dist/components/general/visually-hidden.js +9 -0
  130. package/dist/components/layout/app-launcher.d.ts +34 -0
  131. package/dist/components/layout/app-launcher.js +228 -0
  132. package/dist/components/layout/app-shell.d.ts +3 -1
  133. package/dist/components/layout/app-shell.js +69 -12
  134. package/dist/components/layout/aspect-ratio.js +0 -1
  135. package/dist/components/layout/auth-divider.js +0 -1
  136. package/dist/components/layout/breadcrumb.d.ts +14 -2
  137. package/dist/components/layout/breadcrumb.js +46 -4
  138. package/dist/components/layout/flex.d.ts +1 -1
  139. package/dist/components/layout/flex.js +41 -3
  140. package/dist/components/layout/index.d.ts +3 -0
  141. package/dist/components/layout/index.js +5 -1
  142. package/dist/components/layout/nav-surface.d.ts +26 -0
  143. package/dist/components/layout/nav-surface.js +17 -0
  144. package/dist/components/layout/org-switcher.d.ts +5 -1
  145. package/dist/components/layout/org-switcher.js +25 -4
  146. package/dist/components/layout/page-container.d.ts +1 -1
  147. package/dist/components/layout/page-container.js +5 -3
  148. package/dist/components/layout/responsive-grid.d.ts +16 -2
  149. package/dist/components/layout/responsive-grid.js +29 -2
  150. package/dist/components/layout/separator.js +0 -1
  151. package/dist/components/layout/sidebar.js +4 -1
  152. package/dist/components/layout/split-pane.d.ts +14 -1
  153. package/dist/components/layout/split-pane.js +26 -13
  154. package/dist/components/layout/topbar-item.d.ts +3 -0
  155. package/dist/components/layout/topbar-item.js +19 -5
  156. package/dist/components/layout/topbar.d.ts +1 -1
  157. package/dist/components/layout/topbar.js +27 -6
  158. package/dist/components/navigation/app-setting-picker.js +21 -2
  159. package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
  160. package/dist/components/navigation/app-setting-toggle.js +96 -0
  161. package/dist/components/navigation/dropdown-menu.d.ts +227 -18
  162. package/dist/components/navigation/dropdown-menu.js +402 -118
  163. package/dist/components/navigation/index.d.ts +2 -0
  164. package/dist/components/navigation/index.js +2 -0
  165. package/dist/components/navigation/pagination-utils.d.ts +2 -1
  166. package/dist/components/navigation/pagination.d.ts +2 -2
  167. package/dist/components/navigation/pagination.js +155 -83
  168. package/dist/components/navigation/steps.d.ts +2 -2
  169. package/dist/components/navigation/steps.js +29 -4
  170. package/dist/components/navigation/tabs.d.ts +29 -16
  171. package/dist/components/navigation/tabs.js +374 -144
  172. package/dist/components/ui/accordion.d.ts +50 -5
  173. package/dist/components/ui/accordion.js +239 -33
  174. package/dist/components/ui/aspect-ratio.d.ts +23 -2
  175. package/dist/components/ui/aspect-ratio.js +15 -13
  176. package/dist/components/ui/avatar.d.ts +29 -4
  177. package/dist/components/ui/avatar.js +111 -25
  178. package/dist/components/ui/hover-card.d.ts +42 -4
  179. package/dist/components/ui/hover-card.js +183 -27
  180. package/dist/components/ui/input-otp.d.ts +26 -28
  181. package/dist/components/ui/input-otp.js +50 -7
  182. package/dist/components/ui/label.js +0 -1
  183. package/dist/components/ui/password-input.d.ts +36 -2
  184. package/dist/components/ui/password-input.js +27 -7
  185. package/dist/components/ui/rating.d.ts +25 -0
  186. package/dist/components/ui/rating.js +67 -27
  187. package/dist/components/ui/segmented.d.ts +12 -3
  188. package/dist/components/ui/segmented.js +17 -2
  189. package/dist/components/ui/separator.d.ts +12 -2
  190. package/dist/components/ui/separator.js +30 -9
  191. package/dist/components/ui/tag-input.d.ts +45 -0
  192. package/dist/components/ui/tag-input.js +109 -27
  193. package/dist/components/ui/toggle-group.d.ts +50 -5
  194. package/dist/components/ui/toggle-group.js +79 -20
  195. package/dist/components/ui/toggle.d.ts +31 -5
  196. package/dist/components/ui/toggle.js +42 -3
  197. package/dist/form/form-context.d.ts +29 -0
  198. package/dist/form/form-context.js +44 -4
  199. package/dist/form/form-field-array.d.ts +22 -0
  200. package/dist/form/form-field-array.js +50 -0
  201. package/dist/form/form-field-control.d.ts +2 -1
  202. package/dist/form/form-field-control.js +116 -40
  203. package/dist/form/form-root.d.ts +2 -1
  204. package/dist/form/form-root.js +142 -23
  205. package/dist/form/index.d.ts +3 -1
  206. package/dist/form/index.js +12 -1
  207. package/dist/i18n/messages/en.json +64 -5
  208. package/dist/i18n/messages/ja.json +64 -5
  209. package/dist/i18n/messages/vi.json +64 -5
  210. package/dist/inertia/index.d.ts +20 -0
  211. package/dist/inertia/index.js +60 -1
  212. package/dist/lib/control-styles.d.ts +25 -3
  213. package/dist/lib/control-styles.js +9 -3
  214. package/dist/lib/datetime/picker-format.d.ts +8 -0
  215. package/dist/lib/datetime/picker-format.js +54 -0
  216. package/dist/lib/slot.d.ts +32 -0
  217. package/dist/lib/slot.js +22 -0
  218. package/dist/lib/variants.d.ts +22 -3
  219. package/dist/lib/variants.js +56 -1
  220. package/dist/props/components/app.prop.d.ts +25 -1
  221. package/dist/props/components/charts.prop.d.ts +21 -0
  222. package/dist/props/components/data-display.prop.d.ts +72 -4
  223. package/dist/props/components/data-entry.prop.d.ts +718 -46
  224. package/dist/props/components/form.prop.d.ts +142 -4
  225. package/dist/props/components/general.prop.d.ts +17 -1
  226. package/dist/props/components/index.d.ts +3 -3
  227. package/dist/props/components/layout.prop.d.ts +281 -11
  228. package/dist/props/components/navigation.prop.d.ts +114 -4
  229. package/dist/props/registry.d.ts +397 -11
  230. package/dist/props/registry.js +526 -11
  231. package/dist/props/vocabulary/content.prop.d.ts +1 -1
  232. package/dist/props/vocabulary/data.prop.d.ts +179 -1
  233. package/dist/props/vocabulary/index.d.ts +5 -5
  234. package/dist/props/vocabulary/interaction.prop.d.ts +68 -3
  235. package/dist/props/vocabulary/layout.prop.d.ts +57 -1
  236. package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
  237. package/dist/props/vocabulary/shared.prop.d.ts +33 -0
  238. package/dist/styles/badge-layout.css +2 -1
  239. package/dist/styles/card-layout.css +26 -4
  240. package/dist/styles/chart-layout.css +15 -0
  241. package/dist/styles/control.css +657 -15
  242. package/dist/styles/core.css +5 -2
  243. package/dist/styles/data-display-layout.css +313 -6
  244. package/dist/styles/data-entry-layout.css +13 -0
  245. package/dist/styles/focus-ring.css +4 -0
  246. package/dist/styles/index.css +5 -2
  247. package/dist/styles/layout.css +327 -4
  248. package/dist/styles/navigation-layout.css +227 -0
  249. package/dist/styles/shell-layout.css +369 -13
  250. package/dist/styles/table-layout.css +95 -6
  251. package/dist/styles/text-layout.css +10 -4
  252. package/dist/tokens/base.css +2 -1
  253. package/dist/tokens/components/badge.css +1 -0
  254. package/dist/tokens/components/chart.css +6 -0
  255. package/dist/tokens/components/control.css +79 -2
  256. package/dist/tokens/components/data-display.css +20 -0
  257. package/dist/tokens/components/descriptions.css +11 -0
  258. package/dist/tokens/components/flex.css +8 -0
  259. package/dist/tokens/components/navigation.css +55 -0
  260. package/dist/tokens/components/shell.css +53 -3
  261. package/dist/tokens/components/table.css +14 -0
  262. package/dist/tokens/foundation.css +2 -0
  263. package/dist/tokens/semantic/layout.css +9 -0
  264. package/docs/COMPONENTS.md +9 -3
  265. package/docs/CONSUMER-RULES.md +13 -0
  266. package/docs/DESIGN-AUTHORITY.md +145 -83
  267. package/docs/DEVELOPMENT.md +4 -3
  268. package/docs/FORMS.md +151 -6
  269. package/docs/FRAME-COVERAGE-REPORT.md +32 -17
  270. package/docs/README.md +15 -15
  271. package/docs/STANDARDS-vocabulary-tokens.md +1 -1
  272. package/docs/TESTING.md +15 -6
  273. package/docs/WHAT-BELONGS-HERE.md +179 -0
  274. package/docs/charts/cjk-category-axis.tsx +81 -0
  275. package/docs/data-display/card/index.tsx +22 -0
  276. package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
  277. package/docs/data-display/data-table/index.tsx +23 -0
  278. package/docs/data-display/descriptions.tsx +41 -0
  279. package/docs/data-display/legend.tsx +145 -0
  280. package/docs/data-display/progress.tsx +32 -0
  281. package/docs/data-display/scroll-area.tsx +31 -25
  282. package/docs/data-display/table.tsx +104 -45
  283. package/docs/data-display/timeline.tsx +41 -0
  284. package/docs/data-entry/date-picker.tsx +85 -0
  285. package/docs/data-entry/date-range-picker.tsx +26 -0
  286. package/docs/data-entry/form-dynamic-fields.tsx +199 -0
  287. package/docs/data-entry/form.tsx +74 -4
  288. package/docs/data-entry/input-otp.tsx +24 -0
  289. package/docs/data-entry/input.tsx +46 -1
  290. package/docs/data-entry/month-range-picker.tsx +1 -1
  291. package/docs/data-entry/segmented.tsx +1 -1
  292. package/docs/data-entry/select.tsx +29 -2
  293. package/docs/data-entry/switch.tsx +41 -0
  294. package/docs/data-entry/textarea.tsx +38 -0
  295. package/docs/data-entry/time-picker.tsx +85 -1
  296. package/docs/data-entry/time-range-picker.tsx +55 -0
  297. package/docs/data-entry/transfer.tsx +17 -0
  298. package/docs/data-entry/upload.tsx +56 -12
  299. package/docs/feedback/sheet.tsx +1 -1
  300. package/docs/feedback/tooltip.tsx +1 -1
  301. package/docs/general/activity.tsx +2 -2
  302. package/docs/general/button/index.tsx +14 -1
  303. package/docs/general/typography.tsx +14 -1
  304. package/docs/layout/app-launcher.tsx +151 -0
  305. package/docs/layout/app-shell-arrangements.tsx +225 -0
  306. package/docs/layout/app-shell.tsx +11 -0
  307. package/docs/layout/aspect-ratio.tsx +1 -1
  308. package/docs/layout/flex.tsx +50 -0
  309. package/docs/layout/responsive-grid.tsx +21 -1
  310. package/docs/layout/topbar.tsx +5 -9
  311. package/docs/navigation/app-setting-picker.tsx +37 -1
  312. package/docs/navigation/app-setting-toggle.tsx +111 -0
  313. package/docs/navigation/breadcrumb.tsx +48 -0
  314. package/docs/navigation/dropdown-menu.tsx +21 -0
  315. package/docs/navigation/pagination.tsx +52 -0
  316. package/docs/navigation/steps.tsx +37 -0
  317. package/docs/navigation/tabs.tsx +97 -1
  318. package/docs/query/button-refetch.tsx +1 -0
  319. package/package.json +28 -8
  320. package/scripts/_agent-setup.mjs +182 -3
  321. package/scripts/consumer-rule.md +98 -0
  322. package/scripts/guinea-pig-skill.md +322 -0
  323. package/scripts/init-guinea-pig.mjs +84 -0
  324. package/scripts/postinstall.mjs +13 -2
  325. package/scripts/ui-audit.mjs +351 -37
  326. /package/dist/tokens/{antd.generated.css → derived.css} +0 -0
@@ -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,131 @@ 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
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.
196
-
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.
200
-
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/`.
206
-
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`.
210
-
211
- **What is deliberately NOT taken from antd, and why — each a measurement, not a preference:**
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
+
227
+ ## Derived colour is AUTHORED, and MEASUREMENT is what makes it authoritative
228
+
229
+ **Twenty values, in `src/tokens/derived.css`:** `--primary-hover`, `--primary-active`,
230
+ `--primary-border`, `--destructive-hover`, `--destructive-active`, `--control-outline` and its
231
+ alpha, `--control-outline-error` and its alpha, and `--ring` — each in both themes. They are the
232
+ interaction states that hang off the five authored seeds per theme (SmartHR MAIN plus four 和色,
233
+ and the lifted dark ramp), which stay in `src/tokens/foundation.css`.
234
+
235
+ **These twenty were once generated.** A colour algorithm ran at build time, took the seeds and
236
+ emitted the derived map; the authority behind each value was "the algorithm said so". That
237
+ generator has been removed and its dependency with it. The values did not change — but the reason
238
+ to trust them had to.
239
+
240
+ **The authority is now the measurement, not the derivation.** Four suites read `derived.css`
241
+ directly and hold every value in it to a threshold this repo has already committed to:
242
+
243
+ | suite | what it holds |
244
+ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `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 |
246
+ | `src/tokens/__tests__/interactive-fill-contrast.test.ts` | an interactive fill must clear **4.5:1** against the label sitting on it |
247
+ | `src/tokens/__tests__/destructive-contrast.test.ts` | `--destructive-hover` / `--destructive-active` against the same bar |
248
+ | `src/lib/__tests__/theme-tokens-css.test.ts` | the tier is actually loaded, and complete in both themes |
249
+
250
+ The first three also pin each value as a literal, so an edit to `derived.css` alone turns CI red
251
+ rather than quietly retinting the library. **That is a stronger claim than the generator made, not
252
+ a weaker one.** An algorithm guarantees a value is _consistent_; this repo had to override it four
253
+ times (below) precisely because consistent is not the same as _accessible_. A threshold guarantees
254
+ the property actually being sold.
255
+
256
+ **To change a derived value:** change it, run `pnpm test`, and if a threshold breaks the value is
257
+ wrong. Do not relax the threshold.
258
+
259
+ **The four overrides that existed even while a generator did** — the clearest evidence the
260
+ algorithm was never the real authority. Stepping an interactive fill towards the label sitting on
261
+ it lands under 4.5:1, so those four states take the same ramp at the same step size in the
262
+ opposite direction:
263
+
264
+ | token | conventional step | shipped |
265
+ | ------------------------ | ----------------- | ----------------- |
266
+ | light `--primary-hover` | #208bc9 · 3.69:1 | #005596 · 7.53:1 |
267
+ | light `--primary-active` | #005596 · 7.53:1 | #003c70 · 10.97:1 |
268
+ | dark `--primary-hover` | #61b6e8 · 7.92:1 | #61b6e8 · 7.92:1 |
269
+ | dark `--primary-active` | #2f76a6 · 3.60:1 | #8bd0f3 · 10.50:1 |
270
+
271
+ **What was deliberately never derived, and why — each a measurement, not a preference:**
212
272
 
213
273
  - **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
274
+ not derived from the brand seed. The one derived role on offer for a control boundary measures
275
+ **1.43:1** on the page, where `--input` is held to 3:1 by SC 1.4.11 and by
276
+ `input-boundary-contrast.test.ts`.
277
+ - **The text ramp.** The derived one is alpha-based (`rgba(0,0,0,0.88)`), which cannot enter this
278
+ library's opaque `H S% L%` triple without choosing a surface to composite against — lossy by
279
+ construction.
280
+ - **The dark `--primary` itself.** A mechanical dark derivation MOVES the seed. Deriving from this
281
+ library's light seed gives `#0363a4` at **2.81:1** on the dark spine, which
282
+ `primary-text-contrast.test.ts` rejects outright; deriving from the committed dark seed gives
283
+ `#3794d3` at 5.36:1, where the seed itself measures 7.07:1. The dark theme therefore keeps its
284
+ own seed — recorded, with the measurement, in `focus-ring-contrast.test.ts`.
285
+
286
+ **The focus geometry was already on the named scale.** Border 1, halo width 2, heavy outline 3,
287
+ radius 6, control height 32 and font size 14 all match scales this library already ships
288
+ (`--stroke-*`, `--radius`, `--band-height-md`, `--font-size-base`), so the focus tokens bind to
289
+ those steps instead of restating a second copy of the number.
290
+
291
+ **antd itself is gone from this repository**, and `pnpm check:no-antd-runtime` is what keeps it
292
+ gone: it fails if antd or any `@ant-design/*` package appears in any manifest field, is imported
293
+ anywhere in `src/`, or leaves a trace in `dist/`. Deleting that gate re-opens the door it closes.
294
+
295
+ ## Focus appearance — a two-form convention, and the indicator SHIPS OFF
296
+
297
+ ### The two forms
235
298
 
236
299
  **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:
238
-
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`.
245
-
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`.
248
-
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.
300
+ spacer. This library takes a different, two-form convention instead — widely used in enterprise
301
+ component libraries, and surveyed across several before it was adopted here:
302
+
303
+ - **Field** the boundary RECOLOURS to the primary at its unchanged hairline width, plus
304
+ `box-shadow: 0 0 0 var(--control-outline-width) var(--control-outline)`. A Select emits the same
305
+ declarations as an Input, which is why the two focus identically.
306
+ - **Non-field** — an outline outside the box model:
307
+ `outline: var(--focus-outline-weight) solid var(--primary-border); outline-offset: 1px`.
308
+
309
+ **Why this shape wins.** The library previously shipped an opaque brand ring drawn immediately
310
+ outside an untouched grey border two outlines of different colours claiming the same edge — and a
311
+ Select that could not be told to agree with an Input. Recolouring the existing boundary rather than
312
+ adding a second one resolves both. SmartHR still owns the hue: the focus colour is `--ring`, which
313
+ the derived tier declares as `var(--primary)`, because a focused field takes the primary rather than
314
+ a focus colour of its own.
254
315
 
255
316
  ### The indicator ships OFF. That forfeits WCAG 2.4.7 and a JIS X 8341-3 AA claim.
256
317
 
@@ -269,18 +330,18 @@ owner chose to ship it off and let whoever needs it turn it on. What that costs,
269
330
  **Turning it back on is one attribute, on the root element, with no code change:**
270
331
 
271
332
  ```html
272
- <html data-focus-outline="on">
333
+ <html data-focus-outline="on"></html>
273
334
  ```
274
335
 
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`:
336
+ **The switch is a single multiplier, not a scatter of overrides.** `--focus-outline` is one flag,
337
+ and every painted focus length multiplies by it:
277
338
 
278
- ```js
279
- lineWidthFocus: mergedToken.focusOutline === false ? 0 : mergedToken.lineWidth * 3
339
+ ```css
340
+ --focus-ring-width: calc(var(--focus-ring-weight) * var(--focus-outline));
280
341
  ```
281
342
 
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
343
+ Setting it to `0` zeroes every focus length at once, so no component rebind can bring the mark back
344
+ while it is off which is the property a scatter of per-component overrides could never give. The two paints
284
345
  that are not lengths — the halo and a field's recoloured boundary — are scoped to the same attribute
285
346
  in `styles/focus-ring.css`. **Every `:focus-visible` selector stays exactly where it is**; only the
286
347
  painted result disappears.
@@ -288,18 +349,18 @@ painted result disappears.
288
349
  ### The ON state is the LIGHT one
289
350
 
290
351
  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
352
+ the **field** indicator on every control — one hairline (1px) in the focus hue, plus the
353
+ `--control-outline` halo — rather than the heavy 3px outline form. Measured, in Chromium, on
293
354
  `ql.test` after the transition settles:
294
355
 
295
- | control | switch off | switch on |
296
- | --- | --- | --- |
297
- | 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` |
298
- | Button (primary) | outline `0px`, resting shadow intact | `outline: 1px solid rgb(0,113,189)` @ `0px` + same halo |
299
- | 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) |
300
361
 
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,
362
+ The field pair is the recoloured boundary plus the halo, exactly as the convention specifies for
363
+ this seed. The nav row insets its mark into its own shape rather than wrapping an already-shaded surface,
303
364
  which is the specific stacking that read as heavy.
304
365
 
305
366
  **No control's box moves when it is focused**, measured with `getBoundingClientRect()` before and
@@ -308,11 +369,12 @@ Input, Save and Delete all stay at 32.00px.
308
369
 
309
370
  **Two criteria, and only one is met by thickness alone.** SC 1.4.11 (AA, non-text contrast) is about
310
371
  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.
372
+ the light weight costs nothing there. `--primary-border` (#6dc0e3) measures **2.00:1 light /
373
+ 1.66:1 dark** and could not have satisfied it at any thickness, which is why the on-state takes the
374
+ focus hue instead — the single place it departs from the outline form. SC 2.4.13 Focus Appearance
375
+ (AAA) additionally wants a 2px perimeter; the on-state does not target it, and
376
+ `--focus-outline-weight: var(--stroke-lg)` restores the 3px weight if a customer needs the area
377
+ clause.
316
378
 
317
379
  Both positions of the switch are gated in `src/tokens/__tests__/focus-ring-contrast.test.ts`: with
318
380
  it off nothing paints and no rebind can route around it, with it on the geometry and the ≥3:1
@@ -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: **129**
7
- - With ≥1 frame: **129** · 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 | · | · | · | · | · | · |
@@ -41,6 +53,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
41
53
  | StatCard | ✓ | N/A | N/A | · | · | · | · | · | · | · |
42
54
  | ServiceLauncherCard | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
43
55
  | Badge | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
56
+ | Legend | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
44
57
  | ListRow | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
45
58
  | CredentialReveal | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
46
59
  | QrCode | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
@@ -54,21 +67,21 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
54
67
  | Table | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
55
68
  | DataState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
56
69
  | InfiniteQueryState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
57
- | Form | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
70
+ | Form | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
58
71
  | FormField | ✓ | N/A | · | N/A | · | · | · | · | · | · |
59
72
  | FormErrors | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
60
73
  | Input | ✓ | ✓ | · | · | · | · | · | · | · | · |
61
74
  | NumberInput | ✓ | ✓ | · | · | · | · | · | · | · | · |
62
- | SearchInput | ✓ | N/A | · | · | · | · | · | · | · | · |
75
+ | SearchInput | ✓ | | · | · | · | · | · | · | · | · |
63
76
  | Select | ✓ | N/A | · | · | · | · | · | · | · | · |
64
77
  | Switch | ✓ | ✓ | · | · | · | · | · | · | · | · |
65
78
  | Textarea | ✓ | ✓ | · | · | · | · | · | · | · | · |
66
79
  | Label | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
67
80
  | Checkbox | ✓ | N/A | · | · | · | · | · | · | · | · |
68
81
  | RadioGroup | ✓ | N/A | · | · | · | · | · | · | · | · |
69
- | MonthPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
70
- | MonthRangePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
71
- | DatePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
82
+ | MonthPicker | ✓ | · | · | · | · | · | · | · | · | · |
83
+ | MonthRangePicker | ✓ | · | · | · | · | · | · | · | · | · |
84
+ | DatePicker | ✓ | · | · | · | · | · | · | · | · | · |
72
85
  | Dialog | ✓ | N/A | N/A | · | · | · | · | · | · | · |
73
86
  | AlertDialog | ✓ | ✓ | · | · | · | · | · | · | · | · |
74
87
  | AlertDialogRoot | ✓ | N/A | N/A | · | · | · | · | · | · | · |
@@ -78,16 +91,16 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
78
91
  | SkeletonTable | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
79
92
  | Toaster | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
80
93
  | Tabs | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
81
- | Pagination | ✓ | N/A | · | · | · | · | · | · | · | · |
94
+ | Pagination | ✓ | · | · | · | · | · | · | · | · | · |
82
95
  | DropdownMenu | ✓ | N/A | N/A | · | · | · | · | · | · | · |
83
96
  | Steps | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
84
97
  | Toolbar | ✓ | N/A | · | N/A | · | · | · | · | · | · |
85
98
  | AppProvider | ✓ | · | · | · | · | · | · | · | · | · |
86
99
  | formatDate | ✓ | · | · | · | · | · | · | · | · | · |
87
- | TimePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
88
- | DateRangePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
89
- | Cascader | ✓ | N/A | · | · | · | · | · | · | · | · |
90
- | TreeSelect | ✓ | N/A | · | · | · | · | · | · | · | · |
100
+ | TimePicker | ✓ | · | · | · | · | · | · | · | · | · |
101
+ | DateRangePicker | ✓ | · | · | · | · | · | · | · | · | · |
102
+ | Cascader | ✓ | | · | · | · | · | · | · | · | · |
103
+ | TreeSelect | ✓ | | · | · | · | · | · | · | · | · |
91
104
  | Transfer | ✓ | N/A | · | · | · | · | · | · | · | · |
92
105
  | Upload | ✓ | ✓ | · | · | · | · | · | · | · | · |
93
106
  | UploadCropDialog | ✓ | · | · | · | · | · | · | · | · | · |
@@ -106,23 +119,24 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
106
119
  | Avatar | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
107
120
  | Separator | ✓ | · | N/A | N/A | · | · | · | · | · | · |
108
121
  | Skeleton | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
109
- | Toggle | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
122
+ | Toggle | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
110
123
  | ToggleGroup | ✓ | ✓ | · | · | · | · | · | · | · | · |
111
124
  | AspectRatio | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
112
125
  | Accordion | ✓ | N/A | · | · | · | · | · | · | · | · |
113
126
  | HoverCard | ✓ | N/A | N/A | · | · | · | · | · | · | · |
114
- | PasswordInput | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
127
+ | PasswordInput | ✓ | ✓ | · | · | · | · | · | · | · | · |
115
128
  | PasswordStrength | ✓ | N/A | N/A | · | · | · | · | · | · | · |
116
- | InputOTP | ✓ | N/A | · | N/A | · | · | · | · | · | · |
129
+ | InputOTP | ✓ | · | · | · | · | · | · | · | · | · |
117
130
  | Rating | ✓ | N/A | · | · | · | · | · | · | · | · |
118
- | Segmented | ✓ | N/A | · | · | · | · | · | · | · | · |
119
- | TagInput | ✓ | N/A | · | · | · | · | · | · | · | · |
131
+ | Segmented | ✓ | | · | · | · | · | · | · | · | · |
132
+ | TagInput | ✓ | | · | · | · | · | · | · | · | · |
120
133
  | ContextMenu | ✓ | N/A | N/A | · | · | · | · | · | · | · |
121
134
  | Menubar | ✓ | N/A | N/A | · | · | · | · | · | · | · |
122
135
  | NavigationMenu | ✓ | N/A | N/A | · | · | · | · | · | · | · |
123
136
  | ResizablePanel | ✓ | N/A | · | N/A | · | · | · | · | · | · |
124
137
  | Carousel | ✓ | N/A | N/A | · | · | · | · | · | · | · |
125
138
  | AppSettingPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
139
+ | AppSettingToggle | ✓ | N/A | · | · | · | · | · | · | · | · |
126
140
  | Field | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
127
141
  | LineChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
128
142
  | BarChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
@@ -138,6 +152,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
138
152
  | AuthAccountSummary | ✓ | N/A | · | N/A | · | · | · | · | · | · |
139
153
  | AuthStack | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
140
154
  | OrgSwitcher | ✓ | N/A | · | · | · | · | · | · | · | · |
155
+ | AppLauncher | ✓ | N/A | · | · | · | · | · | · | · | · |
141
156
  | FilterBar | ✓ | N/A | · | N/A | · | · | · | · | · | · |
142
157
  | PermissionMatrix | ✓ | N/A | · | N/A | · | · | · | · | · | · |
143
158
  | BranchScopePicker | ✓ | N/A | · | · | · | · | · | · | · | · |