@godxjp/ui 19.1.0 → 19.3.1

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 (437) hide show
  1. package/README.md +25 -128
  2. package/dist/app/theme-axes.d.ts +4 -15
  3. package/dist/app/timezones.d.ts +1 -4
  4. package/dist/components/charts/area-chart.d.ts +2 -3
  5. package/dist/components/charts/bar-chart.d.ts +2 -3
  6. package/dist/components/charts/chart-frame.d.ts +7 -16
  7. package/dist/components/charts/chart-summary.d.ts +2 -2
  8. package/dist/components/charts/compact-bar-trend.d.ts +9 -24
  9. package/dist/components/charts/index.d.ts +1 -6
  10. package/dist/components/data-display/badge.d.ts +20 -14
  11. package/dist/components/data-display/badge.js +18 -4
  12. package/dist/components/data-display/card.d.ts +21 -41
  13. package/dist/components/data-display/carousel.d.ts +1 -2
  14. package/dist/components/data-display/code-block.d.ts +4 -0
  15. package/dist/components/data-display/code-block.js +24 -0
  16. package/dist/components/data-display/data-table.d.ts +15 -37
  17. package/dist/components/data-display/data-table.js +1 -3
  18. package/dist/components/data-display/descriptions.d.ts +2 -6
  19. package/dist/components/data-display/descriptions.js +0 -2
  20. package/dist/components/data-display/index.d.ts +4 -0
  21. package/dist/components/data-display/index.js +4 -0
  22. package/dist/components/data-display/list-row.d.ts +3 -22
  23. package/dist/components/data-display/permission-matrix.d.ts +7 -20
  24. package/dist/components/data-display/progress.d.ts +1 -1
  25. package/dist/components/data-display/prose.d.ts +4 -0
  26. package/dist/components/data-display/prose.js +20 -0
  27. package/dist/components/data-display/service-launcher-card.d.ts +3 -10
  28. package/dist/components/data-display/table.d.ts +20 -80
  29. package/dist/components/data-display/table.js +0 -3
  30. package/dist/components/data-display/timeline.d.ts +3 -5
  31. package/dist/components/data-entry/branch-scope-picker.d.ts +2 -19
  32. package/dist/components/data-entry/branch-scope-picker.js +0 -1
  33. package/dist/components/data-entry/calendar.d.ts +3 -2
  34. package/dist/components/data-entry/calendar.js +58 -3
  35. package/dist/components/data-entry/command-palette.d.ts +8 -22
  36. package/dist/components/data-entry/date-picker.d.ts +4 -5
  37. package/dist/components/data-entry/date-picker.js +6 -1
  38. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  39. package/dist/components/data-entry/date-range-picker.js +8 -3
  40. package/dist/components/data-entry/form-errors.d.ts +7 -18
  41. package/dist/components/data-entry/form-field.js +1 -5
  42. package/dist/components/data-entry/form.d.ts +1 -9
  43. package/dist/components/data-entry/input.d.ts +16 -14
  44. package/dist/components/data-entry/input.js +3 -0
  45. package/dist/components/data-entry/month-picker.js +2 -2
  46. package/dist/components/data-entry/month-range-picker.d.ts +4 -6
  47. package/dist/components/data-entry/month-range-picker.js +2 -2
  48. package/dist/components/data-entry/number-input.d.ts +1 -5
  49. package/dist/components/data-entry/number-input.js +1 -0
  50. package/dist/components/data-entry/radio.js +0 -1
  51. package/dist/components/data-entry/search-select.d.ts +2 -4
  52. package/dist/components/data-entry/select.d.ts +11 -6
  53. package/dist/components/data-entry/select.js +4 -2
  54. package/dist/components/data-entry/switch.js +0 -1
  55. package/dist/components/data-entry/textarea.d.ts +10 -16
  56. package/dist/components/data-entry/time-picker.d.ts +3 -5
  57. package/dist/components/data-entry/upload-types.d.ts +1 -5
  58. package/dist/components/data-entry/use-upload-draft.d.ts +0 -4
  59. package/dist/components/feedback/dialog.d.ts +6 -15
  60. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  61. package/dist/components/feedback/sheet.d.ts +3 -10
  62. package/dist/components/feedback/sheet.js +1 -1
  63. package/dist/components/feedback/sonner.js +2 -3
  64. package/dist/components/general/activity.d.ts +4 -30
  65. package/dist/components/general/button.js +1 -10
  66. package/dist/components/general/logo.d.ts +6 -28
  67. package/dist/components/general/reveal.d.ts +3 -9
  68. package/dist/components/general/typography.d.ts +1 -3
  69. package/dist/components/layout/account-chip.d.ts +8 -0
  70. package/dist/components/layout/account-chip.js +45 -0
  71. package/dist/components/layout/auth-divider.d.ts +2 -9
  72. package/dist/components/layout/auth-shell.d.ts +1 -14
  73. package/dist/components/layout/breadcrumb.d.ts +1 -5
  74. package/dist/components/layout/centered-shell.d.ts +4 -18
  75. package/dist/components/layout/error-surface.d.ts +11 -28
  76. package/dist/components/layout/error-surface.js +4 -1
  77. package/dist/components/layout/index.d.ts +2 -0
  78. package/dist/components/layout/index.js +2 -0
  79. package/dist/components/layout/legal-document-shell.d.ts +5 -28
  80. package/dist/components/layout/master-detail.d.ts +2 -18
  81. package/dist/components/layout/page-container.js +0 -1
  82. package/dist/components/layout/responsive-grid.d.ts +6 -1
  83. package/dist/components/layout/responsive-grid.js +9 -2
  84. package/dist/components/layout/service-role-panel.d.ts +7 -19
  85. package/dist/components/layout/sidebar.d.ts +5 -27
  86. package/dist/components/layout/sidebar.js +0 -1
  87. package/dist/components/layout/split-pane.d.ts +0 -7
  88. package/dist/components/layout/topbar.d.ts +1 -14
  89. package/dist/components/navigation/app-setting-picker.js +1 -3
  90. package/dist/components/navigation/filter-bar.d.ts +1 -6
  91. package/dist/components/navigation/pagination-utils.d.ts +2 -12
  92. package/dist/components/navigation/tabs-scroll.d.ts +2 -36
  93. package/dist/components/navigation/tabs.js +4 -8
  94. package/dist/components/query/data-state.d.ts +2 -2
  95. package/dist/components/ui/credential-reveal.d.ts +1 -4
  96. package/dist/components/ui/hover-card.d.ts +1 -2
  97. package/dist/components/ui/separator.d.ts +2 -15
  98. package/dist/components/ui/toggle-group.d.ts +1 -5
  99. package/dist/components/ui/toggle.d.ts +6 -31
  100. package/dist/email/brand-mark.d.ts +6 -34
  101. package/dist/email/color.d.ts +7 -17
  102. package/dist/email/geometry.d.ts +4 -8
  103. package/dist/email/index.d.ts +4 -48
  104. package/dist/email/inline-style.d.ts +3 -9
  105. package/dist/email/urgency.d.ts +2 -7
  106. package/dist/form/form-context.d.ts +2 -5
  107. package/dist/form/form-root.d.ts +1 -2
  108. package/dist/i18n/messages/en.json +4 -0
  109. package/dist/i18n/messages/ja.json +4 -0
  110. package/dist/i18n/messages/vi.json +4 -0
  111. package/dist/inertia/index.d.ts +2 -10
  112. package/dist/lib/control-styles.d.ts +10 -21
  113. package/dist/lib/control-styles.js +4 -6
  114. package/dist/lib/datetime/format-date.d.ts +1 -4
  115. package/dist/lib/field-a11y.d.ts +11 -68
  116. package/dist/lib/field-a11y.js +0 -1
  117. package/dist/lib/format.d.ts +4 -8
  118. package/dist/lib/hooks.d.ts +6 -12
  119. package/dist/lib/permission-grid.d.ts +3 -18
  120. package/dist/lib/query-error.d.ts +3 -3
  121. package/dist/props/components/app.prop.d.ts +15 -38
  122. package/dist/props/components/charts.prop.d.ts +2 -3
  123. package/dist/props/components/data-display.prop.d.ts +55 -72
  124. package/dist/props/components/data-entry.prop.d.ts +35 -40
  125. package/dist/props/components/form.prop.d.ts +2 -6
  126. package/dist/props/components/general.prop.d.ts +11 -27
  127. package/dist/props/components/layout.prop.d.ts +89 -260
  128. package/dist/props/components/navigation.prop.d.ts +27 -42
  129. package/dist/props/registry.d.ts +36 -1
  130. package/dist/props/registry.js +45 -1
  131. package/dist/props/vocabulary/data.prop.d.ts +18 -49
  132. package/dist/props/vocabulary/index.d.ts +1 -1
  133. package/dist/props/vocabulary/interaction.prop.d.ts +10 -12
  134. package/dist/props/vocabulary/layout.prop.d.ts +8 -30
  135. package/dist/props/vocabulary/shared.prop.d.ts +3 -2
  136. package/dist/styles/alert-layout.css +1 -32
  137. package/dist/styles/badge-layout.css +19 -18
  138. package/dist/styles/base.css +14 -113
  139. package/dist/styles/card-layout.css +6 -128
  140. package/dist/styles/chart-layout.css +2 -17
  141. package/dist/styles/control.css +380 -458
  142. package/dist/styles/core.css +21 -0
  143. package/dist/styles/data-display-layout.css +175 -94
  144. package/dist/styles/data-entry-layout.css +0 -45
  145. package/dist/styles/density.css +1 -30
  146. package/dist/styles/dialog-layout.css +5 -69
  147. package/dist/styles/focus-ring.css +13 -87
  148. package/dist/styles/fonts.css +1 -31
  149. package/dist/styles/form-layout.css +7 -91
  150. package/dist/styles/index.css +2 -20
  151. package/dist/styles/layout.css +38 -456
  152. package/dist/styles/logo-layout.css +1 -35
  153. package/dist/styles/motion.css +3 -42
  154. package/dist/styles/navigation-layout.css +3 -50
  155. package/dist/styles/shell-layout.css +37 -403
  156. package/dist/styles/table-layout.css +16 -274
  157. package/dist/styles/text-layout.css +7 -21
  158. package/dist/styles/toggle.css +1 -23
  159. package/dist/theme/dxs.canonical.css +1 -55
  160. package/dist/theme/example.service.css +1 -26
  161. package/dist/theme/famgia.service.css +5 -15
  162. package/dist/tokens/axes.css +3 -40
  163. package/dist/tokens/base.css +1 -4
  164. package/dist/tokens/components/activity.css +6 -42
  165. package/dist/tokens/components/badge.css +12 -13
  166. package/dist/tokens/components/banner.css +4 -9
  167. package/dist/tokens/components/card.css +32 -88
  168. package/dist/tokens/components/chart.css +9 -26
  169. package/dist/tokens/components/control.css +37 -222
  170. package/dist/tokens/components/data-display.css +28 -79
  171. package/dist/tokens/components/data-entry.css +5 -19
  172. package/dist/tokens/components/descriptions.css +7 -27
  173. package/dist/tokens/components/email.css +38 -84
  174. package/dist/tokens/components/error-surface.css +2 -18
  175. package/dist/tokens/components/feedback.css +27 -124
  176. package/dist/tokens/components/form.css +13 -45
  177. package/dist/tokens/components/legal-document.css +16 -41
  178. package/dist/tokens/components/list-row.css +11 -36
  179. package/dist/tokens/components/logo.css +2 -57
  180. package/dist/tokens/components/navigation.css +16 -94
  181. package/dist/tokens/components/scroll-area.css +2 -18
  182. package/dist/tokens/components/separator.css +8 -30
  183. package/dist/tokens/components/sheet.css +3 -14
  184. package/dist/tokens/components/shell.css +53 -307
  185. package/dist/tokens/components/sidebar.css +5 -22
  186. package/dist/tokens/components/table.css +19 -140
  187. package/dist/tokens/components/toggle.css +3 -65
  188. package/dist/tokens/components/upload.css +7 -43
  189. package/dist/tokens/foundation.css +108 -383
  190. package/dist/tokens/semantic/layout.css +33 -207
  191. package/docs/CANONICAL-CONTRACTS.md +206 -0
  192. package/docs/COMPONENTS.md +157 -0
  193. package/docs/COMPOSITION-VS-COMPONENT.md +105 -0
  194. package/docs/CONSUMER-RULES.md +14 -0
  195. package/docs/CUSTOMER-THEMING.md +162 -0
  196. package/docs/DATETIME.md +69 -0
  197. package/docs/DEVELOPMENT.md +173 -0
  198. package/docs/FORMS.md +143 -0
  199. package/docs/FRAME-A11Y-CI.md +267 -0
  200. package/docs/FRAME-COVERAGE-LEDGER.md +123 -0
  201. package/docs/FRAME-COVERAGE-REPORT.md +140 -0
  202. package/docs/FRAME-COVERAGE-STANDARD.md +65 -0
  203. package/docs/FRAME-COVERAGE.md +29 -0
  204. package/docs/PROPS-REGISTRY.md +61 -0
  205. package/docs/PROPS-VOCABULARY.md +100 -0
  206. package/docs/README.md +60 -0
  207. package/docs/REGISTRY.md +70 -0
  208. package/docs/SPACING.md +56 -0
  209. package/docs/STANDARDS-vocabulary-tokens.md +48 -0
  210. package/docs/TESTING.md +98 -0
  211. package/docs/TOKENS.md +347 -0
  212. package/docs/asset-modules.d.ts +46 -0
  213. package/docs/assets/cover-terrain.svg +6 -0
  214. package/docs/assets/portrait-amber.svg +5 -0
  215. package/docs/assets/portrait-clay.svg +5 -0
  216. package/docs/assets/portrait-indigo.svg +5 -0
  217. package/docs/assets/portrait-iris.svg +5 -0
  218. package/docs/assets/portrait-moss.svg +5 -0
  219. package/docs/assets/portrait-plum.svg +5 -0
  220. package/docs/assets/portrait-slate.svg +5 -0
  221. package/docs/charts/compact-bar-trend.tsx +328 -0
  222. package/docs/charts/size-tiers.tsx +192 -0
  223. package/docs/data-display/accordion.tsx +99 -0
  224. package/docs/data-display/avatar.tsx +355 -0
  225. package/docs/data-display/badge.tsx +283 -0
  226. package/docs/data-display/card/examples/detail-panel.tsx +106 -0
  227. package/docs/data-display/card/index.md +3 -0
  228. package/docs/data-display/card/index.tsx +285 -0
  229. package/docs/data-display/carousel.tsx +160 -0
  230. package/docs/data-display/charts.tsx +198 -0
  231. package/docs/data-display/code-block.tsx +90 -0
  232. package/docs/data-display/collapsible.tsx +162 -0
  233. package/docs/data-display/credential-reveal.tsx +145 -0
  234. package/docs/data-display/data-table/examples/approval-queue.tsx +212 -0
  235. package/docs/data-display/data-table/examples/grid-features.tsx +158 -0
  236. package/docs/data-display/data-table/examples/list-page.tsx +175 -0
  237. package/docs/data-display/data-table/index.md +5 -0
  238. package/docs/data-display/data-table/index.tsx +295 -0
  239. package/docs/data-display/descriptions.tsx +143 -0
  240. package/docs/data-display/empty-state.tsx +164 -0
  241. package/docs/data-display/hover-card.tsx +181 -0
  242. package/docs/data-display/list-row.tsx +420 -0
  243. package/docs/data-display/permission-matrix.tsx +188 -0
  244. package/docs/data-display/popover-touch.tsx +27 -0
  245. package/docs/data-display/popover.tsx +246 -0
  246. package/docs/data-display/progress.tsx +84 -0
  247. package/docs/data-display/prose.tsx +105 -0
  248. package/docs/data-display/qr-code.tsx +68 -0
  249. package/docs/data-display/scroll-area.tsx +303 -0
  250. package/docs/data-display/service-launcher-card.tsx +145 -0
  251. package/docs/data-display/stat-card.tsx +99 -0
  252. package/docs/data-display/table.tsx +251 -0
  253. package/docs/data-display/timeline.tsx +112 -0
  254. package/docs/data-display/touch-actions.tsx +36 -0
  255. package/docs/data-display/tree-list.tsx +107 -0
  256. package/docs/data-entry/branch-scope-picker.tsx +123 -0
  257. package/docs/data-entry/calendar.tsx +180 -0
  258. package/docs/data-entry/cascader.tsx +378 -0
  259. package/docs/data-entry/checkbox.tsx +256 -0
  260. package/docs/data-entry/color-picker.tsx +117 -0
  261. package/docs/data-entry/command-palette.tsx +179 -0
  262. package/docs/data-entry/command.tsx +262 -0
  263. package/docs/data-entry/country-picker-recipe.tsx +175 -0
  264. package/docs/data-entry/date-picker.tsx +191 -0
  265. package/docs/data-entry/date-range-picker.tsx +149 -0
  266. package/docs/data-entry/form/examples/employee-settings.tsx +267 -0
  267. package/docs/data-entry/form/examples/invoice-form.tsx +387 -0
  268. package/docs/data-entry/form-field/examples/a11y-contract.tsx +240 -0
  269. package/docs/data-entry/form-field/examples/create-form.tsx +105 -0
  270. package/docs/data-entry/form-field/index.md +8 -0
  271. package/docs/data-entry/form-field/index.tsx +330 -0
  272. package/docs/data-entry/form-inertia.tsx +80 -0
  273. package/docs/data-entry/form-root.tsx +102 -0
  274. package/docs/data-entry/form.tsx +724 -0
  275. package/docs/data-entry/input-otp.tsx +231 -0
  276. package/docs/data-entry/input.tsx +114 -0
  277. package/docs/data-entry/label.tsx +100 -0
  278. package/docs/data-entry/month-picker.tsx +101 -0
  279. package/docs/data-entry/month-range-picker.tsx +115 -0
  280. package/docs/data-entry/number-input.tsx +240 -0
  281. package/docs/data-entry/password-input.tsx +102 -0
  282. package/docs/data-entry/password-strength.tsx +97 -0
  283. package/docs/data-entry/radio-group.tsx +95 -0
  284. package/docs/data-entry/rating.tsx +87 -0
  285. package/docs/data-entry/search-input.tsx +120 -0
  286. package/docs/data-entry/select-async-contracts.tsx +108 -0
  287. package/docs/data-entry/select-matrix.tsx +204 -0
  288. package/docs/data-entry/select.tsx +369 -0
  289. package/docs/data-entry/slider.tsx +130 -0
  290. package/docs/data-entry/switch.tsx +67 -0
  291. package/docs/data-entry/tag-input.tsx +108 -0
  292. package/docs/data-entry/textarea.tsx +209 -0
  293. package/docs/data-entry/time-picker.tsx +99 -0
  294. package/docs/data-entry/toggle-count.tsx +293 -0
  295. package/docs/data-entry/toggle-group.tsx +225 -0
  296. package/docs/data-entry/toggle.tsx +127 -0
  297. package/docs/data-entry/transfer.tsx +97 -0
  298. package/docs/data-entry/tree-select.tsx +204 -0
  299. package/docs/data-entry/upload.tsx +184 -0
  300. package/docs/feedback/alert-dialog.tsx +270 -0
  301. package/docs/feedback/alert.tsx +248 -0
  302. package/docs/feedback/banner.tsx +183 -0
  303. package/docs/feedback/danger-confirm.tsx +88 -0
  304. package/docs/feedback/dialog-touch.tsx +24 -0
  305. package/docs/feedback/dialog.tsx +193 -0
  306. package/docs/feedback/sheet.tsx +420 -0
  307. package/docs/feedback/skeleton.tsx +111 -0
  308. package/docs/feedback/toast.tsx +135 -0
  309. package/docs/feedback/tooltip.tsx +171 -0
  310. package/docs/feedback/two-factor-setup.tsx +68 -0
  311. package/docs/foundation/_email-specimen.ts +274 -0
  312. package/docs/foundation/colors.tsx +204 -0
  313. package/docs/foundation/density.tsx +232 -0
  314. package/docs/foundation/email-tokens.tsx +323 -0
  315. package/docs/foundation/radius-shadow.tsx +154 -0
  316. package/docs/foundation/spacing.tsx +211 -0
  317. package/docs/foundation/typography.tsx +147 -0
  318. package/docs/general/activity.tsx +224 -0
  319. package/docs/general/button/examples/destructive-confirm.tsx +90 -0
  320. package/docs/general/button/examples/form-actions.tsx +85 -0
  321. package/docs/general/button/examples/toolbar.tsx +160 -0
  322. package/docs/general/button/index.md +12 -0
  323. package/docs/general/button/index.tsx +211 -0
  324. package/docs/general/logo.tsx +131 -0
  325. package/docs/general/reveal.tsx +41 -0
  326. package/docs/general/typography.tsx +152 -0
  327. package/docs/layout/account-chip.tsx +72 -0
  328. package/docs/layout/admin-collection.tsx +122 -0
  329. package/docs/layout/app-shell.tsx +374 -0
  330. package/docs/layout/aspect-ratio.tsx +135 -0
  331. package/docs/layout/auth-account-summary.tsx +103 -0
  332. package/docs/layout/auth-divider.tsx +28 -0
  333. package/docs/layout/auth-footer.tsx +37 -0
  334. package/docs/layout/auth-identity.tsx +36 -0
  335. package/docs/layout/auth-recovery/examples/mfa-challenge.md +41 -0
  336. package/docs/layout/auth-recovery/examples/mfa-challenge.tsx +254 -0
  337. package/docs/layout/auth-recovery/examples/mobile-390.md +25 -0
  338. package/docs/layout/auth-recovery/examples/mobile-390.tsx +82 -0
  339. package/docs/layout/auth-recovery/examples/password-recovery.md +24 -0
  340. package/docs/layout/auth-recovery/examples/password-recovery.tsx +197 -0
  341. package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.md +27 -0
  342. package/docs/layout/auth-recovery/examples/wrapping-ja-en-vi.tsx +156 -0
  343. package/docs/layout/auth-recovery/index.md +140 -0
  344. package/docs/layout/auth-recovery/index.tsx +96 -0
  345. package/docs/layout/auth-shell-context.tsx +103 -0
  346. package/docs/layout/auth-shell-device.tsx +112 -0
  347. package/docs/layout/auth-shell-registration.tsx +335 -0
  348. package/docs/layout/auth-shell-variants.tsx +139 -0
  349. package/docs/layout/auth-shell.tsx +59 -0
  350. package/docs/layout/auth-stack.tsx +40 -0
  351. package/docs/layout/centered-shell.tsx +102 -0
  352. package/docs/layout/error-surface/examples/application-403.tsx +84 -0
  353. package/docs/layout/error-surface/examples/application-404.tsx +77 -0
  354. package/docs/layout/error-surface/examples/system-500.tsx +47 -0
  355. package/docs/layout/error-surface/examples/system-503.tsx +48 -0
  356. package/docs/layout/error-surface/index.md +153 -0
  357. package/docs/layout/error-surface/index.tsx +239 -0
  358. package/docs/layout/flex.tsx +266 -0
  359. package/docs/layout/legal-document-shell/_data.ts +110 -0
  360. package/docs/layout/legal-document-shell/examples/desktop-1440.md +12 -0
  361. package/docs/layout/legal-document-shell/examples/desktop-1440.tsx +83 -0
  362. package/docs/layout/legal-document-shell/examples/mobile-390.md +22 -0
  363. package/docs/layout/legal-document-shell/examples/mobile-390.tsx +70 -0
  364. package/docs/layout/legal-document-shell/examples/tablet-1024.md +3 -0
  365. package/docs/layout/legal-document-shell/examples/tablet-1024.tsx +84 -0
  366. package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.md +18 -0
  367. package/docs/layout/legal-document-shell/examples/wrapping-ja-en-vi.tsx +61 -0
  368. package/docs/layout/legal-document-shell/index.md +51 -0
  369. package/docs/layout/legal-document-shell/index.tsx +101 -0
  370. package/docs/layout/master-detail.tsx +387 -0
  371. package/docs/layout/org-switcher.tsx +131 -0
  372. package/docs/layout/page-container.tsx +954 -0
  373. package/docs/layout/resizable-panel.tsx +419 -0
  374. package/docs/layout/responsive-grid.tsx +243 -0
  375. package/docs/layout/separator.tsx +327 -0
  376. package/docs/layout/service-role-panel.tsx +144 -0
  377. package/docs/layout/sidebar.tsx +609 -0
  378. package/docs/layout/split-pane.tsx +468 -0
  379. package/docs/layout/topbar.tsx +349 -0
  380. package/docs/navigation/app-setting-picker.tsx +142 -0
  381. package/docs/navigation/breadcrumb.tsx +135 -0
  382. package/docs/navigation/context-menu.tsx +123 -0
  383. package/docs/navigation/dropdown-menu.tsx +213 -0
  384. package/docs/navigation/filter-bar.tsx +227 -0
  385. package/docs/navigation/menubar.tsx +141 -0
  386. package/docs/navigation/navigation-menu.tsx +158 -0
  387. package/docs/navigation/pagination.tsx +241 -0
  388. package/docs/navigation/steps.tsx +265 -0
  389. package/docs/navigation/tabs-rtl.tsx +28 -0
  390. package/docs/navigation/tabs.tsx +314 -0
  391. package/docs/navigation/toolbar.tsx +339 -0
  392. package/docs/providers/app-provider.tsx +191 -0
  393. package/docs/providers/format-date.tsx +230 -0
  394. package/docs/query/button-refetch.tsx +160 -0
  395. package/docs/query/data-state.tsx +192 -0
  396. package/docs/query/infinite-query-state.tsx +93 -0
  397. package/docs/query/mutation-feedback.tsx +70 -0
  398. package/docs/query/prefetch-link.tsx +262 -0
  399. package/docs/query/touch-actions.tsx +68 -0
  400. package/docs/showcase/acme-portal.tsx +455 -0
  401. package/docs/showcase/acme-website.tsx +461 -0
  402. package/docs/showcase/case1-warehouse-dashboard.tsx +359 -0
  403. package/docs/showcase/case2-employee-me.tsx +444 -0
  404. package/docs/showcase/case3-approval-workflow.tsx +529 -0
  405. package/docs/showcase/case4-login.tsx +313 -0
  406. package/docs/showcase/case5-shift-calendar.tsx +795 -0
  407. package/docs/showcase/case6-agency-handy.tsx +1113 -0
  408. package/docs/showcase/futurelastic-web.tsx +392 -0
  409. package/docs/showcase/org-switcher.tsx +313 -0
  410. package/docs/showcase/permission-matrix.tsx +346 -0
  411. package/docs/showcase/public-landing.tsx +210 -0
  412. package/docs/showcase/settings-account-sections.tsx +559 -0
  413. package/docs/showcase/settings-security-mfa.tsx +145 -0
  414. package/docs/showcase/table-approval-queue.tsx +199 -0
  415. package/docs/showcase/table-bulk-actions.tsx +460 -0
  416. package/docs/showcase/table-compact-kintone.tsx +438 -0
  417. package/docs/showcase/table-conditional-format.tsx +367 -0
  418. package/docs/showcase/table-crud-list.tsx +544 -0
  419. package/docs/showcase/table-density.tsx +347 -0
  420. package/docs/showcase/table-expandable-rows.tsx +480 -0
  421. package/docs/showcase/table-filter-chips.tsx +456 -0
  422. package/docs/showcase/table-footer-totals.tsx +314 -0
  423. package/docs/showcase/table-grouped-subtotals.tsx +391 -0
  424. package/docs/showcase/table-master-detail.tsx +484 -0
  425. package/docs/showcase/table-pagination.tsx +327 -0
  426. package/docs/showcase/table-states.tsx +366 -0
  427. package/docs/showcase/table-sticky-columns.tsx +376 -0
  428. package/docs/showcase/table-tree-rows.tsx +444 -0
  429. package/docs/showcase/table-view-tabs.tsx +488 -0
  430. package/package.json +9 -12
  431. package/scripts/_agent-setup.mjs +16 -2
  432. package/scripts/audit-hook.mjs +3 -13
  433. package/scripts/init-agent-kit.mjs +1 -14
  434. package/scripts/postinstall.mjs +12 -12
  435. package/scripts/ui-audit.mjs +93 -31
  436. package/scripts/visual-audit-rules.mjs +51 -7
  437. package/scripts/visual-audit.mjs +118 -47
