@kerfjs/ui 5.0.0-beta.7 → 5.0.0-beta.71

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 (723) hide show
  1. package/README.md +501 -139
  2. package/ai/application-ui-diagnostic-ids-v1.json +109 -0
  3. package/ai/application-ui-profile-sync.cjs +548 -0
  4. package/ai/application-ui-profile.d.mts +1 -0
  5. package/ai/application-ui-profile.d.ts +106 -0
  6. package/ai/application-ui-profile.defaults.json +96 -0
  7. package/ai/application-ui-profile.mjs +653 -0
  8. package/ai/application-ui-profile.schema.json +140 -0
  9. package/ai/catalog-authoring.json +36 -0
  10. package/ai/catalog-authoring.schema.json +109 -0
  11. package/ai/compile-time-contracts-v1.json +149 -0
  12. package/ai/compile-time-contracts-v1.schema.json +52 -0
  13. package/ai/component-catalog-extension.schema.json +104 -0
  14. package/ai/component-catalog.json +3905 -446
  15. package/ai/component-catalog.schema.json +94 -1
  16. package/ai/component-class-owners.cjs +83 -0
  17. package/ai/component-composition-extension.d.ts +7 -0
  18. package/ai/component-composition-extension.schema.json +7 -0
  19. package/ai/component-composition-rules.json +524 -0
  20. package/ai/component-composition.d.ts +129 -0
  21. package/ai/component-composition.json +7140 -0
  22. package/ai/component-composition.schema.json +408 -0
  23. package/ai/components/README.md +171 -0
  24. package/ai/components/application-tabs.md +65 -0
  25. package/ai/components/badge.md +72 -0
  26. package/ai/components/chip.md +72 -0
  27. package/ai/components/collapsible-panel.md +102 -0
  28. package/ai/components/content-item.md +93 -0
  29. package/ai/components/disclosure-arrow.md +77 -0
  30. package/ai/components/document-baseline.md +75 -0
  31. package/ai/components/empty-state.md +80 -0
  32. package/ai/components/feedback.md +71 -0
  33. package/ai/components/floating-toolbar.md +82 -0
  34. package/ai/components/foundation.md +65 -0
  35. package/ai/components/grid.md +98 -0
  36. package/ai/components/headers.md +63 -0
  37. package/ai/components/layout.md +75 -0
  38. package/ai/components/list-action-row.md +91 -0
  39. package/ai/components/list-header.md +89 -0
  40. package/ai/components/list-inset-control.md +74 -0
  41. package/ai/components/list-inset-text.md +72 -0
  42. package/ai/components/list-item.md +94 -0
  43. package/ai/components/list.md +105 -0
  44. package/ai/components/loading-spinner.md +73 -0
  45. package/ai/components/lucide-icon.md +78 -0
  46. package/ai/components/nav-stack.md +104 -0
  47. package/ai/components/pane.md +101 -0
  48. package/ai/components/popup-menu.md +171 -0
  49. package/ai/components/recipe-app-shell.md +57 -0
  50. package/ai/components/recipe-collapsible-sidebar.md +57 -0
  51. package/ai/components/recipe-compact-toolbar.md +57 -0
  52. package/ai/components/recipe-composer-form.md +57 -0
  53. package/ai/components/recipe-list-detail-dialog.md +57 -0
  54. package/ai/components/recipe-list-workspace-states.md +57 -0
  55. package/ai/components/recipe-loading-inspector.md +57 -0
  56. package/ai/components/recipe-navigation-sidebar.md +57 -0
  57. package/ai/components/recipe-navigation-stack.md +57 -0
  58. package/ai/components/recipe-workspace-header.md +57 -0
  59. package/ai/components/resize.md +106 -0
  60. package/ai/components/row.md +96 -0
  61. package/ai/components/segmented-control.md +82 -0
  62. package/ai/components/select.md +113 -0
  63. package/ai/components/skeleton.md +84 -0
  64. package/ai/components/spacer.md +86 -0
  65. package/ai/components/split-view.md +102 -0
  66. package/ai/components/state-banner.md +94 -0
  67. package/ai/components/sunken-panel.md +79 -0
  68. package/ai/components/surface-scaffold.md +78 -0
  69. package/ai/components/tab-bar.md +113 -0
  70. package/ai/components/tab-scaffold.md +94 -0
  71. package/ai/components/tabs.md +95 -0
  72. package/ai/components/text.md +85 -0
  73. package/ai/components/token-search-field.md +106 -0
  74. package/ai/components/toolbar-control-group.md +102 -0
  75. package/ai/components/toolbar-text.md +79 -0
  76. package/ai/components/toolbar.md +97 -0
  77. package/ai/components/value-table.md +73 -0
  78. package/ai/components/wa-accordion-item.md +55 -0
  79. package/ai/components/wa-accordion.md +65 -0
  80. package/ai/components/wa-animated-image.md +55 -0
  81. package/ai/components/wa-animation.md +55 -0
  82. package/ai/components/wa-avatar.md +55 -0
  83. package/ai/components/wa-badge.md +55 -0
  84. package/ai/components/wa-breadcrumb-item.md +53 -0
  85. package/ai/components/wa-breadcrumb.md +55 -0
  86. package/ai/components/wa-button-group.md +59 -0
  87. package/ai/components/wa-button.md +55 -0
  88. package/ai/components/wa-callout.md +58 -0
  89. package/ai/components/wa-card.md +59 -0
  90. package/ai/components/wa-carousel-item.md +53 -0
  91. package/ai/components/wa-carousel.md +55 -0
  92. package/ai/components/wa-checkbox-group.md +55 -0
  93. package/ai/components/wa-checkbox.md +55 -0
  94. package/ai/components/wa-color-picker.md +55 -0
  95. package/ai/components/wa-comparison.md +55 -0
  96. package/ai/components/wa-copy-button.md +55 -0
  97. package/ai/components/wa-details.md +61 -0
  98. package/ai/components/wa-dialog.md +59 -0
  99. package/ai/components/wa-divider.md +53 -0
  100. package/ai/components/wa-drawer.md +55 -0
  101. package/ai/components/wa-dropdown-item.md +60 -0
  102. package/ai/components/wa-dropdown.md +60 -0
  103. package/ai/components/wa-format-bytes.md +53 -0
  104. package/ai/components/wa-format-date.md +53 -0
  105. package/ai/components/wa-format-number.md +53 -0
  106. package/ai/components/wa-icon.md +58 -0
  107. package/ai/components/wa-include.md +53 -0
  108. package/ai/components/wa-input.md +59 -0
  109. package/ai/components/wa-intersection-observer.md +53 -0
  110. package/ai/components/wa-known-date.md +53 -0
  111. package/ai/components/wa-markdown.md +53 -0
  112. package/ai/components/wa-mutation-observer.md +53 -0
  113. package/ai/components/wa-number-input.md +55 -0
  114. package/ai/components/wa-option.md +59 -0
  115. package/ai/components/wa-otp-input.md +53 -0
  116. package/ai/components/wa-page.md +53 -0
  117. package/ai/components/wa-pagination.md +55 -0
  118. package/ai/components/wa-popover.md +55 -0
  119. package/ai/components/wa-popup.md +53 -0
  120. package/ai/components/wa-progress-bar.md +53 -0
  121. package/ai/components/wa-progress-ring.md +53 -0
  122. package/ai/components/wa-qr-code.md +53 -0
  123. package/ai/components/wa-radio-group.md +59 -0
  124. package/ai/components/wa-radio.md +55 -0
  125. package/ai/components/wa-random-content.md +53 -0
  126. package/ai/components/wa-rating.md +55 -0
  127. package/ai/components/wa-relative-time.md +53 -0
  128. package/ai/components/wa-resize-observer.md +53 -0
  129. package/ai/components/wa-scroller.md +53 -0
  130. package/ai/components/wa-select.md +63 -0
  131. package/ai/components/wa-skeleton.md +53 -0
  132. package/ai/components/wa-slider.md +55 -0
  133. package/ai/components/wa-spinner.md +58 -0
  134. package/ai/components/wa-split-panel.md +58 -0
  135. package/ai/components/wa-switch.md +53 -0
  136. package/ai/components/wa-tab-group.md +69 -0
  137. package/ai/components/wa-tab-panel.md +60 -0
  138. package/ai/components/wa-tab.md +59 -0
  139. package/ai/components/wa-tag.md +59 -0
  140. package/ai/components/wa-textarea.md +53 -0
  141. package/ai/components/wa-time-input.md +55 -0
  142. package/ai/components/wa-toast-item.md +60 -0
  143. package/ai/components/wa-toast.md +60 -0
  144. package/ai/components/wa-tooltip.md +55 -0
  145. package/ai/components/wa-tree-item.md +55 -0
  146. package/ai/components/wa-tree.md +55 -0
  147. package/ai/components/wa-zoomable-frame.md +55 -0
  148. package/ai/components/workbench.md +118 -0
  149. package/ai/public-api-signatures-v1.md +2748 -210
  150. package/ai/skill.md +286 -66
  151. package/ai/webawesome-jsx-signatures-v1.md +9 -2
  152. package/analyzer/cli.mjs +49 -0
  153. package/analyzer/component-ownership.d.mts +72 -0
  154. package/analyzer/component-ownership.mjs +401 -0
  155. package/analyzer/index.d.mts +53 -0
  156. package/analyzer/index.mjs +2307 -0
  157. package/analyzer/loud-pairs.d.mts +49 -0
  158. package/analyzer/loud-pairs.mjs +357 -0
  159. package/analyzer/report.schema.json +66 -0
  160. package/analyzer/selectors.d.mts +21 -0
  161. package/analyzer/selectors.mjs +212 -0
  162. package/catalog-projection-cli.mjs +96 -0
  163. package/catalog-projection.d.mts +55 -0
  164. package/catalog-projection.mjs +62 -0
  165. package/dist/app-tab.d.ts +39 -4
  166. package/dist/app-tab.js +2 -1
  167. package/dist/badge.d.ts +52 -0
  168. package/dist/badge.js +3 -0
  169. package/dist/badge.js.map +1 -0
  170. package/dist/browser/badge.js +3 -0
  171. package/dist/browser/catalog.js +26 -0
  172. package/dist/browser/chip.js +3 -0
  173. package/dist/browser/collapsible-panel.js +9 -0
  174. package/dist/browser/content-item.js +3 -0
  175. package/dist/browser/floating-toolbar.js +3 -0
  176. package/dist/browser/grid.js +3 -0
  177. package/dist/browser/list-action-row.js +5 -0
  178. package/dist/browser/{menu-header.js → list-header.js} +4 -2
  179. package/dist/browser/list-inset-control.js +3 -0
  180. package/dist/browser/list-inset-text.js +3 -0
  181. package/dist/browser/list-item.js +5 -0
  182. package/dist/browser/list.js +3 -0
  183. package/dist/browser/{panel-header.js → nav-stack.js} +5 -3
  184. package/dist/browser/pane.js +3 -0
  185. package/dist/browser/popup-menu.js +3 -0
  186. package/dist/browser/row.js +3 -0
  187. package/dist/browser/segmented-control.js +0 -1
  188. package/dist/browser/select.js +1 -0
  189. package/dist/browser/spacer.js +3 -0
  190. package/dist/browser/split-view.js +11 -0
  191. package/dist/browser/state-banner.js +1 -0
  192. package/dist/browser/sunken-panel.js +3 -0
  193. package/dist/browser/surface-scaffold.js +3 -0
  194. package/dist/browser/tab-scaffold.js +4 -0
  195. package/dist/browser/text.js +3 -0
  196. package/dist/browser/toolbar-control-group.js +1 -0
  197. package/dist/browser/workbench.js +14 -0
  198. package/dist/catalog-resources.d.ts +48 -0
  199. package/dist/catalog-resources.js +22 -0
  200. package/dist/catalog-resources.js.map +1 -0
  201. package/dist/catalog.d.ts +92 -0
  202. package/dist/catalog.js +503 -0
  203. package/dist/catalog.js.map +1 -0
  204. package/dist/chip.d.ts +35 -0
  205. package/dist/chip.js +3 -0
  206. package/dist/chip.js.map +1 -0
  207. package/dist/chunk-2G2ZJQYK.js +152 -0
  208. package/dist/chunk-2G2ZJQYK.js.map +1 -0
  209. package/dist/chunk-3BYFWTP3.js +108 -0
  210. package/dist/chunk-3BYFWTP3.js.map +1 -0
  211. package/dist/chunk-47CKKLBE.js +34 -0
  212. package/dist/chunk-47CKKLBE.js.map +1 -0
  213. package/dist/chunk-4SQK27MR.js +202 -0
  214. package/dist/chunk-4SQK27MR.js.map +1 -0
  215. package/dist/chunk-5I7QGQVT.js +24 -0
  216. package/dist/chunk-5I7QGQVT.js.map +1 -0
  217. package/dist/chunk-6DFDTWGG.js +288 -0
  218. package/dist/chunk-6DFDTWGG.js.map +1 -0
  219. package/dist/chunk-7OKX3EF6.js +135 -0
  220. package/dist/chunk-7OKX3EF6.js.map +1 -0
  221. package/dist/chunk-AHEG27M7.js +66 -0
  222. package/dist/chunk-AHEG27M7.js.map +1 -0
  223. package/dist/chunk-AYUHMAI6.js +48 -0
  224. package/dist/chunk-AYUHMAI6.js.map +1 -0
  225. package/dist/chunk-B2ZMR7NY.js +85 -0
  226. package/dist/chunk-B2ZMR7NY.js.map +1 -0
  227. package/dist/chunk-BFPBS5Y4.js +250 -0
  228. package/dist/chunk-BFPBS5Y4.js.map +1 -0
  229. package/dist/chunk-DB5ADJKF.js +775 -0
  230. package/dist/chunk-DB5ADJKF.js.map +1 -0
  231. package/dist/chunk-DC6CN2DS.js +123 -0
  232. package/dist/chunk-DC6CN2DS.js.map +1 -0
  233. package/dist/chunk-DG6G5EPF.js +35 -0
  234. package/dist/chunk-DG6G5EPF.js.map +1 -0
  235. package/dist/{chunk-56RJXPHG.js → chunk-G4S2QU6V.js} +7 -4
  236. package/dist/chunk-G4S2QU6V.js.map +1 -0
  237. package/dist/chunk-GLDWRCAM.js +49 -0
  238. package/dist/chunk-GLDWRCAM.js.map +1 -0
  239. package/dist/chunk-I3Q7KKYM.js +33 -0
  240. package/dist/chunk-I3Q7KKYM.js.map +1 -0
  241. package/dist/chunk-IA7TMD4Y.js +288 -0
  242. package/dist/chunk-IA7TMD4Y.js.map +1 -0
  243. package/dist/chunk-J3KGJ6P6.js +135 -0
  244. package/dist/chunk-J3KGJ6P6.js.map +1 -0
  245. package/dist/chunk-K47P7QZC.js +17 -0
  246. package/dist/chunk-K47P7QZC.js.map +1 -0
  247. package/dist/chunk-K67Y2JNF.js +222 -0
  248. package/dist/chunk-K67Y2JNF.js.map +1 -0
  249. package/dist/chunk-KCCEH7RF.js +492 -0
  250. package/dist/chunk-KCCEH7RF.js.map +1 -0
  251. package/dist/chunk-KLJKU6ZL.js +109 -0
  252. package/dist/chunk-KLJKU6ZL.js.map +1 -0
  253. package/dist/chunk-KVBRMEPM.js +44 -0
  254. package/dist/chunk-KVBRMEPM.js.map +1 -0
  255. package/dist/chunk-M6DEEBEF.js +156 -0
  256. package/dist/chunk-M6DEEBEF.js.map +1 -0
  257. package/dist/chunk-MEY33VWB.js +44 -0
  258. package/dist/chunk-MEY33VWB.js.map +1 -0
  259. package/dist/chunk-MP2GDBQZ.js +50 -0
  260. package/dist/chunk-MP2GDBQZ.js.map +1 -0
  261. package/dist/chunk-NC4CFOID.js +188 -0
  262. package/dist/chunk-NC4CFOID.js.map +1 -0
  263. package/dist/chunk-NCSWXG7G.js +80 -0
  264. package/dist/chunk-NCSWXG7G.js.map +1 -0
  265. package/dist/chunk-NN6GYOA2.js +47 -0
  266. package/dist/chunk-NN6GYOA2.js.map +1 -0
  267. package/dist/chunk-OXO2QJWV.js +55 -0
  268. package/dist/chunk-OXO2QJWV.js.map +1 -0
  269. package/dist/chunk-P7D3EMXM.js +52 -0
  270. package/dist/chunk-P7D3EMXM.js.map +1 -0
  271. package/dist/chunk-PSHUSWQH.js +18 -0
  272. package/dist/chunk-PSHUSWQH.js.map +1 -0
  273. package/dist/chunk-Q6YU3BT7.js +138 -0
  274. package/dist/chunk-Q6YU3BT7.js.map +1 -0
  275. package/dist/chunk-QJ5VFCW3.js +131 -0
  276. package/dist/chunk-QJ5VFCW3.js.map +1 -0
  277. package/dist/chunk-RGS4WX2M.js +44 -0
  278. package/dist/chunk-RGS4WX2M.js.map +1 -0
  279. package/dist/chunk-RNQNEENY.js +146 -0
  280. package/dist/chunk-RNQNEENY.js.map +1 -0
  281. package/dist/chunk-RVERAFFY.js +291 -0
  282. package/dist/chunk-RVERAFFY.js.map +1 -0
  283. package/dist/chunk-SLWCCHNP.js +491 -0
  284. package/dist/chunk-SLWCCHNP.js.map +1 -0
  285. package/dist/chunk-SMZGAPCU.js +70 -0
  286. package/dist/chunk-SMZGAPCU.js.map +1 -0
  287. package/dist/{chunk-7BR3DO2A.js → chunk-SRSJO5QE.js} +13 -4
  288. package/dist/chunk-SRSJO5QE.js.map +1 -0
  289. package/dist/chunk-TJK44RQC.js +452 -0
  290. package/dist/chunk-TJK44RQC.js.map +1 -0
  291. package/dist/chunk-UIDKY4UB.js +14 -0
  292. package/dist/chunk-UIDKY4UB.js.map +1 -0
  293. package/dist/chunk-V3DX3K65.js +43 -0
  294. package/dist/chunk-V3DX3K65.js.map +1 -0
  295. package/dist/chunk-VZOFZPSO.js +71 -0
  296. package/dist/chunk-VZOFZPSO.js.map +1 -0
  297. package/dist/chunk-W2AC6Q2A.js +83 -0
  298. package/dist/chunk-W2AC6Q2A.js.map +1 -0
  299. package/dist/chunk-WF75U3V4.js +33 -0
  300. package/dist/chunk-WF75U3V4.js.map +1 -0
  301. package/dist/chunk-WTSNV5RG.js +52 -0
  302. package/dist/chunk-WTSNV5RG.js.map +1 -0
  303. package/dist/{chunk-YGN7OMLT.js → chunk-X5QSICJ3.js} +8 -5
  304. package/dist/chunk-X5QSICJ3.js.map +1 -0
  305. package/dist/chunk-YDGNU2OD.js +61 -0
  306. package/dist/chunk-YDGNU2OD.js.map +1 -0
  307. package/dist/collapsible-panel.d.ts +133 -0
  308. package/dist/collapsible-panel.js +131 -0
  309. package/dist/collapsible-panel.js.map +1 -0
  310. package/dist/content-item.d.ts +81 -0
  311. package/dist/content-item.js +4 -0
  312. package/dist/content-item.js.map +1 -0
  313. package/dist/css-values.d.ts +107 -0
  314. package/dist/css-values.js +3 -0
  315. package/dist/css-values.js.map +1 -0
  316. package/dist/device-class.js +33 -5
  317. package/dist/device-class.js.map +1 -1
  318. package/dist/disclosure-arrow.d.ts +6 -1
  319. package/dist/disclosure-arrow.js +2 -1
  320. package/dist/empty-state.d.ts +5 -2
  321. package/dist/empty-state.js +2 -1
  322. package/dist/flex-alignment-4ms8ZbV8.d.ts +5 -0
  323. package/dist/floating-toolbar.d.ts +51 -0
  324. package/dist/floating-toolbar.js +3 -0
  325. package/dist/floating-toolbar.js.map +1 -0
  326. package/dist/grid.d.ts +50 -0
  327. package/dist/grid.js +5 -0
  328. package/dist/grid.js.map +1 -0
  329. package/dist/help-tags-register.d.ts +13 -0
  330. package/dist/help-tags-register.js +9 -0
  331. package/dist/help-tags-register.js.map +1 -0
  332. package/dist/index.d.ts +34 -14
  333. package/dist/index.js +43 -23
  334. package/dist/{menu-action-row.d.ts → list-action-row.d.ts} +21 -9
  335. package/dist/list-action-row.js +8 -0
  336. package/dist/list-action-row.js.map +1 -0
  337. package/dist/list-header.d.ts +99 -0
  338. package/dist/list-header.js +10 -0
  339. package/dist/list-header.js.map +1 -0
  340. package/dist/list-inset-control.d.ts +22 -0
  341. package/dist/list-inset-control.js +3 -0
  342. package/dist/list-inset-control.js.map +1 -0
  343. package/dist/list-inset-text.d.ts +30 -0
  344. package/dist/list-inset-text.js +4 -0
  345. package/dist/list-inset-text.js.map +1 -0
  346. package/dist/list-item.d.ts +62 -0
  347. package/dist/list-item.js +8 -0
  348. package/dist/list-item.js.map +1 -0
  349. package/dist/list.d.ts +63 -0
  350. package/dist/list.js +6 -0
  351. package/dist/list.js.map +1 -0
  352. package/dist/loading-spinner.d.ts +9 -2
  353. package/dist/loading-spinner.js +2 -1
  354. package/dist/lucide-icon.d.ts +9 -2
  355. package/dist/lucide-icon.js +2 -1
  356. package/dist/nav-stack.d.ts +66 -10
  357. package/dist/nav-stack.js +6 -1
  358. package/dist/pane.d.ts +108 -0
  359. package/dist/pane.js +4 -0
  360. package/dist/pane.js.map +1 -0
  361. package/dist/panel-toolbar-DDKh4uKd.d.ts +72 -0
  362. package/dist/popup-menu-register.d.ts +9 -0
  363. package/dist/popup-menu-register.js +244 -0
  364. package/dist/popup-menu-register.js.map +1 -0
  365. package/dist/popup-menu.d.ts +100 -0
  366. package/dist/popup-menu.js +3 -0
  367. package/dist/popup-menu.js.map +1 -0
  368. package/dist/resizable-region.d.ts +32 -3
  369. package/dist/resizable-region.js +1 -1
  370. package/dist/row.d.ts +48 -0
  371. package/dist/row.js +6 -0
  372. package/dist/row.js.map +1 -0
  373. package/dist/segmented-control.d.ts +12 -9
  374. package/dist/segmented-control.js +1 -2
  375. package/dist/select-register.d.ts +5 -1
  376. package/dist/select-register.js +243 -2
  377. package/dist/select-register.js.map +1 -1
  378. package/dist/select.d.ts +102 -8
  379. package/dist/select.js +3 -1
  380. package/dist/semantic-content-BbzjvSu9.d.ts +14 -0
  381. package/dist/sides-BPSWde0A.d.ts +4 -0
  382. package/dist/skeleton.d.ts +14 -7
  383. package/dist/skeleton.js +2 -1
  384. package/dist/spacer.d.ts +18 -0
  385. package/dist/spacer.js +4 -0
  386. package/dist/spacer.js.map +1 -0
  387. package/dist/split-view.d.ts +39 -7
  388. package/dist/split-view.js +120 -10
  389. package/dist/split-view.js.map +1 -1
  390. package/dist/state-banner.d.ts +18 -6
  391. package/dist/state-banner.js +3 -1
  392. package/dist/styles/app-tab.css +224 -0
  393. package/dist/styles/badge.css +118 -0
  394. package/dist/styles/catalog/components/catalog-example-stack.css +9 -0
  395. package/dist/styles/catalog/components/catalog-example.css +168 -0
  396. package/dist/styles/catalog/components/catalog-stage.css +55 -0
  397. package/dist/styles/catalog/components/catalog.css +12 -0
  398. package/dist/styles/catalog.css +5 -0
  399. package/dist/styles/chip.css +132 -0
  400. package/dist/styles/collapsible-panel.css +575 -0
  401. package/dist/styles/content-item.css +75 -0
  402. package/dist/styles/document.css +23 -0
  403. package/dist/styles/floating-toolbar.css +119 -0
  404. package/dist/styles/foundation.css +178 -40
  405. package/dist/styles/grid.css +35 -0
  406. package/dist/styles/layout.css +3 -41
  407. package/dist/styles/list-action-row.css +258 -0
  408. package/dist/styles/list-header.css +229 -0
  409. package/dist/styles/list-inset-control.css +30 -0
  410. package/dist/styles/list-inset-text.css +29 -0
  411. package/dist/styles/list-item.css +255 -0
  412. package/dist/styles/list.css +133 -0
  413. package/dist/styles/loading-spinner.css +4 -0
  414. package/dist/styles/lucide-icon.css +5 -0
  415. package/dist/styles/nav-stack.css +172 -47
  416. package/dist/styles/pane.css +263 -0
  417. package/dist/styles/popup-menu.css +285 -0
  418. package/dist/styles/resizable-region.css +438 -4
  419. package/dist/styles/row.css +111 -0
  420. package/dist/styles/segmented-control.css +36 -9
  421. package/dist/styles/select.css +337 -21
  422. package/dist/styles/skeleton.css +64 -3
  423. package/dist/styles/spacer.css +11 -0
  424. package/dist/styles/split-view.css +114 -3
  425. package/dist/styles/state-banner.css +94 -19
  426. package/dist/styles/styles.css +13 -4
  427. package/dist/styles/sunken-panel.css +29 -0
  428. package/dist/styles/surface-scaffold.css +56 -0
  429. package/dist/styles/tab-bar.css +147 -9
  430. package/dist/styles/tab-scaffold.css +126 -3
  431. package/dist/styles/text.css +112 -0
  432. package/dist/styles/token-search-field.css +155 -55
  433. package/dist/styles/toolbar-control-group.css +608 -29
  434. package/dist/styles/toolbar-text.css +101 -8
  435. package/dist/styles/toolbar.css +343 -12
  436. package/dist/styles/value-table.css +34 -4
  437. package/dist/styles/webawesome.css +365 -10
  438. package/dist/styles/workbench.css +980 -7
  439. package/dist/sunken-panel.d.ts +27 -0
  440. package/dist/sunken-panel.js +3 -0
  441. package/dist/sunken-panel.js.map +1 -0
  442. package/dist/surface-scaffold.d.ts +30 -0
  443. package/dist/surface-scaffold.js +3 -0
  444. package/dist/surface-scaffold.js.map +1 -0
  445. package/dist/tab-bar.d.ts +39 -6
  446. package/dist/tab-bar.js +1 -1
  447. package/dist/tab-scaffold.d.ts +57 -7
  448. package/dist/tab-scaffold.js +83 -9
  449. package/dist/tab-scaffold.js.map +1 -1
  450. package/dist/text.d.ts +56 -0
  451. package/dist/text.js +3 -0
  452. package/dist/text.js.map +1 -0
  453. package/dist/token-search-field-CVGwDn2y.d.ts +154 -0
  454. package/dist/token-search-field.d.ts +3 -43
  455. package/dist/token-search-field.js +2 -1
  456. package/dist/token-search-model.d.ts +3 -0
  457. package/dist/token-search-model.js +3 -0
  458. package/dist/token-search-model.js.map +1 -0
  459. package/dist/toolbar-control-group.d.ts +77 -7
  460. package/dist/toolbar-control-group.js +3 -1
  461. package/dist/toolbar-text.d.ts +35 -4
  462. package/dist/toolbar-text.js +2 -1
  463. package/dist/toolbar.d.ts +59 -7
  464. package/dist/toolbar.js +1 -1
  465. package/dist/types-BuHELdXR.d.ts +117 -0
  466. package/dist/value-table.d.ts +11 -3
  467. package/dist/value-table.js +2 -1
  468. package/dist/webawesome.d.ts +8 -1
  469. package/dist/wire-catalog.d.ts +82 -0
  470. package/dist/wire-catalog.js +327 -0
  471. package/dist/wire-catalog.js.map +1 -0
  472. package/dist/wire-content-items.d.ts +11 -0
  473. package/dist/wire-content-items.js +114 -0
  474. package/dist/wire-content-items.js.map +1 -0
  475. package/dist/wire-nav-stack.d.ts +4 -1
  476. package/dist/wire-nav-stack.js +147 -7
  477. package/dist/wire-nav-stack.js.map +1 -1
  478. package/dist/wire-resizable-regions.d.ts +2 -1
  479. package/dist/wire-resizable-regions.js +2 -1
  480. package/dist/wire-scroll-dividers.d.ts +72 -0
  481. package/dist/wire-scroll-dividers.js +3 -0
  482. package/dist/wire-scroll-dividers.js.map +1 -0
  483. package/dist/wire-sidebar.d.ts +63 -0
  484. package/dist/wire-sidebar.js +231 -0
  485. package/dist/wire-sidebar.js.map +1 -0
  486. package/dist/wire-tab-bars.d.ts +19 -2
  487. package/dist/wire-tab-bars.js +1 -1
  488. package/dist/wire-tab-scaffold.js +9 -4
  489. package/dist/wire-tab-scaffold.js.map +1 -1
  490. package/dist/wire-token-search-fields.d.ts +65 -3
  491. package/dist/wire-token-search-fields.js +2 -1
  492. package/dist/wire-workbench.d.ts +116 -0
  493. package/dist/wire-workbench.js +6 -0
  494. package/dist/wire-workbench.js.map +1 -0
  495. package/dist/workbench.d.ts +244 -9
  496. package/dist/workbench.js +17 -15
  497. package/dist/workbench.js.map +1 -1
  498. package/docs/accessibility.md +476 -24
  499. package/docs/app-layouts.md +106 -8
  500. package/docs/apple-hig.md +1 -0
  501. package/docs/catalog.md +483 -0
  502. package/docs/collapsible-panel.md +371 -0
  503. package/docs/component-contract.md +667 -56
  504. package/docs/component-integration.md +75 -0
  505. package/docs/component-selection.md +540 -95
  506. package/docs/design/templates/empty-state/filtered-dark.svg +1 -0
  507. package/docs/design/templates/empty-state/filtered.svg +1 -0
  508. package/docs/design/templates/empty-state/with-action-dark.svg +1 -0
  509. package/docs/design/templates/empty-state/with-action.svg +1 -0
  510. package/docs/design/templates/empty-state-dark.svg +1 -0
  511. package/docs/design/templates/empty-state.svg +1 -0
  512. package/docs/design/templates/list/compact-dark.svg +1 -0
  513. package/docs/design/templates/list/compact.svg +1 -0
  514. package/docs/design/templates/list/stack-dark.svg +1 -0
  515. package/docs/design/templates/list/stack.svg +1 -0
  516. package/docs/design/templates/list-action-row/default-dark.svg +1 -0
  517. package/docs/design/templates/list-action-row/default.svg +1 -0
  518. package/docs/design/templates/list-action-row/selected-dark.svg +1 -0
  519. package/docs/design/templates/list-action-row/selected.svg +1 -0
  520. package/docs/design/templates/list-action-row-dark.svg +1 -0
  521. package/docs/design/templates/list-action-row.svg +1 -0
  522. package/docs/design/templates/list-dark.svg +1 -0
  523. package/docs/design/templates/list-header/action-dark.svg +1 -0
  524. package/docs/design/templates/list-header/action.svg +1 -0
  525. package/docs/design/templates/list-header/count-dark.svg +1 -0
  526. package/docs/design/templates/list-header/count.svg +1 -0
  527. package/docs/design/templates/list-header/inline-action-dark.svg +1 -0
  528. package/docs/design/templates/list-header/inline-action.svg +1 -0
  529. package/docs/design/templates/list-header/toggle-collapsed-dark.svg +1 -0
  530. package/docs/design/templates/list-header/toggle-collapsed.svg +1 -0
  531. package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -0
  532. package/docs/design/templates/list-header/toggle-expanded.svg +1 -0
  533. package/docs/design/templates/list-header-dark.svg +1 -0
  534. package/docs/design/templates/list-header.svg +1 -0
  535. package/docs/design/templates/list-item/default-dark.svg +1 -0
  536. package/docs/design/templates/list-item/default.svg +1 -0
  537. package/docs/design/templates/list-item/multiline-dark.svg +1 -0
  538. package/docs/design/templates/list-item/multiline.svg +1 -0
  539. package/docs/design/templates/list-item/selected-dark.svg +1 -0
  540. package/docs/design/templates/list-item/selected.svg +1 -0
  541. package/docs/design/templates/list-item/trailing-dark.svg +1 -0
  542. package/docs/design/templates/list-item/trailing.svg +1 -0
  543. package/docs/design/templates/list-item-dark.svg +1 -0
  544. package/docs/design/templates/list-item.svg +1 -0
  545. package/docs/design/templates/list.svg +1 -0
  546. package/docs/design/templates/segmented-control/equal-dark.svg +1 -0
  547. package/docs/design/templates/segmented-control/equal.svg +1 -0
  548. package/docs/design/templates/segmented-control/pill-small-dark.svg +1 -0
  549. package/docs/design/templates/segmented-control/pill-small.svg +1 -0
  550. package/docs/design/templates/segmented-control-dark.svg +1 -0
  551. package/docs/design/templates/segmented-control.svg +1 -0
  552. package/docs/design/templates/skeleton/avatar-dark.svg +1 -0
  553. package/docs/design/templates/skeleton/avatar.svg +1 -0
  554. package/docs/design/templates/skeleton/block-dark.svg +1 -0
  555. package/docs/design/templates/skeleton/block.svg +1 -0
  556. package/docs/design/templates/skeleton/lines-dark.svg +1 -0
  557. package/docs/design/templates/skeleton/lines.svg +1 -0
  558. package/docs/design/templates/skeleton-dark.svg +1 -0
  559. package/docs/design/templates/skeleton.svg +1 -0
  560. package/docs/design/templates/state-banner/danger-dark.svg +1 -0
  561. package/docs/design/templates/state-banner/danger.svg +1 -0
  562. package/docs/design/templates/state-banner/info-badge-dark.svg +1 -0
  563. package/docs/design/templates/state-banner/info-badge.svg +1 -0
  564. package/docs/design/templates/state-banner/info-dark.svg +1 -0
  565. package/docs/design/templates/state-banner/info.svg +1 -0
  566. package/docs/design/templates/state-banner/success-dark.svg +1 -0
  567. package/docs/design/templates/state-banner/success.svg +1 -0
  568. package/docs/design/templates/state-banner/warning-dark.svg +1 -0
  569. package/docs/design/templates/state-banner/warning.svg +1 -0
  570. package/docs/design/templates/state-banner-dark.svg +1 -0
  571. package/docs/design/templates/state-banner.svg +1 -0
  572. package/docs/design/templates/tab-bar/workspace-tabs-dark.svg +1 -0
  573. package/docs/design/templates/tab-bar/workspace-tabs.svg +1 -0
  574. package/docs/design/templates/tab-bar-dark.svg +1 -0
  575. package/docs/design/templates/tab-bar.svg +1 -0
  576. package/docs/design/templates/token-search-field/empty-dark.svg +1 -0
  577. package/docs/design/templates/token-search-field/empty.svg +1 -0
  578. package/docs/design/templates/token-search-field/tokens-dark.svg +1 -0
  579. package/docs/design/templates/token-search-field/tokens.svg +1 -0
  580. package/docs/design/templates/token-search-field-dark.svg +1 -0
  581. package/docs/design/templates/token-search-field.svg +1 -0
  582. package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -0
  583. package/docs/design/templates/toolbar/title-and-actions.svg +1 -0
  584. package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
  585. package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
  586. package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
  587. package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
  588. package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
  589. package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
  590. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
  591. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
  592. package/docs/design/templates/toolbar-control-group-dark.svg +1 -0
  593. package/docs/design/templates/toolbar-control-group.svg +1 -0
  594. package/docs/design/templates/toolbar-dark.svg +1 -0
  595. package/docs/design/templates/toolbar-text/default-dark.svg +1 -0
  596. package/docs/design/templates/toolbar-text/default.svg +1 -0
  597. package/docs/design/templates/toolbar-text/large-dark.svg +1 -0
  598. package/docs/design/templates/toolbar-text/large.svg +1 -0
  599. package/docs/design/templates/toolbar-text/small-dark.svg +1 -0
  600. package/docs/design/templates/toolbar-text/small.svg +1 -0
  601. package/docs/design/templates/toolbar-text-dark.svg +1 -0
  602. package/docs/design/templates/toolbar-text.svg +1 -0
  603. package/docs/design/templates/toolbar.svg +1 -0
  604. package/docs/design/templates/value-table/details-dark.svg +1 -0
  605. package/docs/design/templates/value-table/details.svg +1 -0
  606. package/docs/design/templates/value-table-dark.svg +1 -0
  607. package/docs/design/templates/value-table.svg +1 -0
  608. package/docs/design/templates.md +138 -0
  609. package/docs/design-philosophy.md +48 -20
  610. package/docs/device-class.md +10 -9
  611. package/docs/document-baseline.md +38 -0
  612. package/docs/examples/application-ui-profile.json +60 -0
  613. package/docs/examples/command-palette-adapter.tsx +69 -17
  614. package/docs/examples/component-catalog-extension.json +52 -0
  615. package/docs/examples/component-composition-extension.json +80 -0
  616. package/docs/layout.md +534 -59
  617. package/docs/nav-stack.md +138 -14
  618. package/docs/recipes.md +137 -54
  619. package/docs/split-view.md +74 -6
  620. package/docs/sunken-panel.md +87 -0
  621. package/docs/surface-scaffold.md +28 -0
  622. package/docs/tab-scaffold.md +99 -10
  623. package/docs/type-contracts.md +134 -0
  624. package/docs/ui-analyzer.md +384 -0
  625. package/docs/ui-doctor.md +153 -0
  626. package/docs/ui-evaluator.md +139 -0
  627. package/docs/ux-demo.md +41 -16
  628. package/docs/webawesome-theme.md +218 -16
  629. package/docs/workbench.md +608 -12
  630. package/doctor/cli.mjs +120 -0
  631. package/doctor/config.schema.json +94 -0
  632. package/doctor/eslint-diagnostics.d.mts +24 -0
  633. package/doctor/eslint-diagnostics.mjs +24 -0
  634. package/doctor/index.d.mts +122 -0
  635. package/doctor/index.mjs +1364 -0
  636. package/doctor/report.schema.json +118 -0
  637. package/evaluator/cli.mjs +89 -0
  638. package/evaluator/index.d.mts +90 -0
  639. package/evaluator/index.mjs +1203 -0
  640. package/evaluator/report.schema.json +152 -0
  641. package/llms.txt +445 -48
  642. package/package.json +249 -37
  643. package/scripts/component-catalog-extension-validation.mjs +28 -0
  644. package/traversal-exclusions.mjs +27 -0
  645. package/ux-demo/recipes/app-shell.tsx +205 -17
  646. package/ux-demo/recipes/collapsible-sidebar.tsx +260 -0
  647. package/ux-demo/recipes/compact-toolbar.tsx +135 -14
  648. package/ux-demo/recipes/composer-form.tsx +133 -21
  649. package/ux-demo/recipes/list-detail-dialog.tsx +282 -0
  650. package/ux-demo/recipes/list-workspace-states.tsx +183 -11
  651. package/ux-demo/recipes/loaders.ts +15 -5
  652. package/ux-demo/recipes/loading-inspector.tsx +204 -0
  653. package/ux-demo/recipes/mount-recipe.ts +28 -10
  654. package/ux-demo/recipes/navigation-sidebar.tsx +139 -13
  655. package/ux-demo/recipes/navigation-stack.tsx +79 -24
  656. package/ux-demo/recipes/types.ts +23 -2
  657. package/ux-demo/recipes/workspace-header.tsx +86 -5
  658. package/dist/browser/menu-action-row.js +0 -4
  659. package/dist/browser/menu-item.js +0 -4
  660. package/dist/chunk-2RJBFNB6.js +0 -172
  661. package/dist/chunk-2RJBFNB6.js.map +0 -1
  662. package/dist/chunk-2Z5TWHKQ.js +0 -34
  663. package/dist/chunk-2Z5TWHKQ.js.map +0 -1
  664. package/dist/chunk-3YD6ZNRA.js +0 -34
  665. package/dist/chunk-3YD6ZNRA.js.map +0 -1
  666. package/dist/chunk-55DATV3A.js +0 -10
  667. package/dist/chunk-55DATV3A.js.map +0 -1
  668. package/dist/chunk-56RJXPHG.js.map +0 -1
  669. package/dist/chunk-7BR3DO2A.js.map +0 -1
  670. package/dist/chunk-7LDCTU7D.js +0 -134
  671. package/dist/chunk-7LDCTU7D.js.map +0 -1
  672. package/dist/chunk-AQNKJRFH.js +0 -79
  673. package/dist/chunk-AQNKJRFH.js.map +0 -1
  674. package/dist/chunk-B24C4A6T.js +0 -22
  675. package/dist/chunk-B24C4A6T.js.map +0 -1
  676. package/dist/chunk-DAFFDO33.js +0 -14
  677. package/dist/chunk-DAFFDO33.js.map +0 -1
  678. package/dist/chunk-H6URQGJI.js +0 -42
  679. package/dist/chunk-H6URQGJI.js.map +0 -1
  680. package/dist/chunk-I7INRJO4.js +0 -17
  681. package/dist/chunk-I7INRJO4.js.map +0 -1
  682. package/dist/chunk-KJYXOTG3.js +0 -10
  683. package/dist/chunk-KJYXOTG3.js.map +0 -1
  684. package/dist/chunk-LC3YSAZC.js +0 -39
  685. package/dist/chunk-LC3YSAZC.js.map +0 -1
  686. package/dist/chunk-M5JRI6MN.js +0 -27
  687. package/dist/chunk-M5JRI6MN.js.map +0 -1
  688. package/dist/chunk-MBR3TYNL.js +0 -14
  689. package/dist/chunk-MBR3TYNL.js.map +0 -1
  690. package/dist/chunk-MFKMHK5W.js +0 -10
  691. package/dist/chunk-MFKMHK5W.js.map +0 -1
  692. package/dist/chunk-MQIZAD4A.js +0 -227
  693. package/dist/chunk-MQIZAD4A.js.map +0 -1
  694. package/dist/chunk-N3SUXLLS.js +0 -23
  695. package/dist/chunk-N3SUXLLS.js.map +0 -1
  696. package/dist/chunk-PONXLV5U.js +0 -19
  697. package/dist/chunk-PONXLV5U.js.map +0 -1
  698. package/dist/chunk-TZNCCM5H.js +0 -20
  699. package/dist/chunk-TZNCCM5H.js.map +0 -1
  700. package/dist/chunk-W2FK7ZNY.js +0 -26
  701. package/dist/chunk-W2FK7ZNY.js.map +0 -1
  702. package/dist/chunk-WVPYVDXY.js +0 -15
  703. package/dist/chunk-WVPYVDXY.js.map +0 -1
  704. package/dist/chunk-YGN7OMLT.js.map +0 -1
  705. package/dist/chunk-YKUUZPED.js +0 -24
  706. package/dist/chunk-YKUUZPED.js.map +0 -1
  707. package/dist/menu-action-row.js +0 -5
  708. package/dist/menu-action-row.js.map +0 -1
  709. package/dist/menu-header.d.ts +0 -43
  710. package/dist/menu-header.js +0 -7
  711. package/dist/menu-header.js.map +0 -1
  712. package/dist/menu-item.d.ts +0 -33
  713. package/dist/menu-item.js +0 -5
  714. package/dist/menu-item.js.map +0 -1
  715. package/dist/panel-header.d.ts +0 -28
  716. package/dist/panel-header.js +0 -7
  717. package/dist/panel-header.js.map +0 -1
  718. package/dist/styles/menu-action-row.css +0 -160
  719. package/dist/styles/menu-header.css +0 -160
  720. package/dist/styles/menu-item.css +0 -123
  721. package/dist/styles/panel-header.css +0 -64
  722. package/ux-demo/recipes/master-detail-dialog.tsx +0 -45
  723. package/ux-demo/recipes/recipes.css +0 -355