@@ -0,0 +1,173 @@
1
+ # Development Guideline — `@godxjp/ui`
2
+
3
+ How to work **on** the design system (not just with it). If you only consume the framework from the app, read [the ten consumer rules](./CONSUMER-RULES.md) instead — this file is for people **editing `packages/godx-ui/`**.
4
+
5
+ ---
6
+
7
+ ## 0. What this package IS — and the boundary it must keep
8
+
9
+ `@godxjp/ui` is **the** UI framework for every godx surface (admin, agency portal, handheld). It is shared, versioned infrastructure: one change here ripples to every screen and every consumer. Two consequences:
10
+
11
+ 1. **Editing it needs explicit session permission** — the hard gate in [CLAUDE.md](../CLAUDE.md) (MANDATORY: read the component skill before touching UI). Treat the package as off-limits by default. 2. **It is generic and presentational only.** The framework knows about _tokens, layout, accessibility, and interaction_ — never about the app's data, routes, language files, or business rules.
12
+
13
+ ### The hard boundary — consumer-layer concerns MUST NOT leak in
14
+
15
+ A component in this package may **not** import or assume any of the following. These belong to the **consumer** (the app), which composes framework primitives around them:
16
+
17
+ | ❌ Never inside `packages/godx-ui/` | ✅ Where it belongs |
18
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
19
+ | App i18n — `useTranslation`, `t()`, `resources/js/i18n` | App passes already-translated strings in as props |
20
+ | Inertia — `router`, `usePage`, `<Form>`, `@inertiajs/*` | App wires navigation/submit; pass `onClick`/`href`/`onSubmit` props |
21
+ | Wayfinder routes — `@/routes`, `@/actions`, `.url()` | App resolves the URL, passes a plain string |
22
+ | Business entities / domain logic (Item, Packing, shipment rules) | App-level component in `resources/js/components/**` |
23
+ | App copy / labels / placeholder text baked into the component | Comes in via props; framework ships no product wording |
24
+ | Raw colors / hex / `hsl(...)` literals, `dark:` overrides | Semantic tokens (`var(--…)`, `bg-primary`, …) — see §3 |
25
+ | A consumer's theme values redefined inside a component | Theme lives in `src/tokens/` (the framework default ships it) |
26
+
27
+ > **Litmus test:** if a component can't render in a Storybook-style preview with
28
+ > plain props and zero app context, it does **not** belong in this package. Keep it
29
+ > app-level (`resources/js/components/admin/…`) and have it _compose_ framework
30
+ > primitives. See the **`godx-ui-component-placement`** skill for the full decision.
31
+
32
+ The framework ships its **own theme** (colors, fonts, type scale, wa-iro palette) in `src/tokens/foundation.css`, so consumers import `@godxjp/ui/styles` and need **zero** extra theme config. Do not push theme decisions back onto the consumer.
33
+
34
+ ---
35
+
36
+ ## 1. Architecture — the layers, bottom-up
37
+
38
+ ```
39
+ src/tokens/ Design tokens (the single source of values)
40
+ foundation.css :root + .dark — colors, fonts, type scale, spacing, radius, wa-iro
41
+ primitives/*.css Per-domain primitive tokens (card, table, control, badge, …)
42
+ base.css Coordinates the token layer
43
+ src/styles/ CSS that styles components by [data-slot] + density.css (the density knob)
44
+ index.css Entry: @import fontsource → tailwindcss → @theme (token→utility map) → layout css
45
+ src/components/ React components, grouped (data-display, data-entry, layout, …)
46
+ src/props/ Prop type system — vocabulary/ (atomic) + components/ + registry.ts
47
+ src/lib/ cn(), control-styles, variants — shared helpers
48
+ examples/ *.preview.tsx — Storybook-style stories (preview app)
49
+ docs/primitives/ <component>/index.tsx demo + examples/ + generated .md
50
+ preview/ The preview app (vite) on :6008 that renders examples + docs
51
+ ```
52
+
53
+ **Token → utility flow:** a value is defined once as a CSS var in `tokens/foundation.css` (e.g. `--primary`), mapped to a Tailwind utility in the `@theme` block of `styles/index.css` (`--color-primary: hsl(var(--primary))`), and consumed as `bg-primary` / `hsl(var(--primary))`. Never skip a layer with a literal.
54
+
55
+ ### The component pattern — markup emits slots, CSS owns styling
56
+
57
+ Components render semantic structure and `data-slot` / `data-*` flags; **the spacing, padding, and chrome live in `src/styles/*-layout.css`**, keyed on those slots. This keeps density and theming centralized.
58
+
59
+ ```tsx
60
+ // component: emits slots + flags only
61
+ <div data-slot="card" data-variant={variant} data-density={density} />
62
+ ```
63
+
64
+ ```css
65
+ /* styles/card-layout.css: owns the look */
66
+ [data-slot="card"] {
67
+ border: 1px solid hsl(var(--card-border));
68
+ border-radius: var(--card-radius);
69
+ }
70
+ [data-slot="card"][data-density="tight"] {
71
+ --card-space-inset: var(--space-3);
72
+ }
73
+ ```
74
+
75
+ Prefer this over hardcoding Tailwind padding inside the component. Use Tailwind utility classes for **one-off layout** (flex/grid/gap), not for re-theming.
76
+
77
+ ### Density
78
+
79
+ One knob — `.ui-density-{compact,default,comfortable}` in `styles/density.css` — retunes `--phi-unit`, control heights, and table row heights together. Components read the resulting tokens; never branch on density in component JS.
80
+
81
+ ### The `ui/` layer
82
+
83
+ `src/components/ui/*` are thin **re-exports** of the canonical implementation (`export * from "../data-display/card"`). They exist for shadcn-style import paths. Edit the canonical file under its group; the `ui/` path follows automatically.
84
+
85
+ ---
86
+
87
+ ## 2. Adding or changing a component (after you have §0 permission)
88
+
89
+ Work in this order; only advance when the previous step genuinely can't express the need:
90
+
91
+ 1. **Use** an existing primitive. 2. **Compose** primitives (Card + FormField + Flex…). 3. **Extend** an existing component — add a prop/slot (e.g. `labelAddon`, `accent`). Prefer this: one more prop beats one more component. 4. **Create** a new component — last resort.
92
+
93
+ Document the decision (which promotion criteria it met) so review can check it — see [COMPOSITION-VS-COMPONENT.md](./COMPOSITION-VS-COMPONENT.md).
94
+
95
+ ---
96
+
97
+ ## 3. Rules for framework code
98
+
99
+ - **Semantic tokens only.** No raw hex / `rgb()` / `hsl()` literals, no palette utilities (`bg-blue-500`), no `dark:` overrides — tokens adapt automatically. Structural literals (a `3px` accent stripe, `1px` borders) are fine; _color/size scale_ values come from tokens. - **No app coupling** (the §0 boundary table above). - **Props live in `src/props/`** — atomic concepts in `vocabulary/`, per-component interfaces in `components/`. Check `registry.ts` + `PROP_ALIASES_FORBIDDEN` before inventing a prop name.
100
+
101
+ ---
102
+
103
+ ## 4. Every component change ships its story + docs
104
+
105
+ A change isn't done until its documentation reflects it:
106
+
107
+ 1. **Preview story** — `examples/<group>/<Component>.preview.tsx` (Storybook-style). New props get a story (see `examples/data-display/Card.preview.tsx`: Surfaces / Density / AccentEdges). 2. **Docs demo** — `docs/primitives/<group>/<component>/index.tsx` shows the new capability; `examples/` holds focused per-feature demos. 3. **Regenerate props docs** — `pnpm docs:sync-primitives` regenerates the `.md` from source. Run it after prop changes so the tables stay accurate.
108
+
109
+ > Docs are mid-migration to the `<component>/index.tsx (+ examples/)` shape. Do **not**
110
+ > resurrect flat `docs/primitives/<component>.tsx` demos — they were dead orphans and
111
+ > were removed.
112
+
113
+ ---
114
+
115
+ ## 5. Verify before finishing
116
+
117
+ ```bash
118
+ pnpm lint # eslint — self-contained flat config
119
+ pnpm typecheck # tsc --noEmit
120
+ pnpm test # vitest per component
121
+ pnpm preview:build # integration test: examples + docs must build
122
+ pnpm audit # godxjp-ui-audit — 0 errors for touched files
123
+ pnpm check:mcp-sync # MCP registry ↔ library export drift guard
124
+ ```
125
+
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
+
128
+ 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
+ The app side additionally runs **`npm run ui:audit`** (the design-system linter) and must report 0 errors for touched files.
131
+
132
+ ---
133
+
134
+ ## 6. Releasing — the lib and its MCP, in lockstep
135
+
136
+ This repo publishes **two packages** that must agree: `@godxjp/ui` (the browser component library, root `package.json`) and `@godxjp/ui-mcp` (the Node MCP server that tells agents how to use it, `mcp/`).
137
+
138
+ ### The lockstep contract (issue #140)
139
+
140
+ A consumer that installs `@godxjp/ui@16.10.x` but points its agent at `@godxjp/ui-mcp@16.7.x` gets prop/token/pattern guidance for a build it never installed. To make that impossible, the two packages carry **mutual, machine-readable compatibility metadata**:
141
+
142
+ - root `package.json` → `"godxUiMcp": "<mcp version>"` — the catalog version this library ships with.
143
+ - `mcp/package.json` → `"version"` (identical to the library) **and** `"godxUiCompatibility": "<maj>.<min>.x"` — the UI minor this catalog describes.
144
+ - the MCP exposes it at runtime: `serverInfo.version` == package version, the `godx-ui://compatibility` resource, and the `check_compatibility` tool (an agent calls it with the app's installed `@godxjp/ui` version to get a match/mismatch verdict).
145
+
146
+ - **Lockstep guard** (`pnpm check:mcp-lockstep` → `scripts/check-release-lockstep.mjs`) — asserts ui.version == mcp.version, ui.godxUiMcp == mcp.version, and mcp.godxUiCompatibility covers the UI version. Any split fails CI. - **Catalog drift guard** (`pnpm check:mcp-sync` / `check:mcp-orphans`) — every component the MCP catalogs must still be a real library export, and vice-versa. - **Packed-artifact guard** (`release-integrity` CI, step 4) — packs both tarballs and re-asserts lockstep on the **packed** manifests, i.e. the bytes that would actually ship.
147
+
148
+ ### Coordinated release (`pnpm release`)
149
+
150
+ ```bash
151
+ pnpm release --ui minor --mcp sync # bump ui, republish mcp at the SAME new version — the norm
152
+ pnpm release --mcp sync # mcp-only fix at the current ui version
153
+ ```
154
+
155
+ `--ui <bump>` **requires** `--mcp sync` (the tool refuses a ui-only release) so the two packages can never split. On a bump the tool refreshes both compatibility fields, runs `verify:release` (incl. all three guards) before publishing the lib, `npm publish`es each package, re-runs the lockstep check as a final fail-closed gate, then commits the version bumps.
156
+
157
+ ### Recovery from a partial publish
158
+
159
+ If a run publishes `@godxjp/ui` but the `@godxjp/ui-mcp` publish fails (npm hiccup, expired token), the two are momentarily split on npm. Recover:
160
+
161
+ 1. Fix the cause (e.g. re-auth `npm whoami`), keep the working tree at the just-published version. 2. Re-run **only** the MCP half at that same version: `pnpm --dir mcp build && npm --prefix mcp version <ui-version> --allow-same-version && npm --prefix mcp publish --access public`. 3.
162
+
163
+ npm versions are immutable, so you can never "fix" a bad published version in place — you can only publish the matching partner at the same number, or move both forward with a fresh `pnpm release --ui patch --mcp sync`. Never hand-publish one package and forget the other.
164
+
165
+ ---
166
+
167
+ ## See also
168
+
169
+ - [README](../README.md) — overview, component groups, consumer setup.
170
+ - [CONSUMER-RULES.md](./CONSUMER-RULES.md) — the ten rules every consumer follows (the audit enforces them).
171
+ - `docs/TOKENS.md`, `docs/SPACING.md`, `docs/PROPS-VOCABULARY.md`, `docs/PROPS-REGISTRY.md`.
172
+ - [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y + geometry + coverage CI gates.
173
+ - **`godx-ui-component-placement`** skill — decide whether a component belongs here.
package/docs/FORMS.md ADDED
@@ -0,0 +1,143 @@
1
+ # Forms — react-hook-form + Zod 4 (mandatory)
2
+
3
+ All forms in admin apps **must** use:
4
+
5
+ | Package | Version | Role |
6
+ | --------------------- | ------- | -------------------- |
7
+ | `react-hook-form` | ^7.76 | Form state |
8
+ | `zod` | ^4.4 | Schema validation |
9
+ | `@hookform/resolvers` | ^5.2 | `zodResolver` bridge |
10
+
11
+ Import from `@godxjp/ui/form` — **never** call `useForm()` without `zodResolver`.
12
+
13
+ ## Quick start
14
+
15
+ ```tsx
16
+ import { z } from "zod";
17
+ import { useZodForm, FormRoot, FormFieldControl } from "@godxjp/ui/form";
18
+ import { Input } from "@godxjp/ui/data-entry";
19
+ import { Button } from "@godxjp/ui/general";
20
+
21
+ const schema = z.object({
22
+ name: z.string().min(1, "Bắt buộc"),
23
+ email: z.string().email("Email không hợp lệ"),
24
+ });
25
+
26
+ type Values = z.infer<typeof schema>;
27
+
28
+ export function CreateCustomerForm({ onSubmit }: { onSubmit: (v: Values) => void }) {
29
+ const form = useZodForm(schema, { defaultValues: { name: "", email: "" } });
30
+
31
+ return (
32
+ <FormRoot form={form} onSubmit={onSubmit}>
33
+ <FormFieldControl name="name" label="Tên" required>
34
+ {(field) => <Input {...field} value={String(field.value ?? "")} />}
35
+ </FormFieldControl>
36
+ <FormFieldControl name="email" label="Email" required>
37
+ {(field) => <Input {...field} type="email" value={String(field.value ?? "")} />}
38
+ </FormFieldControl>
39
+ <Button type="submit">Lưu</Button>
40
+ </FormRoot>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## API
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 |
52
+
53
+ ## Forbidden
54
+
55
+ - ❌ `useForm()` without Zod resolver
56
+ - ❌ `useState` for form field values in admin pages
57
+ - ❌ Manual validation with `if (!email.includes('@'))`
58
+ - ❌ Yup, Valibot, or other schema libraries
59
+ - ❌ Uncontrolled forms without schema
60
+
61
+ ## App dependencies
62
+
63
+ Each app `package.json` must include:
64
+
65
+ ```json
66
+ {
67
+ "dependencies": {
68
+ "react-hook-form": "^7.76.0",
69
+ "zod": "^4.4.3",
70
+ "@hookform/resolvers": "^5.2.2"
71
+ }
72
+ }
73
+ ```
74
+
75
+ Peer-enforced by `@godxjp/ui`.
76
+
77
+ ## Server error bags — `Form errors` + `FormField name` + `<FormErrors />`
78
+
79
+ Server-driven forms (Inertia's `useForm`) return a Laravel error bag whose keys may include
80
+ **hidden/derived fields** (`action_mode`, `page`, a source-record id) that no visible field can
81
+ display — without a summary the submit fails silently. Pass the WHOLE bag once and let fields
82
+ claim their own keys:
83
+
84
+ ```tsx
85
+ import { Form, FormErrors, FormField, Input } from "@godxjp/ui/data-entry";
86
+ import { useForm } from "@inertiajs/react";
87
+
88
+ const form = useForm({ customer_nm: "", action_mode: "regist" });
89
+
90
+ <Form asChild layout="horizontal" labelWidth={140} errors={form.errors}>
91
+ <form onSubmit={submit}>
92
+ <FormErrors />
93
+ <FormField name="customer_nm" label="顧客名" required>
94
+ <Input
95
+ value={form.data.customer_nm}
96
+ onChange={(e) => form.setData("customer_nm", e.target.value)}
97
+ />
98
+ </FormField>
99
+ </form>
100
+ </Form>;
101
+ ```
102
+
103
+ - A `FormField name="…"` resolves its message from `errors[name]` automatically (an explicit
104
+ `error` prop wins; a `string[]` entry surfaces its FIRST message — `$errors->first()`), and
105
+ **claims** the key.
106
+ - `<FormErrors />` renders only the **unclaimed** remainder as a destructive `Alert`
107
+ (`role="alert"`, localized default title) — and nothing when every entry is claimed.
108
+ - Never hand-filter the bag per page; that except-list is exactly what this mechanism removes.
109
+ - `FormFieldControl` forwards its `name`, so `FormRoot` + adapter fields claim their keys too;
110
+ give `<FormErrors errors={form.errors} />` the bag explicitly when there is no surrounding
111
+ `Form errors`.
112
+
113
+ ### Sibling Forms — one bag over several Card+Form sections
114
+
115
+ An edit screen split into several sibling `Card` + `Form` sections still has ONE server bag.
116
+ Wrap the region in `FormErrorsProvider` instead of passing `errors` to each Form — a Form
117
+ **without** its own `errors` joins the surrounding registry, so claims from every section
118
+ subtract from the same `<FormErrors />`:
119
+
120
+ ```tsx
121
+ import { Form, FormErrors, FormErrorsProvider, FormField, Input } from "@godxjp/ui/data-entry";
122
+
123
+ <FormErrorsProvider errors={form.errors}>
124
+ <FormErrors />
125
+ <Card>
126
+ <Form layout="horizontal" labelWidth={170}>
127
+ <FormField name="customer_nm" label="顧客名">
128
+
129
+ </FormField>
130
+ </Form>
131
+ </Card>
132
+ <Card>
133
+ <Form layout="horizontal" labelWidth={170}>
134
+ <FormField name="mail_subject" label="件名">
135
+
136
+ </FormField>
137
+ </Form>
138
+ </Card>
139
+ </FormErrorsProvider>;
140
+ ```
141
+
142
+ A nested Form **with** its own `errors` starts a new registry that shadows the provider — its
143
+ claims and messages stay inside it (use this for an embedded sub-form with a separate bag).
@@ -0,0 +1,267 @@
1
+ # Per-frame accessibility & coverage CI
2
+
3
+ They render every catalog frame in real Chromium and hold the line on accessibility, responsive geometry and contract coverage. Static checks (`audit:examples`, source regexes) cannot see rendered colour/layout/ARIA — these gates do.
4
+
5
+ ## The three gates
6
+
7
+ | Script | `pnpm` | Drives | Blocking? |
8
+ | ----------------------------- | ---------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
9
+ | `scripts/check-frame-axe.mjs` | `check:frame-axe` | axe over **every frame** at desktop 1440×900 + mobile-sm 375×667 | **Preview-chrome: yes.** Component/demo: allowlisted (regression = fail) |
10
+ | `scripts/frame-geometry.mjs` | `check:frame-geometry` | overflow + clipped-control sweep across **320·375·390·768·1024·1280·1440·1920** | Allowlisted (regression = fail) |
11
+ | `scripts/frame-coverage.mjs` | `check:frame-coverage` | component-inventory ↔ frame ↔ contract-axis cross-check (browser-free) | Report-only (`--strict` fails on any zero-frame component) |
12
+
13
+ `check:frame-geometry` works identically for responsive overflow (see its own header comment); `check:frame-coverage` is covered briefly at the end and fully in [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
14
+
15
+ ### What geometry counts as a "clipped control"
16
+
17
+ A control the **user cannot bring into the frame** — not merely one whose box currently sticks out. Everything inside a deliberately scrollable surface sticks out by design (a `DataTable`'s `.ui-data-table-scroll` at 320px, a `FilterBar overflow="scroll"` strip, a tabs list), and those controls are reachable: a control entirely out of the scrollport is scrolled fully in the moment it takes keyboard focus, and a partially visible one keeps its focus ring on screen (Chromium's `CenterIfNeeded` focus alignment does not move a partially visible target — browser behaviour, and still inside 2.4.7 / 2.4.11 AA). So an out-of-frame focusable is re-probed: the sweep scrolls **only** its user-scrollable ancestors (computed `overflow: auto|scroll|overlay` that actually has scroll room — never `hidden`/`clip`, which no user can scroll) and re-measures.
18
+
19
+ ## Why a per-frame gate, not the old hand-picked checks
20
+
21
+ - `check:contrast` (`scripts/check-contrast.mjs`) — a hand-picked set of ~6 pages checked for WCAG-AA text contrast. It never saw the other ~100 component frames. - `audit:examples` (`pnpm audit docs`) — a static source-regex linter. It can catch a hard-coded hex value but cannot see what actually renders: computed contrast, DOM landmark structure, focus order, or anything a Radix portal produces at runtime.
22
+
23
+ ## Chrome vs component: two independently-tracked scopes
24
+
25
+ `check:frame-axe` runs axe **twice** per frame/viewport:
26
+
27
+ - **Preview chrome** = the zoom / dimension **toolbar** (`.demo-block-toolbar`, `preview/src/demo-block.tsx`). We own this. It **must be 0** — a chrome violation fails the build.
28
+
29
+ ## The component allowlist — and how it shrinks to zero
30
+
31
+ `scripts/frame-axe.baseline.json` is the allowlist of **pre-existing** component violations, keyed `frame → [axe-rule-id]` — the **set of violation types** still firing on each frame (union across desktop + mobile). We key on rule _presence_, not exact node counts, because axe counts jitter run-to-run as a Radix portal (Dialog/Popover/Sheet) opens or closes — an exact-count gate would be flaky. `--update-baseline` unions **two passes** by default (`AXE_RUNS`) so an intermittently-mounted overlay is captured.
32
+
33
+ - **fails** if a frame gains a rule **not** in its baseline set (a genuinely new violation type);
34
+ - **passes** rule sets equal to or a subset of baseline, printing exactly what remains (nothing hidden);
35
+
36
+ The baseline can **only shrink**.
37
+
38
+ ```sh
39
+ git pull # get the merged component fixes
40
+ pnpm check:frame-axe --update-baseline # re-snapshot: rule sets only shrink
41
+ git add scripts/frame-axe.baseline.json && git commit -m "chore(a11y): tighten frame-axe baseline"
42
+ ```
43
+
44
+ There is **no blanket suppression**: a documented axe false positive would be an explicit per-rule entry with a comment, never a mute.
45
+
46
+ `0` chrome violations, `0` regressions. See [Remaining baseline debt](#remaining-baseline-debt--the-12-frames) below for exactly what's left and the path to zero.
47
+
48
+ ## Running it locally
49
+
50
+ ### Prerequisites
51
+
52
+ `playwright` and `@axe-core/playwright` are **optional peers** — `check:frame-axe` detects a missing/unbuilt peer and skips with a warning instead of failing the build (so a browser-less environment, e.g. a docs-only contributor, never gets blocked). To actually run it:
53
+
54
+ ```sh
55
+ pnpm install
56
+ pnpm exec playwright install chromium # local dev — downloads Playwright's managed Chromium
57
+ # CI uses: pnpm exec playwright install --with-deps chromium (adds OS-level deps)
58
+ ```
59
+
60
+ `frame-harness.mjs` looks for a Chromium binary at `PLAYWRIGHT_CHROMIUM_EXECUTABLE`, defaulting to a **CI-only** path (`/opt/pw-browsers/chromium-1228/chrome-linux64/chrome`). That path won't exist on a laptop — when it's absent the harness falls back to Playwright's own managed browser (from `playwright install` above), so a plain local checkout works without setting anything.
61
+
62
+ ### Commands
63
+
64
+ | Command | What it does |
65
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
66
+ | `pnpm check:frame-axe` | The gate: 1 pass over every frame × 2 viewports, compares to the baseline, exits non-zero on fail |
67
+ | `AXE_FRAMES_LIMIT=8 pnpm check:frame-axe` | Quick local smoke — only the first 8 frames (manifest order) |
68
+ | `pnpm check:frame-axe --format json` | Machine-readable result on stdout (same exit-code semantics) — no colored human report |
69
+ | `pnpm check:frame-axe --update-baseline` | Regenerate the allowlist (2-pass union by default) — see below |
70
+ | `AXE_RUNS=3 pnpm check:frame-axe --update-baseline` | More union passes, for a component with flaky portal timing |
71
+ | `pnpm check:frame-axe http://localhost:6008` | Point at an already-running preview instead of building+serving one |
72
+
73
+ If no preview server answers at the base URL (`http://localhost:6008` by default), the script builds one itself: `pnpm preview:build` then serves the static output with `vite preview` (`ensurePreviewServer` in `scripts/frame-harness.mjs`) — deterministic and stable under a long headless sweep, unlike the dev server's per-request recompilation. If a `pnpm preview` dev server (or a remote base) is already reachable, it's reused as-is and never rebuilt.
74
+
75
+ ### Reading the output
76
+
77
+ Terminal report (human mode) has two blocks:
78
+
79
+ ```
80
+ Preview-chrome axe (blocking — must be 0):
81
+ ✓ 0 chrome violations across all 107 frame(s).
82
+
83
+ Component/demo axe (allowlisted — baseline may only shrink):
84
+ 47 component violation node(s) remaining across 12 frame(s).
85
+ ✓ no new component violation types (rule-set within baseline).
86
+ ```
87
+
88
+ - A **red `✗ <rule>`** under chrome is always blocking.
89
+ - A **red `✗ N NEW violation-type(s) not in baseline`** under component means a frame gained a
90
+ rule the baseline didn't have — that's the actual CI failure mode for a component regression.
91
+ - A **yellow `↓ shrink hint`** means a baseline rule no longer fires anywhere it's listed — free
92
+ baseline tightening available via `--update-baseline`.
93
+
94
+ ### Evidence JSON
95
+
96
+ Every non-`--update-baseline` run writes `audit-evidence/frame-axe/frame-axe-results.json`
97
+ (gitignored — a CI artifact, uploaded by `frame-a11y.yml` under the `frame-axe-evidence` name).
98
+ Shape:
99
+
100
+ ```jsonc
101
+ {
102
+ "generatedAt": "2026-07-13T00:45:34.614Z",
103
+ "base": "http://localhost:6008",
104
+ "summary": {
105
+ "status": "ok", // "ok" | "fail"
106
+ "frames": 107,
107
+ "viewports": ["desktop", "mobile"],
108
+ "chromeViolations": 0, // total chrome violation NODE count
109
+ "componentViolations": 47, // total component violation NODE count (reporting only)
110
+ "componentRegressions": 0, // rules present now but not in the baseline
111
+ "infrastructureErrors": 0, // frames that failed to load / axe crashed
112
+ },
113
+ "chrome": {/* rule → { impact, nodes, frames: ["<frameId>@<viewport>", …] } */},
114
+ "componentCurrent": {/* frameId → viewport → ruleId → node count, this run */},
115
+ "regressions": [/* { frame, rule } — new rules, the actual gate failure */],
116
+ "shrinkHints": [/* { frame, rule } — baseline rules that no longer fire */],
117
+ "infraErrors": [/* { frame, viewport, message } */],
118
+ "results": [/* per-frame { id, viewports: { desktop: { chrome, component }, mobile: {…} } } */],
119
+ }
120
+ ```
121
+
122
+ Node counts are recorded for triage but are **not** what the gate compares — the gate compares
123
+ rule _presence_ per frame (`currentRules` vs `baseline.component[frame]`), because node counts
124
+ jitter with portal open/close timing (see "The component allowlist" above). The evidence file does
125
+ **not** carry the underlying axe `nodes[].any[].data` (e.g. the exact contrast ratio or CSS
126
+ selector) — `scan()` in `check-frame-axe.mjs` only keeps `{ id, impact, nodes: v.nodes.length }`.
127
+ To see the raw axe payload for a specific violation while triaging, run `@axe-core/playwright`
128
+ directly against `http://localhost:6008/frame/<id>` in a small throwaway script, or use the
129
+ Playwright Inspector / browser devtools with the `axe-core` extension against the same URL.
130
+
131
+ ### Regenerating / shrinking the baseline after a component fix
132
+
133
+ 1. Fix the root cause in the component (or the docs demo, if the violation is demo-content-only —
134
+ see the categories below for which is which).
135
+ 2. `pnpm check:frame-axe --update-baseline` — re-snapshots every frame's current rule set (2-pass
136
+ union). Frames that are now clean are **removed** from the JSON entirely; frames whose rule set
137
+ shrank keep only what still fires.
138
+ 3. `git diff scripts/frame-axe.baseline.json` — confirm the diff is a **strict shrink** (no rule
139
+ should reappear on a frame that previously didn't have it — if one does, you introduced a new
140
+ violation, not fixed an old one).
141
+ 4. Commit the baseline alongside the fix in the same PR.
142
+
143
+ `AXE_FRAMES_LIMIT` + `--update-baseline` together only touch the frames actually run — the merge
144
+ logic in `check-frame-axe.mjs` overwrites `runFrameIds` and leaves every other frame's baseline
145
+ entry untouched, so a scoped re-snapshot (e.g. after fixing just `Select`) never silently drops
146
+ unrelated frames from the allowlist.
147
+
148
+ ## Violation categories & how each was root-caused (reference)
149
+
150
+ The original #157 audit's 101-frame baseline collapsed to 12 through structural fixes, not
151
+ whack-a-mole per-frame patches. Each category below is a pattern — recognize it the next time a
152
+ new frame trips the same rule.
153
+
154
+ | Category | axe rule(s) | Root cause | Fix pattern |
155
+ | ------------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
+ | **Heading order** | `heading-order` | Every docs demo nests `PageContainer`'s `<h1>` directly around sibling `<Card>` sections whose `CardTitle` defaulted to `level={3}` (`<h3>`) — skipping `<h2>` (91 frames). | AST codemod bumped every **top-level** `CardTitle` to `level={2}`; nested Card-in-Card titles correctly kept the `h3` default (`CardTitle`'s own jsdoc already documents this contract). Commit `030e82a`. |
157
+ | **Landmark — main** | `landmark-one-main`, `landmark-no-duplicate-main`, `landmark-main-is-top-level` | `preview/src/frame-main.tsx` hardcoded a `<main>` around every frame. A story that renders its own page shell (`AppShell`/`AuthShell`, which already emit their own `<main>`) got a nested/duplicate main. Story metadata can't disambiguate — every catalog entry defaults to `layout: "fullscreen"` regardless of content. | `FrameLandmark` (`preview/src/frame-main.tsx`) detects it **after mount**: `useLayoutEffect` checks `node.querySelector('main, [role="main"]')` — stays a plain `<div>` if the subtree already owns a main, otherwise renders the real `<main>` **tag** itself (not `role="main"` — several axe landmark checks walk ancestors by native tag name, not computed role). Commit `030e82a`, `281c8d4`. |
158
+ | **Landmark — banner (chrome)** | `landmark-no-duplicate-banner` | The demo toolbar was a candidate `<header>`, which resolves to an implicit `banner` landmark whenever the rendered story also provides its own (`AppShell` demos). | `demo-block.tsx`'s toolbar is a plain `<div className="demo-block-toolbar">`, never a `<header>` — it's preview chrome, not page banner content (see the comment right above it in the file). |
159
+ | **Landmark — uniqueness** | `landmark-unique` | `Pagination`, `Breadcrumb` (standalone + `PageContainer`'s built-in slot), and `Carousel` each hardcoded ONE localized `aria-label` with no override. Fine for a real screen (one instance); the component-gallery docs stack several instances of the same component on one page, so same-role + same-name landmarks become indistinguishable in a screen-reader landmark list (WCAG 2.4.1 / 1.3.1). | Added an `aria-label` override prop (`PaginationProp`, `BreadcrumbProps`, `PageContainer`'s `breadcrumbAriaLabel`); every repeated docs instance now passes a distinguishing label. Also fixed `PageContainer`'s breadcrumb slot to route its label through `t()` instead of a hardcoded English `"Breadcrumb"` literal (an i18n-contract bug found in the same pass). Commit `741ccf5`. |
160
+ | **Scrollable region focusable** | `scrollable-region-focusable` | An `overflow: auto`/`overflow-x: auto` container was in the tab order for mouse users but unreachable by keyboard (WCAG 2.1.1). Hit three places: `.demo-block-canvas`/`.demo-block-frame` (the preview harness's own scroll wrappers), and the `Table` primitive's/`DataTable`'s/`ScrollArea`'s overflow wrappers. | Add `tabIndex={0}` to the scrolling element — **no `role="region"`**: a landmark role there would break `landmark-*-is-top-level` for any story that renders its own regions (e.g. `AppShell` demos). See the inline comments in `demo-block.tsx` (`.demo-block-canvas`) and `src/components/data-display/table.tsx`. Commits `030e82a`, `8675570`. |
161
+ | **List semantics** | `listitem` | `docs/data-display/list-row.tsx` rendered two `<ListRow as="li">` siblings directly inside `CardContent` (a plain `<div>`) — axe requires an `<li>`'s parent to actually be a list container (`ul`/`ol`/`role="list"`). `ListRow`'s own jsdoc already documents `as="li"` for "when the rows are a semantic list" — the demo just hadn't supplied the wrapping list it implied. | Wrap the `<li>` rows in a real `<ul>` in the demo. Commit `30d22f1`. |
162
+ | **Accessible name — button** | `button-name` | A `<button>` (native or a `SelectTrigger`) with no discernible text (WCAG 4.1.2). | Case-by-case: give the control real text, an `aria-label`, or route it through `FormField` (see below). Remaining `button-name` entries all trace to the `SelectTrigger`/`DataSelect` prop-forwarding gap fixed by #177 — see [Remaining baseline debt](#remaining-baseline-debt--the-12-frames). |
163
+ | **Accessible name — form control / label** | (implicit via `FormField` wiring, WCAG 1.3.1/4.1.2) | A control inside `FormField` was named only via `aria-labelledby`, which some AT/axe paths don't resolve as reliably as a direct name. | `FormField` (`src/components/data-entry/form-field.tsx`) now **also** injects a redundant `aria-label` mirroring the visible label text (when it's plain text), alongside its existing `aria-labelledby` wiring — belt-and-suspenders, only when the child hasn't already set its own `aria-label`. Commit `8675570`. |
164
+ | **Accessible name — dialog** | `aria-dialog-name` / `aria-hidden-focus` | A `role="dialog"` (Popover in `modal` mode) needs a discernible name; a modal Popover **shown open at rest** in a demo (`Popover modal defaultOpen`) also puts Radix's `aria-hide` on everything outside the portal, which can hide the page's own `<main>`/`<h1>` from the accessibility tree scan. | Label the titled/modal Popover demos with `aria-label` on `PopoverContent`. The `aria-hidden-focus`/`landmark-one-main`/`page-has-heading-one` combination on an **open-at-rest modal demo** is the deferred exception — see below. Commit `8675570`. |
165
+ | **Color contrast** | `color-contrast` | `Calendar`'s selected-day ghost `<button>` kept dark text on the blue fill through hover/focus (insufficient contrast on the selected date). | Force `text-primary-foreground` on the day `<button>` through hover/focus states. Commit `8675570`. Some `color-contrast` entries remain on `text-muted-foreground` small text — tracked, not yet fixed (see below and [color-extensibility.md](./roadmap/color-extensibility.md)). |
166
+ | **Stray heading from empty state** | `heading-order` | `DataTable`'s built-in empty state rendered its "no rows" message as an `<h3>` (`EmptyState`'s default `titleAs`), injecting a heading into the outline for a state message, not a section. | `DataTable`'s empty state now renders `titleAs="p"` (plain text) — `EmptyState`/`CardTitle` both expose `titleLevel`/`titleAs` (`level`/`as` for `CardTitle`) precisely so callers can pick outline position without changing visual size. Commit `8675570`. |
167
+
168
+ ## Remaining baseline debt — the 12 frames
169
+
170
+ As of `frame-axe.baseline.json` (`generatedAt: 2026-07-13`), 12 frames still carry an allowlisted
171
+ component violation. Grouped by root cause (per the tracking note in commit `8675570`):
172
+
173
+ | Frame(s) | Rule(s) | Why it's still open | Path to zero |
174
+ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
175
+ | `data-entry-select`, `data-entry-select-matrix`, `feedback-sheet`, `navigation-toolbar` | `button-name` (+ `feedback-sheet` also `color-contrast`) | All four demos use `Select`/`SelectTrigger`. `#177` (merged, commit `1ef05be`) shipped the `DataSelect` prop/aria-forwarding + `SelectTrigger showIndicator` fix this was tracked against, but the axe baseline was **not re-verified/re-snapshotted** against `main` after that PR merged. | Run `pnpm check:frame-axe --update-baseline` on current `main`; if `#177` closed the underlying gap, these 4 entries drop out automatically. If any remain, re-triage against the current `SelectTrigger`/`FormField` DOM. |
176
+ | `navigation-tabs` | `color-contrast` | `#176` (merged, commit `78c1c3b`) fixed `Tabs`' scrollable-overflow + disabled-fallback selection; the `color-contrast` entry here was tracked against that same PR but likewise **not re-verified** since. | `pnpm check:frame-axe --update-baseline` after confirming the fix; re-triage if it persists. |
177
+ | `data-entry-transfer`, `feedback-sheet` | `color-contrast` | Both demos use `text-muted-foreground` on small text (`transfer.tsx:83,101,121`, `sheet.tsx:226`). This is the same **default-theme contrast tradeoff** already tracked in [`docs/roadmap/color-extensibility.md`](./roadmap/color-extensibility.md) — resolving it is a brand-colour decision (darken the token or add a dedicated `--text-muted` variant), deliberately deferred pre-release rather than shipped as an unflagged palette shift. | Land the token decision in `color-extensibility.md`, then re-run `--update-baseline`. |
178
+ | `data-entry-calendar` | `landmark-unique` | `Calendar` wraps `react-day-picker` (`^10.0.1`), which renders its **own internal `<nav>`** for month navigation inside every `DayPicker` instance. The docs demo renders 4 `Calendar` instances on one page (each with a distinguishing container `aria-label`, but the _nested_ `nav` isn't currently overridable through this wrapper's `classNames` config) — 4 identical unnamed `nav` landmarks on one page. | Needs either upstream `react-day-picker` support for a per-instance nav label, or wrapping its nav slot with a custom, labelled replacement in `src/components/data-entry/calendar.tsx`. |
179
+ | `layout-split-pane` | `landmark-unique` | `SplitPane` (`src/components/layout/split-pane.tsx`) renders a bare `<aside className="ui-split-pane-aside">` with **no `aria-label` prop at all**. `docs/layout/split-pane.tsx` renders 4 `SplitPane` instances on one page → 4 identical unnamed `complementary` landmarks. | Add an `aria-label`/`asideAriaLabel` override prop to `SplitPane`, same pattern as `Pagination`/`Breadcrumb` (commit `741ccf5`), then give each docs instance a distinguishing label. |
180
+ | `data-display-popover` | `aria-hidden-focus`, `landmark-one-main`, `page-has-heading-one` | `docs/data-display/popover.tsx` (line ~198) renders `<Popover modal defaultOpen>` — a modal Popover **shown open at rest** so the demo illustrates the modal behavior without a click. Radix's modal mode `aria-hide`s everything outside the portal while open, which hides the frame's own `<main>`/`<h1>` from axe's accessibility-tree scan (and leaves focusable content behind the hidden boundary). | Either stop shipping this specific demo `defaultOpen` (illustrate modal-ness a different way, e.g. a screenshot/GIF or a "click to open" note) or accept it as a **known, deliberate** demo-only artifact of showing a Radix modal-trap mid-interaction and add a per-rule documented exception (not a blanket mute) once the maintainers decide which. |
181
+ | `layout-resizable-panel` | `scrollable-region-focusable` | `docs/layout/resizable-panel.tsx:72` — a demo panel with an inline `style={{ overflowY: "auto" }}`, added directly in the doc content, not through a primitive that already carries the `tabIndex={0}` fix. | Add `tabIndex={0}` to that panel's `<div>` in the docs demo (same fix pattern as `.demo-block-canvas`/`Table`, just not yet applied to this hand-rolled demo wrapper). |
182
+ | `navigation-breadcrumb` | `scrollable-region-focusable` | `docs/navigation/breadcrumb.tsx:89,95` — two `<pre className="overflow-x-auto">` code-snippet blocks rendered directly in the demo body (not the footer "View code" panel, which is disabled in frame mode). | Add `tabIndex={0}` to the `<pre>` elements in the docs demo. |
183
+ | `navigation-pagination` | `scrollable-region-focusable` | This one is a **real component gap**, not docs-only: `.ui-pagination-list` (`src/styles/layout.css`, the `#153`/`#165` no-wrap pagination CSS) sets `overflow-x: auto` so the page-number strip scrolls instead of wrapping to a second line — but `PaginationContent`'s `<ul>` (`src/components/navigation/pagination.tsx`) never received a `tabIndex={0}` to match. | Add `tabIndex={0}` to `PaginationContent`'s `<ul>` in `pagination.tsx` (mirrors the `Table`/`ScrollArea` fix), then `--update-baseline`. |
184
+
185
+ Overall rule histogram across the 12 frames: `button-name` ×4, `color-contrast` ×3,
186
+ `scrollable-region-focusable` ×3, `landmark-unique` ×2, `aria-hidden-focus` ×1,
187
+ `landmark-one-main` ×1, `page-has-heading-one` ×1 (the last three all on the same
188
+ `data-display-popover` frame).
189
+
190
+ **Fastest path to a fully green baseline:** re-run `pnpm check:frame-axe --update-baseline` on
191
+ current `main` first (it's plausible `#176`/`#177` already closed 5 of the 12 entries and nobody's
192
+ re-snapshotted since) — then tackle the remaining `SplitPane`/`Calendar` landmark props, the two
193
+ docs-only `tabIndex` additions, the one real `pagination.tsx` `tabIndex` fix, the muted-foreground
194
+ token decision, and finally decide the modal-Popover-at-rest exception.
195
+
196
+ ## Infra frame modes (issue #163)
197
+
198
+ Any `/frame/<id>` accepts query params (implemented in `preview/src/frame-main.tsx`) so a route can
199
+ be exercised across contract axes without editing per-component example content:
200
+
201
+ | Param | Values | Effect |
202
+ | --------- | --------------------------------------- | --------------------------------------------------------- |
203
+ | `dir` | `ltr` \| `rtl` | flips logical CSS for the whole subtree (global RTL mode) |
204
+ | `density` | `compact` \| `default` \| `comfortable` | drives `AppProvider` density |
205
+ | `theme` | `light` \| `dark` | drives `AppProvider` theme |
206
+ | `locale` | `ja` \| `vi` \| `en` \| `ar` … | drives `AppProvider` locale (BCP-47) |
207
+
208
+ Example: `/frame/data-entry-select?dir=rtl&density=compact&theme=dark`.
209
+
210
+ `check:frame-axe` itself always runs the default chrome (`ltr`/`default`/`light`/`ja`) — these
211
+ params are for manual/exploratory testing (e.g. checking a component under RTL + dark in a real
212
+ browser), not currently swept by the automated gate.
213
+
214
+ ## Coverage tracker
215
+
216
+ `check:frame-coverage` reads the public inventory (`mcp/src/data/components.ts`), the frames that
217
+ exist (`docs/` tsx), and the declared ledger (`preview/frame-coverage.ledger.json`), then emits
218
+ `docs/FRAME-COVERAGE-REPORT.md` + `audit-evidence/frame-coverage/coverage.json`. Every contract axis
219
+ is `covered` / `N/A:<reason>` / **`UNTESTED`** — a missing axis is never a silent pass. Those axes
220
+ are rolled up from the 14 contract dimensions the ledger tracks per export.
221
+
222
+ `check:frame-coverage-ledger` is the enforcing gate: it regenerates the ledger from the real public
223
+ surface, recomputes every dimension from the evidence, and fails on regression (coverage falling, a
224
+ new export with no frame, a weakened viewport matrix, a growing geometry/axe baseline). Dimension
225
+ totals climb toward full coverage (the #163 exit criterion). See
226
+ [FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
227
+ [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).
228
+
229
+ ## Adding a new component/frame without regressing the gate
230
+
231
+ Before opening a PR that adds or meaningfully changes a `/frame/**` example:
232
+
233
+ 1. **Give every top-level `Card` a `CardTitle level={2}`** when the frame sits directly under a
234
+ `PageContainer` (`<h1>`) — nested Card-in-Card titles keep the `h3` default. Skipping this
235
+ reintroduces `heading-order`.
236
+ 2. **Don't hand-roll a scroll container.** If a demo needs `overflow: auto`/`overflow-x: auto`,
237
+ add `tabIndex={0}` to the scrolling element and do **not** add `role="region"` (that turns it
238
+ into a landmark and can trip `landmark-*-is-top-level` for stories with their own regions).
239
+ Prefer reusing `Table`/`DataTable`/`ScrollArea`, which already carry this.
240
+ 3. **Stacking more than one instance of a landmark-owning component** (`Pagination`, `Breadcrumb`,
241
+ `SplitPane`'s `aside`, custom `<nav>`/`<aside>`/`<section aria-label>`) **on one page** — give
242
+ each instance a distinguishing `aria-label` (use the `aria-label`/`breadcrumbAriaLabel` override
243
+ props already on `Pagination`/`Breadcrumb`/`PageContainer` where they exist), or the frame trips
244
+ `landmark-unique`.
245
+ 4. **`<li>` needs a real list parent.** `ListRow as="li"` (or any bare `<li>`) must be inside a
246
+ `<ul>`/`<ol>`/`role="list"` container, never a plain `<div>`.
247
+ 5. **Every interactive control needs a discernible name.** Route form controls through `FormField`
248
+ (it injects both `aria-labelledby` and a redundant `aria-label`); give icon-only buttons/triggers
249
+ an explicit `aria-label`.
250
+ 6. **A `role="dialog"` (Popover `modal`, custom dialogs) needs a name**, and avoid shipping a demo
251
+ with a **modal** dialog `defaultOpen`/open-at-rest unless you've checked what it hides from the
252
+ rest of the page (see the `data-display-popover` entry above) — prefer "click to open."
253
+ 7. **Run the gate before pushing:** `pnpm exec playwright install chromium` (once) then
254
+ `AXE_FRAMES_LIMIT=<n> pnpm check:frame-axe` scoped to your new/changed frame(s), or the full
255
+ `pnpm check:frame-axe` if you have time. A brand-new frame with **zero** violations needs no
256
+ baseline entry at all — it's only added if you're knowingly landing a pre-existing violation
257
+ that a follow-up will fix (and even then, prefer fixing it before merge).
258
+ 8. If you do land a frame with a tracked violation, add it to `scripts/frame-axe.baseline.json` via
259
+ `--update-baseline` (never hand-edit the JSON) and note _why_ + the follow-up issue in the PR
260
+ description — the baseline file's own `note` field explains it may only shrink from here.
261
+ 9. Register the case you authored in `preview/frame-coverage.ledger.json` so
262
+ `check:frame-coverage-ledger` stops reporting that dimension `UNTESTED`. You **cannot** hand-write
263
+ a verdict — add an entry to the ledger's `cases` array (frame path + case heading + resolvable
264
+ evidence paths + reviewer + HTTPS review link + ISO timestamp) and run
265
+ `pnpm gen:frame-coverage-ledger`; the gate recomputes every cell from that evidence and rejects
266
+ any verdict it cannot reproduce. See [FRAME-COVERAGE-LEDGER.md](./FRAME-COVERAGE-LEDGER.md) and
267
+ [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md).