@@ -0,0 +1,1203 @@
1
+ /* global CSS, Element, HTMLElement, Node, document, getComputedStyle, innerHeight, innerWidth, location, window */
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
5
+ import { dirname, relative, resolve } from 'node:path';
6
+ import process from 'node:process';
7
+
8
+ import { loadApplicationUiProfile } from '../ai/application-ui-profile.mjs';
9
+
10
+ export const UI_EVALUATION_SCHEMA_VERSION = 1;
11
+
12
+ export const UI_EVALUATION_RULES = Object.freeze({
13
+ 'KUI-B001': {
14
+ severity: 'error',
15
+ title: 'Browser context could not be evaluated',
16
+ },
17
+ 'KUI-B010': { severity: 'error', title: 'Horizontal page overflow' },
18
+ 'KUI-B011': { severity: 'error', title: 'Clipped interactive control' },
19
+ 'KUI-B012': { severity: 'error', title: 'Obscured interactive control' },
20
+ 'KUI-B020': { severity: 'error', title: 'Invalid positive tab order' },
21
+ 'KUI-B021': { severity: 'error', title: 'Focused control is not visible' },
22
+ 'KUI-B022': {
23
+ severity: 'error',
24
+ title: 'Custom interactive role is not keyboard reachable',
25
+ },
26
+ 'KUI-B023': { severity: 'error', title: 'Keyboard action did not operate' },
27
+ 'KUI-B030': { severity: 'error', title: 'Interactive control has no name' },
28
+ 'KUI-B040': { severity: 'error', title: 'Insufficient text contrast' },
29
+ 'KUI-B050': {
30
+ severity: 'error',
31
+ title: 'Interactive hit target is too small',
32
+ },
33
+ 'KUI-B060': { severity: 'error', title: 'Competing scroll owners' },
34
+ 'KUI-B070': { severity: 'error', title: 'Shared alignment edge drift' },
35
+ 'KUI-B080': { severity: 'error', title: 'Runtime geometry ownership drift' },
36
+ });
37
+
38
+ export const SUBJECTIVE_REVIEW_RUBRIC = Object.freeze([
39
+ {
40
+ id: 'hierarchy',
41
+ prompt:
42
+ '0 unclear scope; 1 usable with competing emphasis; 2 clear application, page, and section priority.',
43
+ },
44
+ {
45
+ id: 'rhythm',
46
+ prompt:
47
+ '0 arbitrary or doubled spacing; 1 isolated inconsistencies; 2 one coherent semantic spacing system.',
48
+ },
49
+ {
50
+ id: 'density',
51
+ prompt:
52
+ '0 clipped or undersized controls; 1 usable but uneven; 2 compact and consistently operable.',
53
+ },
54
+ {
55
+ id: 'alignment',
56
+ prompt:
57
+ '0 visibly broken columns or edges; 1 minor drift; 2 shared columns and intentional edges.',
58
+ },
59
+ {
60
+ id: 'scroll-ownership',
61
+ prompt:
62
+ '0 competing or misplaced scroll regions; 1 usable with an awkward boundary; 2 one obvious scroll owner per pane.',
63
+ },
64
+ {
65
+ id: 'aesthetic-fit',
66
+ prompt:
67
+ '0 visually conflicts with the product; 1 broadly coherent with isolated mismatches; 2 intentionally fits the product and Kerf language.',
68
+ },
69
+ ]);
70
+
71
+ const baseContexts = [
72
+ { id: 'wide-light', width: 1440, height: 900, zoom: 1, colorScheme: 'light' },
73
+ {
74
+ id: 'intermediate-light',
75
+ width: 900,
76
+ height: 768,
77
+ zoom: 1,
78
+ colorScheme: 'light',
79
+ },
80
+ {
81
+ id: 'narrow-light',
82
+ width: 390,
83
+ height: 844,
84
+ zoom: 1,
85
+ colorScheme: 'light',
86
+ },
87
+ {
88
+ id: 'zoom-200-light',
89
+ width: 720,
90
+ height: 450,
91
+ zoom: 2,
92
+ colorScheme: 'light',
93
+ },
94
+ { id: 'wide-dark', width: 1440, height: 900, zoom: 1, colorScheme: 'dark' },
95
+ {
96
+ id: 'wide-reduced-motion',
97
+ width: 1440,
98
+ height: 900,
99
+ zoom: 1,
100
+ colorScheme: 'light',
101
+ reducedMotion: 'reduce',
102
+ },
103
+ ];
104
+
105
+ export function buildEvaluationContexts(overrides) {
106
+ const contexts = overrides ?? baseContexts;
107
+ return contexts.map((context) => ({
108
+ reducedMotion: 'no-preference',
109
+ ...context,
110
+ }));
111
+ }
112
+
113
+ function parseColor(color) {
114
+ const values = color?.match(/[\d.]+/g)?.map(Number);
115
+ if (!values || values.length < 3) return null;
116
+ return [...values.slice(0, 3), values[3] ?? 1];
117
+ }
118
+
119
+ function composite([red, green, blue, alpha = 1], background) {
120
+ return [
121
+ red * alpha + background[0] * (1 - alpha),
122
+ green * alpha + background[1] * (1 - alpha),
123
+ blue * alpha + background[2] * (1 - alpha),
124
+ ];
125
+ }
126
+
127
+ function luminance(rgb) {
128
+ const channels = rgb.map((channel) => {
129
+ const value = channel / 255;
130
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
131
+ });
132
+ return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
133
+ }
134
+
135
+ export function contrastRatio(foreground, background) {
136
+ const foregroundColor = Array.isArray(foreground)
137
+ ? foreground
138
+ : parseColor(foreground);
139
+ const backgroundRgb = Array.isArray(background)
140
+ ? background
141
+ : parseColor(background);
142
+ if (!foregroundColor || !backgroundRgb) return null;
143
+ const renderedBackground = composite(backgroundRgb, [255, 255, 255]);
144
+ const foregroundRgb = composite(foregroundColor, renderedBackground);
145
+ const values = [luminance(foregroundRgb), luminance(renderedBackground)].sort(
146
+ (left, right) => right - left,
147
+ );
148
+ return (values[0] + 0.05) / (values[1] + 0.05);
149
+ }
150
+
151
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
152
+
153
+ function cancellationError() {
154
+ return Object.assign(new Error('Kerf UI evaluation was cancelled.'), {
155
+ name: 'AbortError',
156
+ });
157
+ }
158
+
159
+ function abortable(value, signal) {
160
+ if (!signal) return value;
161
+ if (signal.aborted) return Promise.reject(cancellationError());
162
+ return new Promise((resolvePromise, rejectPromise) => {
163
+ const abort = () => rejectPromise(cancellationError());
164
+ signal.addEventListener('abort', abort, { once: true });
165
+ Promise.resolve(value).then(
166
+ (result) => {
167
+ signal.removeEventListener('abort', abort);
168
+ resolvePromise(result);
169
+ },
170
+ (error) => {
171
+ signal.removeEventListener('abort', abort);
172
+ rejectPromise(error);
173
+ },
174
+ );
175
+ });
176
+ }
177
+
178
+ async function loadGeometryContracts(profileResult) {
179
+ const contracts = [];
180
+ for (const catalog of profileResult.profile?.catalogs ?? []) {
181
+ const owner = profileResult.provenance?.[`$catalogs.${catalog.package}`];
182
+ if (!owner) continue;
183
+ try {
184
+ const artifact = JSON.parse(
185
+ await readFile(
186
+ resolve(dirname(owner), catalog.composition.path),
187
+ 'utf8',
188
+ ),
189
+ );
190
+ for (const entry of artifact.entries ?? []) {
191
+ const className = entry.boundaries?.rootClass;
192
+ if (className)
193
+ contracts.push({
194
+ key: entry.key ?? `${artifact.package}:${entry.id}`,
195
+ className,
196
+ geometry: entry.layout?.geometry,
197
+ });
198
+ }
199
+ } catch {
200
+ // Profile loading already emits source-located catalog diagnostics.
201
+ }
202
+ }
203
+ return contracts;
204
+ }
205
+
206
+ function reportDiagnostic(code, context, item = {}) {
207
+ const rule = UI_EVALUATION_RULES[code];
208
+ const diagnostic = {
209
+ code,
210
+ severity: rule.severity,
211
+ context,
212
+ message: item.message ?? rule.title,
213
+ };
214
+ for (const property of ['repair', 'selector', 'evidence'])
215
+ if (item[property] !== undefined) diagnostic[property] = item[property];
216
+ return diagnostic;
217
+ }
218
+
219
+ function summarize(diagnostics) {
220
+ return {
221
+ errors: diagnostics.filter((item) => item.severity === 'error').length,
222
+ warnings: diagnostics.filter((item) => item.severity === 'warning').length,
223
+ passed: diagnostics.every((item) => item.severity !== 'error'),
224
+ };
225
+ }
226
+
227
+ async function inspectPage(page, geometryContracts) {
228
+ return page.evaluate((contracts) => {
229
+ const diagnostics = [];
230
+ const repair = {
231
+ overflow:
232
+ 'Remove fixed inline sizing or assign horizontal scrolling to one documented owner.',
233
+ clipping:
234
+ 'Move the control into the visible flow or make its documented scroll owner reachable.',
235
+ focus:
236
+ 'Use native controls in DOM order and retain a visible :focus-visible indicator.',
237
+ name: 'Provide visible text, aria-label, or a valid aria-labelledby relationship.',
238
+ contrast:
239
+ 'Use Kerf semantic foreground/background tokens with WCAG AA contrast.',
240
+ target: 'Keep interactive controls at least 44 by 44 CSS pixels.',
241
+ scroll: 'Keep exactly one scroll owner per pane.',
242
+ alignment:
243
+ 'Use the shared public layout/content classes so aligned edges use one inset.',
244
+ geometry:
245
+ 'Remove geometry from the component root and leave it on the catalog-declared owner.',
246
+ };
247
+ const visible = (element) => {
248
+ const style = getComputedStyle(element);
249
+ const rect = element.getBoundingClientRect();
250
+ return (
251
+ style.display !== 'none' &&
252
+ style.visibility !== 'hidden' &&
253
+ Number(style.opacity) !== 0 &&
254
+ rect.width > 0 &&
255
+ rect.height > 0
256
+ );
257
+ };
258
+ const selector = (element) => {
259
+ if (element.id) return `#${CSS.escape(element.id)}`;
260
+ const testId = element.getAttribute('data-testid');
261
+ if (testId) return `[data-testid="${CSS.escape(testId)}"]`;
262
+ const classes = [...element.classList].slice(0, 2);
263
+ return `${element.localName}${classes.map((name) => `.${CSS.escape(name)}`).join('')}`;
264
+ };
265
+ const widgetRoles = new Set([
266
+ 'button',
267
+ 'checkbox',
268
+ 'combobox',
269
+ 'gridcell',
270
+ 'link',
271
+ 'menuitem',
272
+ 'menuitemcheckbox',
273
+ 'menuitemradio',
274
+ 'option',
275
+ 'radio',
276
+ 'scrollbar',
277
+ 'searchbox',
278
+ 'slider',
279
+ 'spinbutton',
280
+ 'switch',
281
+ 'tab',
282
+ 'textbox',
283
+ 'treeitem',
284
+ ]);
285
+ const interactives = [
286
+ ...document.querySelectorAll(
287
+ 'button, a[href], input:not([type="hidden"]), select, textarea, summary, [role], [tabindex]',
288
+ ),
289
+ ].filter(
290
+ (element) =>
291
+ (element.matches(
292
+ 'button, a[href], input:not([type="hidden"]), select, textarea, summary, [tabindex]',
293
+ ) ||
294
+ widgetRoles.has(element.getAttribute('role'))) &&
295
+ visible(element) &&
296
+ !element.matches(':disabled,[aria-disabled="true"],[inert] *'),
297
+ );
298
+ const interactiveEvidence = [];
299
+
300
+ if (
301
+ document.documentElement.scrollWidth >
302
+ document.documentElement.clientWidth + 1
303
+ )
304
+ diagnostics.push({
305
+ code: 'KUI-B010',
306
+ selector: 'html',
307
+ message: `Page scrollWidth ${document.documentElement.scrollWidth}px exceeds clientWidth ${document.documentElement.clientWidth}px.`,
308
+ repair: repair.overflow,
309
+ evidence: {
310
+ scrollWidth: document.documentElement.scrollWidth,
311
+ clientWidth: document.documentElement.clientWidth,
312
+ },
313
+ });
314
+
315
+ const clippedBy = (element) => {
316
+ const rect = element.getBoundingClientRect();
317
+ let ancestor = element.parentElement;
318
+ while (ancestor) {
319
+ const style = getComputedStyle(ancestor);
320
+ if (
321
+ /hidden|clip/.test(
322
+ `${style.overflow} ${style.overflowX} ${style.overflowY}`,
323
+ )
324
+ ) {
325
+ const bounds = ancestor.getBoundingClientRect();
326
+ if (
327
+ rect.right > bounds.right + 1 ||
328
+ rect.left < bounds.left - 1 ||
329
+ rect.bottom > bounds.bottom + 1 ||
330
+ rect.top < bounds.top - 1
331
+ )
332
+ return selector(ancestor);
333
+ }
334
+ ancestor = ancestor.parentElement;
335
+ }
336
+ return null;
337
+ };
338
+
339
+ const hitExtent = (element, rect) => {
340
+ const root = element.getRootNode();
341
+ const probe = root.elementFromPoint ? root : document;
342
+ const inViewport = (x, y) =>
343
+ x >= 0 && y >= 0 && x < innerWidth && y < innerHeight;
344
+ const owns = (x, y) => {
345
+ const hit = probe.elementFromPoint(x, y);
346
+ return !!hit && (hit === element || element.contains(hit));
347
+ };
348
+ const x = rect.left + rect.width / 2;
349
+ const y = rect.top + rect.height / 2;
350
+ if (!inViewport(x, y) || !owns(x, y)) return null;
351
+ // Walk out in 1px steps (no further than the 44px goal needs), then
352
+ // refine the last step in quarter pixels; the reach is the last owned
353
+ // sample, so it never overstates the target. A walk stopped by
354
+ // the viewport edge cannot see the rest of the layer, so it is marked
355
+ // and mirrored from the opposite side (hit layers are symmetric insets).
356
+ const reach = (dx, dy) => {
357
+ const at = (distance) => [x + dx * distance, y + dy * distance];
358
+ let owned = 0;
359
+ while (owned < 24) {
360
+ const point = at(owned + 1);
361
+ if (!inViewport(...point)) return { distance: owned, clipped: true };
362
+ if (!owns(...point)) break;
363
+ owned += 1;
364
+ }
365
+ if (owned >= 24) return { distance: owned, clipped: false };
366
+ while (owns(...at(owned + 0.25))) owned += 0.25;
367
+ return { distance: owned, clipped: false };
368
+ };
369
+ const span = (first, second) =>
370
+ first.clipped && second.clipped
371
+ ? first.distance + second.distance
372
+ : first.clipped
373
+ ? 2 * second.distance
374
+ : second.clipped
375
+ ? 2 * first.distance
376
+ : first.distance + second.distance;
377
+ return {
378
+ width: span(reach(-1, 0), reach(1, 0)),
379
+ height: span(reach(0, -1), reach(0, 1)),
380
+ };
381
+ };
382
+
383
+ for (const element of interactives) {
384
+ const target = selector(element);
385
+ const rect = element.getBoundingClientRect();
386
+ const clippingOwner = clippedBy(element);
387
+ if (clippingOwner)
388
+ diagnostics.push({
389
+ code: 'KUI-B011',
390
+ selector: target,
391
+ message: `Interactive control is clipped by ${clippingOwner}.`,
392
+ repair: repair.clipping,
393
+ evidence: { clippingOwner },
394
+ });
395
+ const outsideViewport =
396
+ rect.right <= 0 ||
397
+ rect.bottom <= 0 ||
398
+ rect.left >= innerWidth ||
399
+ rect.top >= innerHeight;
400
+ if (outsideViewport)
401
+ diagnostics.push({
402
+ code: 'KUI-B011',
403
+ selector: target,
404
+ message: 'Interactive control is entirely outside the viewport.',
405
+ repair: repair.clipping,
406
+ evidence: {
407
+ viewport: { width: innerWidth, height: innerHeight },
408
+ rect: {
409
+ left: rect.left,
410
+ top: rect.top,
411
+ right: rect.right,
412
+ bottom: rect.bottom,
413
+ },
414
+ },
415
+ });
416
+ if (
417
+ rect.left >= 0 &&
418
+ rect.top >= 0 &&
419
+ rect.right <= innerWidth &&
420
+ rect.bottom <= innerHeight
421
+ ) {
422
+ const center = document.elementFromPoint(
423
+ rect.left + rect.width / 2,
424
+ rect.top + rect.height / 2,
425
+ );
426
+ if (center && center !== element && !element.contains(center))
427
+ diagnostics.push({
428
+ code: 'KUI-B012',
429
+ selector: target,
430
+ message: `Interactive control center is covered by ${selector(center)}.`,
431
+ repair: repair.clipping,
432
+ evidence: { coveringElement: selector(center) },
433
+ });
434
+ }
435
+ if (element.tabIndex > 0)
436
+ diagnostics.push({
437
+ code: 'KUI-B020',
438
+ selector: target,
439
+ message: `Positive tabindex=${element.tabIndex} overrides DOM focus order.`,
440
+ repair: repair.focus,
441
+ evidence: { tabIndex: element.tabIndex },
442
+ });
443
+ if (
444
+ widgetRoles.has(element.getAttribute('role')) &&
445
+ !element.matches(
446
+ 'button,a[href],input:not([type="hidden"]),select,textarea,summary',
447
+ ) &&
448
+ element.tabIndex < 0
449
+ )
450
+ diagnostics.push({
451
+ code: 'KUI-B022',
452
+ selector: target,
453
+ message:
454
+ 'Custom interactive role is not in sequential keyboard order.',
455
+ repair: repair.focus,
456
+ evidence: {
457
+ role: element.getAttribute('role'),
458
+ tabIndex: element.tabIndex,
459
+ },
460
+ });
461
+ const visibleText = (root) =>
462
+ [...root.childNodes]
463
+ .map((node) => {
464
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? '';
465
+ if (!(node instanceof Element)) return node.textContent ?? '';
466
+ if (
467
+ node.getAttribute('aria-hidden') === 'true' ||
468
+ getComputedStyle(node).display === 'none' ||
469
+ getComputedStyle(node).visibility === 'hidden'
470
+ )
471
+ return '';
472
+ return visibleText(node);
473
+ })
474
+ .join(' ')
475
+ .replace(/\s+/g, ' ')
476
+ .trim();
477
+ const referencedText = (ids) =>
478
+ ids
479
+ ?.split(/\s+/)
480
+ .map((id) => {
481
+ const reference = document.getElementById(id);
482
+ return reference ? visibleText(reference) : '';
483
+ })
484
+ .join(' ')
485
+ .trim();
486
+ const inputType = element.getAttribute('type')?.toLowerCase();
487
+ const nativeValueName =
488
+ element.localName === 'input' &&
489
+ ['button', 'submit', 'reset'].includes(inputType)
490
+ ? element.value ||
491
+ (inputType === 'submit'
492
+ ? 'Submit'
493
+ : inputType === 'reset'
494
+ ? 'Reset'
495
+ : '')
496
+ : '';
497
+ const name =
498
+ element.getAttribute('aria-label')?.trim() ||
499
+ referencedText(element.getAttribute('aria-labelledby')) ||
500
+ (element.labels
501
+ ? [...element.labels].map(visibleText).join(' ').trim()
502
+ : '') ||
503
+ (inputType === 'image' ? element.getAttribute('alt')?.trim() : '') ||
504
+ nativeValueName ||
505
+ element.getAttribute('alt')?.trim() ||
506
+ visibleText(element) ||
507
+ element.getAttribute('title')?.trim() ||
508
+ '';
509
+ const computed = getComputedStyle(element);
510
+ interactiveEvidence.push({
511
+ selector: target,
512
+ tag: element.localName,
513
+ role: element.getAttribute('role'),
514
+ name,
515
+ rect: {
516
+ x: rect.x,
517
+ y: rect.y,
518
+ width: rect.width,
519
+ height: rect.height,
520
+ },
521
+ style: {
522
+ color: computed.color,
523
+ backgroundColor: computed.backgroundColor,
524
+ outline: computed.outline,
525
+ boxShadow: computed.boxShadow,
526
+ overflowX: computed.overflowX,
527
+ overflowY: computed.overflowY,
528
+ },
529
+ });
530
+ if (!name)
531
+ diagnostics.push({
532
+ code: 'KUI-B030',
533
+ selector: target,
534
+ message: 'Interactive control has no computed text alternative.',
535
+ repair: repair.name,
536
+ });
537
+ if (
538
+ element.matches(
539
+ 'button,input:not([type="hidden"]),select,textarea,[role]',
540
+ ) &&
541
+ (rect.width < 43.5 || rect.height < 43.5)
542
+ ) {
543
+ // The border box understates a control whose pointer target is
544
+ // extended by a transparent hit layer (a positioned ::before/::after
545
+ // that receives pointer events). Hit-test outward from the center to
546
+ // measure the area the pointer can actually reach; a probe that finds
547
+ // no hit (off-screen, covered center) keeps the border box.
548
+ const hit = hitExtent(element, rect);
549
+ const width = Math.max(rect.width, hit?.width ?? 0);
550
+ const height = Math.max(rect.height, hit?.height ?? 0);
551
+ // Compact density is a deliberate trade-off: its rows are 36px tall,
552
+ // and a taller hit layer would reach into the neighboring row. Inside
553
+ // `[data-density="compact"]` a target still needs the full 44px width
554
+ // but only the 36px compact row height.
555
+ const compact = element.closest('[data-density="compact"]') !== null;
556
+ const requiredHeight = compact ? 36 : 44;
557
+ if (width < 43.5 || height < requiredHeight - 0.5)
558
+ diagnostics.push({
559
+ code: 'KUI-B050',
560
+ selector: target,
561
+ message: `Hit target is ${width.toFixed(1)}×${height.toFixed(1)}px; expected at least 44×${requiredHeight}px${compact ? ' (compact density)' : ''}.`,
562
+ repair: repair.target,
563
+ evidence: {
564
+ width,
565
+ height,
566
+ box: { width: rect.width, height: rect.height },
567
+ required: { width: 44, height: requiredHeight },
568
+ },
569
+ });
570
+ }
571
+ }
572
+
573
+ const parse = (value) => {
574
+ const values = value?.match(/[\d.]+/g)?.map(Number);
575
+ return values?.length >= 3
576
+ ? [...values.slice(0, 3), values[3] ?? 1]
577
+ : null;
578
+ };
579
+ const over = ([red, green, blue, alpha = 1], background) => [
580
+ red * alpha + background[0] * (1 - alpha),
581
+ green * alpha + background[1] * (1 - alpha),
582
+ blue * alpha + background[2] * (1 - alpha),
583
+ ];
584
+ const lum = (rgb) =>
585
+ rgb
586
+ .map((channel) => {
587
+ const value = channel / 255;
588
+ return value <= 0.03928
589
+ ? value / 12.92
590
+ : ((value + 0.055) / 1.055) ** 2.4;
591
+ })
592
+ .reduce(
593
+ (sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index],
594
+ 0,
595
+ );
596
+ for (const element of interactives) {
597
+ const style = getComputedStyle(element);
598
+ const foreground = parse(style.color);
599
+ const ancestors = [];
600
+ let backgroundElement = element;
601
+ while (backgroundElement) {
602
+ ancestors.push(backgroundElement);
603
+ backgroundElement = backgroundElement.parentElement;
604
+ }
605
+ const background = ancestors
606
+ .reverse()
607
+ .map((owner) => parse(getComputedStyle(owner).backgroundColor))
608
+ .filter(Boolean)
609
+ .reduce((result, color) => over(color, result), [255, 255, 255]);
610
+ if (foreground) {
611
+ const renderedForeground = over(foreground, background);
612
+ const values = [lum(renderedForeground), lum(background)].sort(
613
+ (a, b) => b - a,
614
+ );
615
+ const ratio = (values[0] + 0.05) / (values[1] + 0.05);
616
+ const threshold =
617
+ Number.parseFloat(style.fontSize) >= 24 ||
618
+ (Number.parseFloat(style.fontSize) >= 18.66 &&
619
+ Number(style.fontWeight) >= 700)
620
+ ? 3
621
+ : 4.5;
622
+ if (ratio + 0.01 < threshold)
623
+ diagnostics.push({
624
+ code: 'KUI-B040',
625
+ selector: selector(element),
626
+ message: `Text contrast ${ratio.toFixed(2)}:1 is below ${threshold}:1.`,
627
+ repair: repair.contrast,
628
+ evidence: {
629
+ foreground,
630
+ renderedForeground,
631
+ background,
632
+ ratio,
633
+ threshold,
634
+ },
635
+ });
636
+ }
637
+ }
638
+
639
+ const isScrollOwner = (element) => {
640
+ const style = getComputedStyle(element);
641
+ return (
642
+ /auto|scroll/.test(`${style.overflowX} ${style.overflowY}`) &&
643
+ (element.scrollHeight > element.clientHeight + 1 ||
644
+ element.scrollWidth > element.clientWidth + 1)
645
+ );
646
+ };
647
+ for (const scope of document.querySelectorAll(
648
+ '.kui-pane,[data-kui-scroll-scope]',
649
+ )) {
650
+ const owners = [scope, ...scope.querySelectorAll('*')].filter(
651
+ isScrollOwner,
652
+ );
653
+ if (owners.length > 1)
654
+ diagnostics.push({
655
+ code: 'KUI-B060',
656
+ selector: selector(scope),
657
+ message: `Scroll scope contains ${owners.length} active scroll owners.`,
658
+ repair: repair.scroll,
659
+ evidence: { owners: owners.map(selector) },
660
+ });
661
+ }
662
+
663
+ for (const group of document.querySelectorAll('[data-kui-align-group]')) {
664
+ const edges = [...group.querySelectorAll('[data-kui-align-edge]')].map(
665
+ (element) => ({ element, left: element.getBoundingClientRect().left }),
666
+ );
667
+ if (edges.length > 1) {
668
+ const drift =
669
+ Math.max(...edges.map(({ left }) => left)) -
670
+ Math.min(...edges.map(({ left }) => left));
671
+ if (drift > 1)
672
+ diagnostics.push({
673
+ code: 'KUI-B070',
674
+ selector: selector(group),
675
+ message: `Declared shared alignment edges drift by ${drift.toFixed(1)}px.`,
676
+ repair: repair.alignment,
677
+ evidence: {
678
+ edges: edges.map(({ element, left }) => ({
679
+ selector: selector(element),
680
+ left,
681
+ })),
682
+ },
683
+ });
684
+ }
685
+ }
686
+
687
+ const geometryValue = (style, dimension) => {
688
+ const properties =
689
+ dimension === 'margin'
690
+ ? ['marginTop', 'marginRight', 'marginBottom', 'marginLeft']
691
+ : dimension === 'padding'
692
+ ? ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
693
+ : [
694
+ 'borderTopWidth',
695
+ 'borderRightWidth',
696
+ 'borderBottomWidth',
697
+ 'borderLeftWidth',
698
+ ];
699
+ return properties.reduce(
700
+ (sum, property) => sum + Number.parseFloat(style[property] || '0'),
701
+ 0,
702
+ );
703
+ };
704
+ for (const contract of contracts) {
705
+ for (const element of document.getElementsByClassName(
706
+ contract.className,
707
+ )) {
708
+ if (!visible(element)) continue;
709
+ const style = getComputedStyle(element);
710
+ for (const dimension of ['margin', 'border', 'padding']) {
711
+ const owner = contract.geometry?.[dimension];
712
+ const value = geometryValue(style, dimension);
713
+ if (['none', 'parent', 'child'].includes(owner) && value > 0.5)
714
+ diagnostics.push({
715
+ code: 'KUI-B080',
716
+ selector: selector(element),
717
+ message: `${contract.key} declares ${dimension} owner ${owner}, but its root computes ${value.toFixed(1)}px total ${dimension}.`,
718
+ repair: repair.geometry,
719
+ evidence: {
720
+ key: contract.key,
721
+ className: contract.className,
722
+ dimension,
723
+ owner,
724
+ value,
725
+ },
726
+ });
727
+ }
728
+ }
729
+ }
730
+
731
+ return {
732
+ diagnostics,
733
+ evidence: {
734
+ url: location.href,
735
+ title: document.title,
736
+ interactiveCount: interactives.length,
737
+ elements: interactiveEvidence,
738
+ scrollWidth: document.documentElement.scrollWidth,
739
+ clientWidth: document.documentElement.clientWidth,
740
+ },
741
+ };
742
+ }, geometryContracts);
743
+ }
744
+
745
+ async function inspectFocus(page, limit = 40) {
746
+ const diagnostics = [];
747
+ const sequence = [];
748
+ await page.evaluate(() => {
749
+ const properties = [
750
+ 'outlineColor',
751
+ 'outlineStyle',
752
+ 'outlineWidth',
753
+ 'outlineOffset',
754
+ 'boxShadow',
755
+ 'backgroundColor',
756
+ 'color',
757
+ 'borderTopColor',
758
+ 'borderRightColor',
759
+ 'borderBottomColor',
760
+ 'borderLeftColor',
761
+ 'borderTopWidth',
762
+ 'borderRightWidth',
763
+ 'borderBottomWidth',
764
+ 'borderLeftWidth',
765
+ 'textDecorationColor',
766
+ 'textDecorationLine',
767
+ 'textDecorationThickness',
768
+ ];
769
+ window.__kerfEvaluatorFocusBaseline = {};
770
+ for (const [index, element] of [
771
+ ...document.querySelectorAll(
772
+ 'button, a[href], input:not([type="hidden"]), select, textarea, summary, [role], [tabindex]',
773
+ ),
774
+ ].entries()) {
775
+ const id = `kui-focus-${index}`;
776
+ element.setAttribute('data-kui-evaluator-focus-id', id);
777
+ const style = getComputedStyle(element);
778
+ window.__kerfEvaluatorFocusBaseline[id] = Object.fromEntries(
779
+ properties.map((property) => [property, style[property]]),
780
+ );
781
+ }
782
+ if (document.activeElement instanceof HTMLElement)
783
+ document.activeElement.blur();
784
+ });
785
+ try {
786
+ for (let index = 0; index < limit; index += 1) {
787
+ await page.keyboard.press('Tab');
788
+ const state = await page.evaluate(() => {
789
+ const element = document.activeElement;
790
+ if (!(element instanceof HTMLElement) || element === document.body)
791
+ return null;
792
+ const identity = element.getAttribute('data-kui-evaluator-focus-id');
793
+ const rect = element.getBoundingClientRect();
794
+ const style = getComputedStyle(element);
795
+ const baseline = window.__kerfEvaluatorFocusBaseline?.[identity] ?? {};
796
+ const focused = Object.fromEntries(
797
+ Object.keys(baseline).map((property) => [property, style[property]]),
798
+ );
799
+ const changedProperties = Object.keys(baseline).filter(
800
+ (property) => baseline[property] !== focused[property],
801
+ );
802
+ const visualChange =
803
+ (Number.parseFloat(focused.outlineWidth) > 0 &&
804
+ changedProperties.some((property) =>
805
+ property.startsWith('outline'),
806
+ )) ||
807
+ (focused.boxShadow !== 'none' &&
808
+ changedProperties.includes('boxShadow')) ||
809
+ changedProperties.includes('backgroundColor') ||
810
+ changedProperties.includes('color') ||
811
+ ['Top', 'Right', 'Bottom', 'Left'].some(
812
+ (side) =>
813
+ Number.parseFloat(focused[`border${side}Width`]) > 0 &&
814
+ (changedProperties.includes(`border${side}Color`) ||
815
+ changedProperties.includes(`border${side}Width`)),
816
+ ) ||
817
+ (focused.textDecorationLine !== 'none' &&
818
+ changedProperties.some((property) =>
819
+ property.startsWith('textDecoration'),
820
+ ));
821
+ const centerVisible =
822
+ rect.left < innerWidth &&
823
+ rect.top < innerHeight &&
824
+ rect.right > 0 &&
825
+ rect.bottom > 0;
826
+ return {
827
+ identity,
828
+ selector: element.id
829
+ ? `#${CSS.escape(element.id)}`
830
+ : `${element.localName}${[...element.classList]
831
+ .slice(0, 2)
832
+ .map((name) => `.${CSS.escape(name)}`)
833
+ .join('')}`,
834
+ visible:
835
+ rect.width > 0 &&
836
+ rect.height > 0 &&
837
+ centerVisible &&
838
+ style.display !== 'none' &&
839
+ style.visibility !== 'hidden' &&
840
+ Number(style.opacity) !== 0,
841
+ focusVisible: element.matches(':focus-visible') && visualChange,
842
+ changedProperties,
843
+ };
844
+ });
845
+ if (!state || sequence.some((item) => item.identity === state.identity))
846
+ break;
847
+ sequence.push({ identity: state.identity, selector: state.selector });
848
+ if (!state.visible || !state.focusVisible)
849
+ diagnostics.push({
850
+ code: 'KUI-B021',
851
+ selector: state.selector,
852
+ message: !state.visible
853
+ ? 'Sequential focus reached a control outside the visible viewport.'
854
+ : 'Sequential focus has no visual style change from its unfocused state.',
855
+ repair:
856
+ 'Keep focusable controls visible and retain a visible keyboard-focus style change.',
857
+ evidence: state,
858
+ });
859
+ }
860
+ } finally {
861
+ await page.evaluate(() => {
862
+ for (const element of document.querySelectorAll(
863
+ '[data-kui-evaluator-focus-id]',
864
+ ))
865
+ element.removeAttribute('data-kui-evaluator-focus-id');
866
+ delete window.__kerfEvaluatorFocusBaseline;
867
+ });
868
+ }
869
+ return { diagnostics, sequence };
870
+ }
871
+
872
+ async function inspectKeyboardActions(page) {
873
+ const actions = await page.locator('[data-kui-evaluator-action]').count();
874
+ const diagnostics = [];
875
+ const evidence = [];
876
+ for (let index = 0; index < actions; index += 1) {
877
+ const action = page.locator('[data-kui-evaluator-action]').nth(index);
878
+ await action.focus();
879
+ const before = await action.evaluate((element) => ({
880
+ expanded: element.getAttribute('aria-expanded'),
881
+ pressed: element.getAttribute('aria-pressed'),
882
+ checked: element.getAttribute('aria-checked'),
883
+ value: 'value' in element ? element.value : undefined,
884
+ }));
885
+ await action.evaluate((element) => {
886
+ window.__kerfEvaluatorActivation = 0;
887
+ element.addEventListener(
888
+ 'click',
889
+ () => {
890
+ window.__kerfEvaluatorActivation += 1;
891
+ },
892
+ { once: true },
893
+ );
894
+ });
895
+ await page.keyboard.press('Enter');
896
+ await page.waitForTimeout(0);
897
+ const after = await action.evaluate((element) => ({
898
+ activations: window.__kerfEvaluatorActivation ?? 0,
899
+ expanded: element.getAttribute('aria-expanded'),
900
+ pressed: element.getAttribute('aria-pressed'),
901
+ checked: element.getAttribute('aria-checked'),
902
+ value: 'value' in element ? element.value : undefined,
903
+ }));
904
+ const selector = await action.evaluate((element) =>
905
+ element.id ? `#${CSS.escape(element.id)}` : element.localName,
906
+ );
907
+ evidence.push({ selector, before, after });
908
+ if (
909
+ after.activations === 0 &&
910
+ before.expanded === after.expanded &&
911
+ before.pressed === after.pressed &&
912
+ before.checked === after.checked &&
913
+ before.value === after.value
914
+ )
915
+ diagnostics.push({
916
+ code: 'KUI-B023',
917
+ selector,
918
+ message: 'Representative action did not activate with Enter.',
919
+ repair:
920
+ 'Use a native control or implement Enter/Space activation and controlled state updates.',
921
+ evidence: { before, after },
922
+ });
923
+ }
924
+ return { diagnostics, evidence };
925
+ }
926
+
927
+ async function fileArtifact(path, root) {
928
+ const bytes = await readFile(path);
929
+ return {
930
+ path: relative(root, path).replaceAll('\\', '/'),
931
+ sha256: sha256(bytes),
932
+ };
933
+ }
934
+
935
+ async function clearEvaluatorScreenshots(outputDirectory) {
936
+ for (const entry of await readdir(outputDirectory, { withFileTypes: true }))
937
+ if (
938
+ entry.isFile() &&
939
+ /^(chromium|firefox|webkit)-[a-z0-9-]+\.png$/.test(entry.name)
940
+ )
941
+ await rm(resolve(outputDirectory, entry.name), { force: true });
942
+ }
943
+
944
+ export function createEvaluationReport({
945
+ target,
946
+ profileFiles,
947
+ contexts,
948
+ diagnostics,
949
+ artifacts,
950
+ recordedAt,
951
+ retention,
952
+ }) {
953
+ return {
954
+ schemaVersion: UI_EVALUATION_SCHEMA_VERSION,
955
+ tool: { name: '@kerfjs/ui/evaluator', reportVersion: 1 },
956
+ target,
957
+ recordedAt,
958
+ profileFiles,
959
+ contexts,
960
+ diagnostics,
961
+ summary: summarize(diagnostics),
962
+ artifacts: { retention, files: artifacts },
963
+ subjectiveReview: {
964
+ status: 'not-recorded',
965
+ rubric: SUBJECTIVE_REVIEW_RUBRIC,
966
+ ratings: [],
967
+ note: 'These aesthetic judgments require a named reviewer and are never inferred from objective browser checks.',
968
+ },
969
+ };
970
+ }
971
+
972
+ export async function evaluateUi({
973
+ url,
974
+ workspaceRoot = process.cwd(),
975
+ startDirectory = workspaceRoot,
976
+ packageProfile,
977
+ outputDirectory = resolve(workspaceRoot, 'kerf-ui-evidence'),
978
+ reportPath = resolve(outputDirectory, 'report.json'),
979
+ browsers = ['chromium', 'firefox', 'webkit'],
980
+ contexts: contextOverrides,
981
+ timeoutMs = 15_000,
982
+ settleMs = 100,
983
+ retention = 'on-failure',
984
+ recordedAt = new Date().toISOString(),
985
+ playwright: suppliedPlaywright,
986
+ signal,
987
+ } = {}) {
988
+ if (signal?.aborted) throw cancellationError();
989
+ if (!url) throw new Error('evaluateUi requires a running app URL.');
990
+ if (!['always', 'on-failure', 'never'].includes(retention))
991
+ throw new Error('retention must be always, on-failure, or never.');
992
+ const profileResult = await loadApplicationUiProfile({
993
+ workspaceRoot,
994
+ startDirectory,
995
+ packageProfile,
996
+ knownRules: Object.keys(UI_EVALUATION_RULES),
997
+ });
998
+ if (profileResult.diagnostics.length)
999
+ throw new Error(
1000
+ `Application UI profile is invalid:\n${profileResult.diagnostics
1001
+ .map(
1002
+ (item) => `${item.code} ${item.source} ${item.path}: ${item.message}`,
1003
+ )
1004
+ .join('\n')}`,
1005
+ );
1006
+ const geometryContracts = await loadGeometryContracts(profileResult);
1007
+ const playwright = suppliedPlaywright ?? (await import('playwright'));
1008
+ const matrix = buildEvaluationContexts(contextOverrides);
1009
+ const contextReports = [];
1010
+ const diagnostics = [];
1011
+ const screenshotPaths = [];
1012
+ await mkdir(outputDirectory, { recursive: true });
1013
+ await clearEvaluatorScreenshots(outputDirectory);
1014
+ const evaluationTarget = relative(workspaceRoot, startDirectory).replaceAll(
1015
+ '\\',
1016
+ '/',
1017
+ );
1018
+ const exceptedRules = new Set(
1019
+ (profileResult.profile.exceptions ?? [])
1020
+ .filter((exception) => exception.target === evaluationTarget)
1021
+ .flatMap((exception) => exception.rules),
1022
+ );
1023
+ const includeDiagnostic = (item) => !exceptedRules.has(item.code);
1024
+ let activeBrowser;
1025
+ const closeForCancellation = () => {
1026
+ void activeBrowser?.close();
1027
+ };
1028
+ signal?.addEventListener('abort', closeForCancellation);
1029
+
1030
+ try {
1031
+ for (const browserName of browsers) {
1032
+ if (signal?.aborted) throw cancellationError();
1033
+ const browserType = playwright[browserName];
1034
+ if (!browserType)
1035
+ throw new Error(`Unknown Playwright browser ${browserName}.`);
1036
+ let browser;
1037
+ try {
1038
+ try {
1039
+ const launch = browserType.launch();
1040
+ void launch.then(
1041
+ (candidate) => {
1042
+ if (signal?.aborted) void candidate.close();
1043
+ },
1044
+ () => {},
1045
+ );
1046
+ browser = await abortable(launch, signal);
1047
+ activeBrowser = browser;
1048
+ } catch (error) {
1049
+ if (signal?.aborted) throw cancellationError();
1050
+ for (const matrixContext of matrix) {
1051
+ const contextId = `${browserName}:${matrixContext.id}`;
1052
+ const item = reportDiagnostic('KUI-B001', contextId, {
1053
+ message: `Could not launch ${browserName}: ${error.message}`,
1054
+ repair:
1055
+ 'Install the requested Playwright browser and confirm the CI runner can launch it.',
1056
+ });
1057
+ if (includeDiagnostic(item)) diagnostics.push(item);
1058
+ contextReports.push({
1059
+ ...matrixContext,
1060
+ id: contextId,
1061
+ browser: browserName,
1062
+ evidence: {},
1063
+ summary: summarize(includeDiagnostic(item) ? [item] : []),
1064
+ });
1065
+ }
1066
+ continue;
1067
+ }
1068
+ for (const matrixContext of matrix) {
1069
+ if (signal?.aborted) throw cancellationError();
1070
+ const contextId = `${browserName}:${matrixContext.id}`;
1071
+ let browserContext;
1072
+ const contextDiagnostics = [];
1073
+ let evidence = {};
1074
+ try {
1075
+ browserContext = await browser.newContext({
1076
+ viewport: {
1077
+ width: matrixContext.width,
1078
+ height: matrixContext.height,
1079
+ },
1080
+ colorScheme: matrixContext.colorScheme,
1081
+ reducedMotion: matrixContext.reducedMotion,
1082
+ });
1083
+ const page = await browserContext.newPage();
1084
+ page.setDefaultTimeout(timeoutMs);
1085
+ page.setDefaultNavigationTimeout(timeoutMs);
1086
+ await page.goto(url, {
1087
+ waitUntil: 'domcontentloaded',
1088
+ timeout: timeoutMs,
1089
+ });
1090
+ await page.emulateMedia({
1091
+ colorScheme: matrixContext.colorScheme,
1092
+ reducedMotion: matrixContext.reducedMotion,
1093
+ });
1094
+ await page.waitForTimeout(settleMs);
1095
+ const dom = await inspectPage(page, geometryContracts);
1096
+ const focus = await inspectFocus(page);
1097
+ const keyboard = await inspectKeyboardActions(page);
1098
+ for (const item of [
1099
+ ...dom.diagnostics,
1100
+ ...focus.diagnostics,
1101
+ ...keyboard.diagnostics,
1102
+ ]) {
1103
+ if (!includeDiagnostic(item)) continue;
1104
+ contextDiagnostics.push(
1105
+ reportDiagnostic(item.code, contextId, item),
1106
+ );
1107
+ }
1108
+ evidence = {
1109
+ ...dom.evidence,
1110
+ focusSequence: focus.sequence,
1111
+ keyboardActions: keyboard.evidence,
1112
+ };
1113
+ const screenshotPath = resolve(
1114
+ outputDirectory,
1115
+ `${browserName}-${matrixContext.id}.png`,
1116
+ );
1117
+ await page.screenshot({ path: screenshotPath, fullPage: true });
1118
+ screenshotPaths.push(screenshotPath);
1119
+ } catch (error) {
1120
+ if (signal?.aborted) throw cancellationError();
1121
+ const item = reportDiagnostic('KUI-B001', contextId, {
1122
+ message: error.message,
1123
+ repair:
1124
+ 'Confirm the app is running, the URL is reachable, and it settles within the configured timeout.',
1125
+ });
1126
+ if (includeDiagnostic(item)) contextDiagnostics.push(item);
1127
+ } finally {
1128
+ await browserContext?.close();
1129
+ }
1130
+ diagnostics.push(...contextDiagnostics);
1131
+ contextReports.push({
1132
+ ...matrixContext,
1133
+ id: contextId,
1134
+ browser: browserName,
1135
+ evidence,
1136
+ summary: summarize(contextDiagnostics),
1137
+ });
1138
+ }
1139
+ } finally {
1140
+ await browser?.close();
1141
+ activeBrowser = undefined;
1142
+ }
1143
+ }
1144
+ } catch (error) {
1145
+ if (signal?.aborted) {
1146
+ await clearEvaluatorScreenshots(outputDirectory);
1147
+ throw cancellationError();
1148
+ }
1149
+ throw error;
1150
+ } finally {
1151
+ signal?.removeEventListener('abort', closeForCancellation);
1152
+ }
1153
+
1154
+ const ensureActive = async () => {
1155
+ if (!signal?.aborted) return;
1156
+ await clearEvaluatorScreenshots(outputDirectory);
1157
+ throw cancellationError();
1158
+ };
1159
+ await ensureActive();
1160
+ if (
1161
+ retention === 'never' ||
1162
+ (retention === 'on-failure' &&
1163
+ !diagnostics.some((item) => item.severity === 'error'))
1164
+ ) {
1165
+ for (const path of screenshotPaths) await rm(path, { force: true });
1166
+ screenshotPaths.length = 0;
1167
+ }
1168
+ const artifacts = [];
1169
+ for (const path of screenshotPaths) {
1170
+ await ensureActive();
1171
+ artifacts.push(await fileArtifact(path, outputDirectory));
1172
+ }
1173
+ const report = createEvaluationReport({
1174
+ target: { url },
1175
+ profileFiles: profileResult.files.map((path) =>
1176
+ relative(workspaceRoot, path).replaceAll('\\', '/'),
1177
+ ),
1178
+ contexts: contextReports,
1179
+ diagnostics,
1180
+ artifacts,
1181
+ recordedAt,
1182
+ retention,
1183
+ });
1184
+ await ensureActive();
1185
+ await mkdir(dirname(reportPath), { recursive: true });
1186
+ await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
1187
+ return report;
1188
+ }
1189
+
1190
+ export function formatUiEvaluationText(report) {
1191
+ const lines = [
1192
+ `Kerf UI browser evaluation: ${report.summary.passed ? 'PASS' : 'FAIL'}`,
1193
+ `${report.contexts.length} contexts; ${report.summary.errors} errors; ${report.summary.warnings} warnings`,
1194
+ ];
1195
+ for (const diagnostic of report.diagnostics)
1196
+ lines.push(
1197
+ `${diagnostic.code} [${diagnostic.context}] ${diagnostic.selector ?? '<page>'}: ${diagnostic.message}\n Repair: ${diagnostic.repair}`,
1198
+ );
1199
+ lines.push(
1200
+ 'Human visual review: not recorded (use the report rubric with a named reviewer).',
1201
+ );
1202
+ return lines.join('\n');
1203
+ }