@kerfjs/ui 5.0.0-beta.6 → 5.0.0-beta.61

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 (714) hide show
  1. package/README.md +494 -138
  2. package/ai/application-ui-diagnostic-ids-v1.json +109 -0
  3. package/ai/application-ui-profile-sync.cjs +529 -0
  4. package/ai/application-ui-profile.d.mts +1 -0
  5. package/ai/application-ui-profile.d.ts +102 -0
  6. package/ai/application-ui-profile.defaults.json +96 -0
  7. package/ai/application-ui-profile.mjs +626 -0
  8. package/ai/application-ui-profile.schema.json +130 -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 +79 -0
  14. package/ai/component-catalog.json +4041 -591
  15. package/ai/component-catalog.schema.json +78 -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 +125 -0
  21. package/ai/component-composition.json +7074 -0
  22. package/ai/component-composition.schema.json +375 -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 +90 -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 +96 -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 +103 -0
  44. package/ai/components/loading-spinner.md +72 -0
  45. package/ai/components/lucide-icon.md +66 -0
  46. package/ai/components/nav-stack.md +103 -0
  47. package/ai/components/pane.md +100 -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 +112 -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 +112 -0
  70. package/ai/components/tab-scaffold.md +94 -0
  71. package/ai/components/tabs.md +95 -0
  72. package/ai/components/text.md +83 -0
  73. package/ai/components/token-search-field.md +103 -0
  74. package/ai/components/toolbar-control-group.md +102 -0
  75. package/ai/components/toolbar-text.md +78 -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 +114 -0
  149. package/ai/public-api-signatures-v1.md +2597 -190
  150. package/ai/skill.md +259 -65
  151. package/ai/webawesome-jsx-signatures-v1.md +9 -2
  152. package/analyzer/cli.mjs +49 -0
  153. package/analyzer/component-ownership.d.mts +56 -0
  154. package/analyzer/component-ownership.mjs +184 -0
  155. package/analyzer/index.d.mts +49 -0
  156. package/analyzer/index.mjs +1458 -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/dist/app-tab.d.ts +41 -4
  163. package/dist/app-tab.js +3 -1
  164. package/dist/badge.d.ts +52 -0
  165. package/dist/badge.js +3 -0
  166. package/dist/badge.js.map +1 -0
  167. package/dist/browser/app-tab.js +1 -0
  168. package/dist/browser/badge.js +3 -0
  169. package/dist/browser/catalog.js +25 -0
  170. package/dist/browser/chip.js +3 -0
  171. package/dist/browser/collapsible-panel.js +7 -0
  172. package/dist/browser/content-item.js +3 -0
  173. package/dist/browser/floating-toolbar.js +3 -0
  174. package/dist/browser/grid.js +3 -0
  175. package/dist/browser/list-action-row.js +5 -0
  176. package/dist/browser/list-header.js +8 -0
  177. package/dist/browser/list-inset-control.js +3 -0
  178. package/dist/browser/list-inset-text.js +3 -0
  179. package/dist/browser/list-item.js +5 -0
  180. package/dist/browser/list.js +3 -0
  181. package/dist/browser/{panel-header.js → nav-stack.js} +4 -2
  182. package/dist/browser/pane.js +3 -0
  183. package/dist/browser/popup-menu.js +3 -0
  184. package/dist/browser/row.js +3 -0
  185. package/dist/browser/select.js +2 -0
  186. package/dist/browser/skeleton.js +3 -0
  187. package/dist/browser/spacer.js +3 -0
  188. package/dist/browser/split-view.js +10 -0
  189. package/dist/browser/state-banner.js +2 -0
  190. package/dist/browser/sunken-panel.js +3 -0
  191. package/dist/browser/surface-scaffold.js +3 -0
  192. package/dist/browser/tab-scaffold.js +4 -0
  193. package/dist/browser/text.js +3 -0
  194. package/dist/browser/toolbar-text.js +1 -0
  195. package/dist/browser/value-table.js +1 -0
  196. package/dist/browser/workbench.js +10 -0
  197. package/dist/catalog-resources.d.ts +46 -0
  198. package/dist/catalog-resources.js +22 -0
  199. package/dist/catalog-resources.js.map +1 -0
  200. package/dist/catalog.d.ts +90 -0
  201. package/dist/catalog.js +501 -0
  202. package/dist/catalog.js.map +1 -0
  203. package/dist/chip.d.ts +29 -0
  204. package/dist/chip.js +3 -0
  205. package/dist/chip.js.map +1 -0
  206. package/dist/chunk-2G2ZJQYK.js +152 -0
  207. package/dist/chunk-2G2ZJQYK.js.map +1 -0
  208. package/dist/chunk-3SXMEK3B.js +43 -0
  209. package/dist/chunk-3SXMEK3B.js.map +1 -0
  210. package/dist/{chunk-W2FK7ZNY.js → chunk-4R777DUZ.js} +10 -2
  211. package/dist/chunk-4R777DUZ.js.map +1 -0
  212. package/dist/chunk-4SQK27MR.js +202 -0
  213. package/dist/chunk-4SQK27MR.js.map +1 -0
  214. package/dist/chunk-5I7QGQVT.js +24 -0
  215. package/dist/chunk-5I7QGQVT.js.map +1 -0
  216. package/dist/chunk-5PESG2VP.js +115 -0
  217. package/dist/chunk-5PESG2VP.js.map +1 -0
  218. package/dist/chunk-5Q3N6EV7.js +156 -0
  219. package/dist/chunk-5Q3N6EV7.js.map +1 -0
  220. package/dist/chunk-6ZXOQEAN.js +27 -0
  221. package/dist/chunk-6ZXOQEAN.js.map +1 -0
  222. package/dist/chunk-7JUBXOIV.js +103 -0
  223. package/dist/chunk-7JUBXOIV.js.map +1 -0
  224. package/dist/chunk-A3CKOW2R.js +622 -0
  225. package/dist/chunk-A3CKOW2R.js.map +1 -0
  226. package/dist/chunk-AHEG27M7.js +66 -0
  227. package/dist/chunk-AHEG27M7.js.map +1 -0
  228. package/dist/chunk-AUKT3KMG.js +37 -0
  229. package/dist/chunk-AUKT3KMG.js.map +1 -0
  230. package/dist/chunk-AYUHMAI6.js +48 -0
  231. package/dist/chunk-AYUHMAI6.js.map +1 -0
  232. package/dist/chunk-BFPBS5Y4.js +250 -0
  233. package/dist/chunk-BFPBS5Y4.js.map +1 -0
  234. package/dist/{chunk-MQIZAD4A.js → chunk-C4OYOKWX.js} +138 -22
  235. package/dist/chunk-C4OYOKWX.js.map +1 -0
  236. package/dist/chunk-DG6G5EPF.js +35 -0
  237. package/dist/chunk-DG6G5EPF.js.map +1 -0
  238. package/dist/chunk-E36EVCKG.js +34 -0
  239. package/dist/chunk-E36EVCKG.js.map +1 -0
  240. package/dist/chunk-EAC4CDY7.js +57 -0
  241. package/dist/chunk-EAC4CDY7.js.map +1 -0
  242. package/dist/chunk-EZVNC4J6.js +104 -0
  243. package/dist/chunk-EZVNC4J6.js.map +1 -0
  244. package/dist/chunk-F4QMLOXJ.js +135 -0
  245. package/dist/chunk-F4QMLOXJ.js.map +1 -0
  246. package/dist/chunk-GLDWRCAM.js +49 -0
  247. package/dist/chunk-GLDWRCAM.js.map +1 -0
  248. package/dist/chunk-I3Q7KKYM.js +33 -0
  249. package/dist/chunk-I3Q7KKYM.js.map +1 -0
  250. package/dist/chunk-IA7TMD4Y.js +288 -0
  251. package/dist/chunk-IA7TMD4Y.js.map +1 -0
  252. package/dist/chunk-JCV4GXWB.js +249 -0
  253. package/dist/chunk-JCV4GXWB.js.map +1 -0
  254. package/dist/chunk-K47P7QZC.js +17 -0
  255. package/dist/chunk-K47P7QZC.js.map +1 -0
  256. package/dist/chunk-KLJKU6ZL.js +109 -0
  257. package/dist/chunk-KLJKU6ZL.js.map +1 -0
  258. package/dist/{chunk-56RJXPHG.js → chunk-KROD7KZK.js} +7 -4
  259. package/dist/chunk-KROD7KZK.js.map +1 -0
  260. package/dist/chunk-MA73TA6D.js +42 -0
  261. package/dist/chunk-MA73TA6D.js.map +1 -0
  262. package/dist/chunk-MERWRTBG.js +45 -0
  263. package/dist/chunk-MERWRTBG.js.map +1 -0
  264. package/dist/chunk-MEY33VWB.js +44 -0
  265. package/dist/chunk-MEY33VWB.js.map +1 -0
  266. package/dist/chunk-MP2GDBQZ.js +50 -0
  267. package/dist/chunk-MP2GDBQZ.js.map +1 -0
  268. package/dist/chunk-N3GKMPDW.js +51 -0
  269. package/dist/chunk-N3GKMPDW.js.map +1 -0
  270. package/dist/chunk-NAPE7WUS.js +75 -0
  271. package/dist/chunk-NAPE7WUS.js.map +1 -0
  272. package/dist/chunk-NN6GYOA2.js +47 -0
  273. package/dist/chunk-NN6GYOA2.js.map +1 -0
  274. package/dist/chunk-NNMLEDMD.js +399 -0
  275. package/dist/chunk-NNMLEDMD.js.map +1 -0
  276. package/dist/chunk-NYL63PSD.js +95 -0
  277. package/dist/chunk-NYL63PSD.js.map +1 -0
  278. package/dist/chunk-P7D3EMXM.js +52 -0
  279. package/dist/chunk-P7D3EMXM.js.map +1 -0
  280. package/dist/chunk-PSHUSWQH.js +18 -0
  281. package/dist/chunk-PSHUSWQH.js.map +1 -0
  282. package/dist/chunk-Q6YU3BT7.js +138 -0
  283. package/dist/chunk-Q6YU3BT7.js.map +1 -0
  284. package/dist/chunk-RRB26JRJ.js +247 -0
  285. package/dist/chunk-RRB26JRJ.js.map +1 -0
  286. package/dist/{chunk-7BR3DO2A.js → chunk-SRSJO5QE.js} +13 -4
  287. package/dist/chunk-SRSJO5QE.js.map +1 -0
  288. package/dist/chunk-SWVW56CV.js +188 -0
  289. package/dist/chunk-SWVW56CV.js.map +1 -0
  290. package/dist/chunk-TEYARRAK.js +216 -0
  291. package/dist/chunk-TEYARRAK.js.map +1 -0
  292. package/dist/chunk-UCKKT6KO.js +71 -0
  293. package/dist/chunk-UCKKT6KO.js.map +1 -0
  294. package/dist/chunk-UIDKY4UB.js +14 -0
  295. package/dist/chunk-UIDKY4UB.js.map +1 -0
  296. package/dist/chunk-VCBTX62E.js +135 -0
  297. package/dist/chunk-VCBTX62E.js.map +1 -0
  298. package/dist/chunk-VZOFZPSO.js +71 -0
  299. package/dist/chunk-VZOFZPSO.js.map +1 -0
  300. package/dist/{chunk-TRNBHPNW.js → chunk-X5QSICJ3.js} +13 -7
  301. package/dist/chunk-X5QSICJ3.js.map +1 -0
  302. package/dist/chunk-Y74NWGQW.js +418 -0
  303. package/dist/chunk-Y74NWGQW.js.map +1 -0
  304. package/dist/chunk-ZLWQZUGF.js +27 -0
  305. package/dist/chunk-ZLWQZUGF.js.map +1 -0
  306. package/dist/collapsible-panel.d.ts +122 -0
  307. package/dist/collapsible-panel.js +115 -0
  308. package/dist/collapsible-panel.js.map +1 -0
  309. package/dist/content-item.d.ts +46 -0
  310. package/dist/content-item.js +4 -0
  311. package/dist/content-item.js.map +1 -0
  312. package/dist/css-values.d.ts +107 -0
  313. package/dist/css-values.js +3 -0
  314. package/dist/css-values.js.map +1 -0
  315. package/dist/device-class.js +33 -5
  316. package/dist/device-class.js.map +1 -1
  317. package/dist/disclosure-arrow.d.ts +6 -1
  318. package/dist/disclosure-arrow.js +1 -1
  319. package/dist/empty-state.d.ts +5 -2
  320. package/dist/empty-state.js +1 -1
  321. package/dist/flex-alignment-4ms8ZbV8.d.ts +5 -0
  322. package/dist/floating-toolbar.d.ts +51 -0
  323. package/dist/floating-toolbar.js +3 -0
  324. package/dist/floating-toolbar.js.map +1 -0
  325. package/dist/grid.d.ts +42 -0
  326. package/dist/grid.js +5 -0
  327. package/dist/grid.js.map +1 -0
  328. package/dist/help-tags-register.d.ts +13 -0
  329. package/dist/help-tags-register.js +9 -0
  330. package/dist/help-tags-register.js.map +1 -0
  331. package/dist/index.d.ts +35 -14
  332. package/dist/index.js +43 -22
  333. package/dist/list-action-row.d.ts +64 -0
  334. package/dist/list-action-row.js +8 -0
  335. package/dist/list-action-row.js.map +1 -0
  336. package/dist/list-header.d.ts +99 -0
  337. package/dist/list-header.js +10 -0
  338. package/dist/list-header.js.map +1 -0
  339. package/dist/list-inset-control.d.ts +22 -0
  340. package/dist/list-inset-control.js +3 -0
  341. package/dist/list-inset-control.js.map +1 -0
  342. package/dist/list-inset-text.d.ts +30 -0
  343. package/dist/list-inset-text.js +4 -0
  344. package/dist/list-inset-text.js.map +1 -0
  345. package/dist/list-item.d.ts +62 -0
  346. package/dist/list-item.js +8 -0
  347. package/dist/list-item.js.map +1 -0
  348. package/dist/list.d.ts +59 -0
  349. package/dist/list.js +6 -0
  350. package/dist/list.js.map +1 -0
  351. package/dist/loading-spinner.d.ts +3 -1
  352. package/dist/loading-spinner.js +1 -1
  353. package/dist/lucide-icon.d.ts +3 -1
  354. package/dist/lucide-icon.js +1 -1
  355. package/dist/nav-stack.d.ts +60 -10
  356. package/dist/nav-stack.js +6 -1
  357. package/dist/pane.d.ts +102 -0
  358. package/dist/pane.js +4 -0
  359. package/dist/pane.js.map +1 -0
  360. package/dist/panel-toolbar-DqwtBxQL.d.ts +63 -0
  361. package/dist/popup-menu-register.d.ts +9 -0
  362. package/dist/popup-menu-register.js +31 -0
  363. package/dist/popup-menu-register.js.map +1 -0
  364. package/dist/popup-menu.d.ts +99 -0
  365. package/dist/popup-menu.js +3 -0
  366. package/dist/popup-menu.js.map +1 -0
  367. package/dist/resizable-region.d.ts +32 -3
  368. package/dist/resizable-region.js +1 -1
  369. package/dist/row.d.ts +48 -0
  370. package/dist/row.js +6 -0
  371. package/dist/row.js.map +1 -0
  372. package/dist/segmented-control.d.ts +13 -8
  373. package/dist/segmented-control.js +1 -1
  374. package/dist/select-register.d.ts +5 -1
  375. package/dist/select-register.js +243 -2
  376. package/dist/select-register.js.map +1 -1
  377. package/dist/select.d.ts +101 -8
  378. package/dist/select.js +4 -1
  379. package/dist/semantic-content-BbzjvSu9.d.ts +14 -0
  380. package/dist/sides-BPSWde0A.d.ts +4 -0
  381. package/dist/skeleton.d.ts +31 -0
  382. package/dist/skeleton.js +4 -0
  383. package/dist/skeleton.js.map +1 -0
  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 +19 -5
  391. package/dist/state-banner.js +4 -1
  392. package/dist/styles/app-tab.css +104 -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 +164 -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 +118 -0
  400. package/dist/styles/collapsible-panel.css +575 -0
  401. package/dist/styles/content-item.css +30 -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 +28 -0
  406. package/dist/styles/layout.css +3 -41
  407. package/dist/styles/list-action-row.css +257 -0
  408. package/dist/styles/list-header.css +228 -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 +245 -0
  412. package/dist/styles/list.css +133 -0
  413. package/dist/styles/nav-stack.css +158 -50
  414. package/dist/styles/pane.css +229 -0
  415. package/dist/styles/popup-menu.css +271 -0
  416. package/dist/styles/resizable-region.css +429 -4
  417. package/dist/styles/row.css +111 -0
  418. package/dist/styles/segmented-control.css +33 -9
  419. package/dist/styles/select.css +262 -9
  420. package/dist/styles/skeleton.css +92 -0
  421. package/dist/styles/spacer.css +11 -0
  422. package/dist/styles/split-view.css +108 -3
  423. package/dist/styles/state-banner.css +94 -19
  424. package/dist/styles/styles.css +14 -4
  425. package/dist/styles/sunken-panel.css +22 -0
  426. package/dist/styles/surface-scaffold.css +56 -0
  427. package/dist/styles/tab-bar.css +132 -9
  428. package/dist/styles/tab-scaffold.css +113 -3
  429. package/dist/styles/text.css +65 -0
  430. package/dist/styles/token-search-field.css +49 -43
  431. package/dist/styles/toolbar-control-group.css +465 -28
  432. package/dist/styles/toolbar-text.css +88 -4
  433. package/dist/styles/toolbar.css +322 -11
  434. package/dist/styles/value-table.css +34 -4
  435. package/dist/styles/webawesome.css +348 -10
  436. package/dist/styles/workbench.css +828 -7
  437. package/dist/sunken-panel.d.ts +22 -0
  438. package/dist/sunken-panel.js +3 -0
  439. package/dist/sunken-panel.js.map +1 -0
  440. package/dist/surface-scaffold.d.ts +30 -0
  441. package/dist/surface-scaffold.js +3 -0
  442. package/dist/surface-scaffold.js.map +1 -0
  443. package/dist/tab-bar.d.ts +31 -6
  444. package/dist/tab-bar.js +1 -1
  445. package/dist/tab-scaffold.d.ts +54 -7
  446. package/dist/tab-scaffold.js +82 -9
  447. package/dist/tab-scaffold.js.map +1 -1
  448. package/dist/text.d.ts +38 -0
  449. package/dist/text.js +3 -0
  450. package/dist/text.js.map +1 -0
  451. package/dist/token-search-field-DvgFLhLL.d.ts +130 -0
  452. package/dist/token-search-field.d.ts +3 -43
  453. package/dist/token-search-field.js +1 -1
  454. package/dist/token-search-model.d.ts +3 -0
  455. package/dist/token-search-model.js +3 -0
  456. package/dist/token-search-model.js.map +1 -0
  457. package/dist/toolbar-control-group.d.ts +65 -7
  458. package/dist/toolbar-control-group.js +1 -1
  459. package/dist/toolbar-text.d.ts +37 -4
  460. package/dist/toolbar-text.js +3 -1
  461. package/dist/toolbar.d.ts +56 -7
  462. package/dist/toolbar.js +1 -1
  463. package/dist/types-BuHELdXR.d.ts +117 -0
  464. package/dist/value-table.d.ts +13 -3
  465. package/dist/value-table.js +3 -1
  466. package/dist/webawesome.d.ts +8 -1
  467. package/dist/wire-catalog.d.ts +82 -0
  468. package/dist/wire-catalog.js +327 -0
  469. package/dist/wire-catalog.js.map +1 -0
  470. package/dist/wire-nav-stack.d.ts +4 -1
  471. package/dist/wire-nav-stack.js +147 -7
  472. package/dist/wire-nav-stack.js.map +1 -1
  473. package/dist/wire-resizable-regions.d.ts +2 -1
  474. package/dist/wire-resizable-regions.js +2 -1
  475. package/dist/wire-scroll-dividers.d.ts +69 -0
  476. package/dist/wire-scroll-dividers.js +3 -0
  477. package/dist/wire-scroll-dividers.js.map +1 -0
  478. package/dist/wire-sidebar.d.ts +63 -0
  479. package/dist/wire-sidebar.js +231 -0
  480. package/dist/wire-sidebar.js.map +1 -0
  481. package/dist/wire-tab-bars.d.ts +19 -2
  482. package/dist/wire-tab-bars.js +1 -1
  483. package/dist/wire-tab-scaffold.js +9 -4
  484. package/dist/wire-tab-scaffold.js.map +1 -1
  485. package/dist/wire-token-search-fields.d.ts +65 -3
  486. package/dist/wire-token-search-fields.js +1 -1
  487. package/dist/wire-workbench.d.ts +109 -0
  488. package/dist/wire-workbench.js +6 -0
  489. package/dist/wire-workbench.js.map +1 -0
  490. package/dist/workbench.d.ts +218 -7
  491. package/dist/workbench.js +13 -15
  492. package/dist/workbench.js.map +1 -1
  493. package/docs/accessibility.md +413 -23
  494. package/docs/app-layouts.md +106 -8
  495. package/docs/catalog.md +440 -0
  496. package/docs/collapsible-panel.md +363 -0
  497. package/docs/component-contract.md +560 -52
  498. package/docs/component-integration.md +75 -0
  499. package/docs/component-selection.md +421 -89
  500. package/docs/design/templates/empty-state/filtered-dark.svg +1 -0
  501. package/docs/design/templates/empty-state/filtered.svg +1 -0
  502. package/docs/design/templates/empty-state/with-action-dark.svg +1 -0
  503. package/docs/design/templates/empty-state/with-action.svg +1 -0
  504. package/docs/design/templates/empty-state-dark.svg +1 -0
  505. package/docs/design/templates/empty-state.svg +1 -0
  506. package/docs/design/templates/list/compact-dark.svg +1 -0
  507. package/docs/design/templates/list/compact.svg +1 -0
  508. package/docs/design/templates/list/stack-dark.svg +1 -0
  509. package/docs/design/templates/list/stack.svg +1 -0
  510. package/docs/design/templates/list-action-row/default-dark.svg +1 -0
  511. package/docs/design/templates/list-action-row/default.svg +1 -0
  512. package/docs/design/templates/list-action-row/selected-dark.svg +1 -0
  513. package/docs/design/templates/list-action-row/selected.svg +1 -0
  514. package/docs/design/templates/list-action-row-dark.svg +1 -0
  515. package/docs/design/templates/list-action-row.svg +1 -0
  516. package/docs/design/templates/list-dark.svg +1 -0
  517. package/docs/design/templates/list-header/action-dark.svg +1 -0
  518. package/docs/design/templates/list-header/action.svg +1 -0
  519. package/docs/design/templates/list-header/count-dark.svg +1 -0
  520. package/docs/design/templates/list-header/count.svg +1 -0
  521. package/docs/design/templates/list-header/inline-action-dark.svg +1 -0
  522. package/docs/design/templates/list-header/inline-action.svg +1 -0
  523. package/docs/design/templates/list-header/toggle-collapsed-dark.svg +1 -0
  524. package/docs/design/templates/list-header/toggle-collapsed.svg +1 -0
  525. package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -0
  526. package/docs/design/templates/list-header/toggle-expanded.svg +1 -0
  527. package/docs/design/templates/list-header-dark.svg +1 -0
  528. package/docs/design/templates/list-header.svg +1 -0
  529. package/docs/design/templates/list-item/default-dark.svg +1 -0
  530. package/docs/design/templates/list-item/default.svg +1 -0
  531. package/docs/design/templates/list-item/multiline-dark.svg +1 -0
  532. package/docs/design/templates/list-item/multiline.svg +1 -0
  533. package/docs/design/templates/list-item/selected-dark.svg +1 -0
  534. package/docs/design/templates/list-item/selected.svg +1 -0
  535. package/docs/design/templates/list-item/trailing-dark.svg +1 -0
  536. package/docs/design/templates/list-item/trailing.svg +1 -0
  537. package/docs/design/templates/list-item-dark.svg +1 -0
  538. package/docs/design/templates/list-item.svg +1 -0
  539. package/docs/design/templates/list.svg +1 -0
  540. package/docs/design/templates/segmented-control/equal-dark.svg +1 -0
  541. package/docs/design/templates/segmented-control/equal.svg +1 -0
  542. package/docs/design/templates/segmented-control/pill-small-dark.svg +1 -0
  543. package/docs/design/templates/segmented-control/pill-small.svg +1 -0
  544. package/docs/design/templates/segmented-control-dark.svg +1 -0
  545. package/docs/design/templates/segmented-control.svg +1 -0
  546. package/docs/design/templates/skeleton/avatar-dark.svg +1 -0
  547. package/docs/design/templates/skeleton/avatar.svg +1 -0
  548. package/docs/design/templates/skeleton/block-dark.svg +1 -0
  549. package/docs/design/templates/skeleton/block.svg +1 -0
  550. package/docs/design/templates/skeleton/lines-dark.svg +1 -0
  551. package/docs/design/templates/skeleton/lines.svg +1 -0
  552. package/docs/design/templates/skeleton-dark.svg +1 -0
  553. package/docs/design/templates/skeleton.svg +1 -0
  554. package/docs/design/templates/state-banner/danger-dark.svg +1 -0
  555. package/docs/design/templates/state-banner/danger.svg +1 -0
  556. package/docs/design/templates/state-banner/info-badge-dark.svg +1 -0
  557. package/docs/design/templates/state-banner/info-badge.svg +1 -0
  558. package/docs/design/templates/state-banner/info-dark.svg +1 -0
  559. package/docs/design/templates/state-banner/info.svg +1 -0
  560. package/docs/design/templates/state-banner/success-dark.svg +1 -0
  561. package/docs/design/templates/state-banner/success.svg +1 -0
  562. package/docs/design/templates/state-banner/warning-dark.svg +1 -0
  563. package/docs/design/templates/state-banner/warning.svg +1 -0
  564. package/docs/design/templates/state-banner-dark.svg +1 -0
  565. package/docs/design/templates/state-banner.svg +1 -0
  566. package/docs/design/templates/tab-bar/workspace-tabs-dark.svg +1 -0
  567. package/docs/design/templates/tab-bar/workspace-tabs.svg +1 -0
  568. package/docs/design/templates/tab-bar-dark.svg +1 -0
  569. package/docs/design/templates/tab-bar.svg +1 -0
  570. package/docs/design/templates/token-search-field/empty-dark.svg +1 -0
  571. package/docs/design/templates/token-search-field/empty.svg +1 -0
  572. package/docs/design/templates/token-search-field/tokens-dark.svg +1 -0
  573. package/docs/design/templates/token-search-field/tokens.svg +1 -0
  574. package/docs/design/templates/token-search-field-dark.svg +1 -0
  575. package/docs/design/templates/token-search-field.svg +1 -0
  576. package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -0
  577. package/docs/design/templates/toolbar/title-and-actions.svg +1 -0
  578. package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
  579. package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
  580. package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
  581. package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
  582. package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
  583. package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
  584. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
  585. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
  586. package/docs/design/templates/toolbar-control-group-dark.svg +1 -0
  587. package/docs/design/templates/toolbar-control-group.svg +1 -0
  588. package/docs/design/templates/toolbar-dark.svg +1 -0
  589. package/docs/design/templates/toolbar-text/default-dark.svg +1 -0
  590. package/docs/design/templates/toolbar-text/default.svg +1 -0
  591. package/docs/design/templates/toolbar-text/large-dark.svg +1 -0
  592. package/docs/design/templates/toolbar-text/large.svg +1 -0
  593. package/docs/design/templates/toolbar-text/small-dark.svg +1 -0
  594. package/docs/design/templates/toolbar-text/small.svg +1 -0
  595. package/docs/design/templates/toolbar-text-dark.svg +1 -0
  596. package/docs/design/templates/toolbar-text.svg +1 -0
  597. package/docs/design/templates/toolbar.svg +1 -0
  598. package/docs/design/templates/value-table/details-dark.svg +1 -0
  599. package/docs/design/templates/value-table/details.svg +1 -0
  600. package/docs/design/templates/value-table-dark.svg +1 -0
  601. package/docs/design/templates/value-table.svg +1 -0
  602. package/docs/design/templates.md +138 -0
  603. package/docs/design-philosophy.md +48 -20
  604. package/docs/device-class.md +10 -9
  605. package/docs/document-baseline.md +38 -0
  606. package/docs/examples/application-ui-profile.json +60 -0
  607. package/docs/examples/command-palette-adapter.tsx +69 -17
  608. package/docs/examples/component-catalog-extension.json +45 -0
  609. package/docs/examples/component-composition-extension.json +80 -0
  610. package/docs/layout.md +409 -59
  611. package/docs/nav-stack.md +126 -14
  612. package/docs/recipes.md +137 -54
  613. package/docs/split-view.md +69 -6
  614. package/docs/sunken-panel.md +58 -0
  615. package/docs/surface-scaffold.md +25 -0
  616. package/docs/tab-scaffold.md +94 -10
  617. package/docs/type-contracts.md +134 -0
  618. package/docs/ui-analyzer.md +306 -0
  619. package/docs/ui-doctor.md +107 -0
  620. package/docs/ui-evaluator.md +139 -0
  621. package/docs/ux-demo.md +40 -16
  622. package/docs/webawesome-theme.md +175 -16
  623. package/docs/workbench.md +534 -12
  624. package/doctor/cli.mjs +115 -0
  625. package/doctor/config.schema.json +68 -0
  626. package/doctor/eslint-diagnostics.d.mts +24 -0
  627. package/doctor/eslint-diagnostics.mjs +24 -0
  628. package/doctor/index.d.mts +116 -0
  629. package/doctor/index.mjs +1277 -0
  630. package/doctor/report.schema.json +118 -0
  631. package/evaluator/cli.mjs +89 -0
  632. package/evaluator/index.d.mts +90 -0
  633. package/evaluator/index.mjs +1203 -0
  634. package/evaluator/report.schema.json +152 -0
  635. package/llms.txt +423 -42
  636. package/package.json +241 -37
  637. package/traversal-exclusions.mjs +27 -0
  638. package/ux-demo/recipes/app-shell.tsx +205 -17
  639. package/ux-demo/recipes/collapsible-sidebar.tsx +260 -0
  640. package/ux-demo/recipes/compact-toolbar.tsx +135 -14
  641. package/ux-demo/recipes/composer-form.tsx +133 -21
  642. package/ux-demo/recipes/list-detail-dialog.tsx +282 -0
  643. package/ux-demo/recipes/list-workspace-states.tsx +183 -11
  644. package/ux-demo/recipes/loaders.ts +15 -5
  645. package/ux-demo/recipes/loading-inspector.tsx +204 -0
  646. package/ux-demo/recipes/mount-recipe.ts +28 -10
  647. package/ux-demo/recipes/navigation-sidebar.tsx +139 -13
  648. package/ux-demo/recipes/navigation-stack.tsx +79 -24
  649. package/ux-demo/recipes/types.ts +23 -2
  650. package/ux-demo/recipes/workspace-header.tsx +86 -5
  651. package/dist/browser/menu-action-row.js +0 -3
  652. package/dist/browser/menu-header.js +0 -5
  653. package/dist/browser/menu-item.js +0 -3
  654. package/dist/chunk-24Z2XE6C.js +0 -10
  655. package/dist/chunk-24Z2XE6C.js.map +0 -1
  656. package/dist/chunk-2RJBFNB6.js +0 -172
  657. package/dist/chunk-2RJBFNB6.js.map +0 -1
  658. package/dist/chunk-55DATV3A.js +0 -10
  659. package/dist/chunk-55DATV3A.js.map +0 -1
  660. package/dist/chunk-56RJXPHG.js.map +0 -1
  661. package/dist/chunk-7BR3DO2A.js.map +0 -1
  662. package/dist/chunk-7LDCTU7D.js +0 -134
  663. package/dist/chunk-7LDCTU7D.js.map +0 -1
  664. package/dist/chunk-AQNKJRFH.js +0 -79
  665. package/dist/chunk-AQNKJRFH.js.map +0 -1
  666. package/dist/chunk-CTQKI2XL.js +0 -28
  667. package/dist/chunk-CTQKI2XL.js.map +0 -1
  668. package/dist/chunk-DAFFDO33.js +0 -14
  669. package/dist/chunk-DAFFDO33.js.map +0 -1
  670. package/dist/chunk-EGHL5S4P.js +0 -33
  671. package/dist/chunk-EGHL5S4P.js.map +0 -1
  672. package/dist/chunk-KZKSBUKC.js +0 -19
  673. package/dist/chunk-KZKSBUKC.js.map +0 -1
  674. package/dist/chunk-M5JRI6MN.js +0 -27
  675. package/dist/chunk-M5JRI6MN.js.map +0 -1
  676. package/dist/chunk-MBR3TYNL.js +0 -14
  677. package/dist/chunk-MBR3TYNL.js.map +0 -1
  678. package/dist/chunk-MFKMHK5W.js +0 -10
  679. package/dist/chunk-MFKMHK5W.js.map +0 -1
  680. package/dist/chunk-MQIZAD4A.js.map +0 -1
  681. package/dist/chunk-NPPD2A6O.js +0 -40
  682. package/dist/chunk-NPPD2A6O.js.map +0 -1
  683. package/dist/chunk-R6JHHBKQ.js +0 -19
  684. package/dist/chunk-R6JHHBKQ.js.map +0 -1
  685. package/dist/chunk-RBTVBGRD.js +0 -23
  686. package/dist/chunk-RBTVBGRD.js.map +0 -1
  687. package/dist/chunk-S3TPM2LP.js +0 -23
  688. package/dist/chunk-S3TPM2LP.js.map +0 -1
  689. package/dist/chunk-TRNBHPNW.js.map +0 -1
  690. package/dist/chunk-VHW5YNIB.js +0 -17
  691. package/dist/chunk-VHW5YNIB.js.map +0 -1
  692. package/dist/chunk-W2FK7ZNY.js.map +0 -1
  693. package/dist/chunk-WVPYVDXY.js +0 -15
  694. package/dist/chunk-WVPYVDXY.js.map +0 -1
  695. package/dist/chunk-XZXRLY5Q.js +0 -33
  696. package/dist/chunk-XZXRLY5Q.js.map +0 -1
  697. package/dist/menu-action-row.d.ts +0 -50
  698. package/dist/menu-action-row.js +0 -4
  699. package/dist/menu-action-row.js.map +0 -1
  700. package/dist/menu-header.d.ts +0 -41
  701. package/dist/menu-header.js +0 -6
  702. package/dist/menu-header.js.map +0 -1
  703. package/dist/menu-item.d.ts +0 -31
  704. package/dist/menu-item.js +0 -4
  705. package/dist/menu-item.js.map +0 -1
  706. package/dist/panel-header.d.ts +0 -26
  707. package/dist/panel-header.js +0 -6
  708. package/dist/panel-header.js.map +0 -1
  709. package/dist/styles/menu-action-row.css +0 -160
  710. package/dist/styles/menu-header.css +0 -160
  711. package/dist/styles/menu-item.css +0 -123
  712. package/dist/styles/panel-header.css +0 -59
  713. package/ux-demo/recipes/master-detail-dialog.tsx +0 -45
  714. package/ux-demo/recipes/recipes.css +0 -355
@@ -0,0 +1,1458 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises';
2
+ import {
3
+ basename,
4
+ dirname,
5
+ extname,
6
+ isAbsolute,
7
+ relative,
8
+ resolve,
9
+ } from 'node:path';
10
+ import process from 'node:process';
11
+
12
+ import postcss from 'postcss';
13
+ import ts from 'typescript';
14
+
15
+ import { loadApplicationUiProfile } from '../ai/application-ui-profile.mjs';
16
+ import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
17
+ import {
18
+ componentLabel,
19
+ componentName,
20
+ componentOwnershipFacts,
21
+ configurationFor,
22
+ privateVariableOwner,
23
+ reportGap,
24
+ restyledComponents,
25
+ typedPropForToken,
26
+ } from './component-ownership.mjs';
27
+ import { collectLoudDeclarations, inspectLoudPairs } from './loud-pairs.mjs';
28
+ import {
29
+ classNames,
30
+ complexSelectorParts,
31
+ withoutRelationalArguments,
32
+ } from './selectors.mjs';
33
+
34
+ export const UI_ANALYSIS_SCHEMA_VERSION = 1;
35
+
36
+ export const UI_ANALYSIS_RULES = Object.freeze({
37
+ 'KUI-L001': { severity: 'error', title: 'Private or unknown Kerf selector' },
38
+ 'KUI-L002': { severity: 'error', title: 'Unknown Kerf token' },
39
+ 'KUI-L003': { severity: 'error', title: 'Competing geometry owners' },
40
+ 'KUI-L004': { severity: 'review', title: 'Repeated content inset' },
41
+ // KUI-L005 ('Forced component dimension') is retired: KUI-L019 reports any
42
+ // restyle of a cataloged component, dimensions included, and L005's only
43
+ // other shape (a public class as an ancestor) sizes an application element.
44
+ // The id stays in the diagnostic registry so existing exceptions still load.
45
+ 'KUI-L006': {
46
+ severity: 'review',
47
+ title: 'Hard-coded spacing outside the approved scale',
48
+ },
49
+ 'KUI-L007': { severity: 'error', title: 'Nested scroll owners' },
50
+ 'KUI-L008': { severity: 'review', title: 'Dynamic class requires review' },
51
+ 'KUI-L009': { severity: 'error', title: 'Stylesheet could not be parsed' },
52
+ 'KUI-L010': {
53
+ severity: 'error',
54
+ title: 'Private Kerf descendant override',
55
+ },
56
+ 'KUI-L011': { severity: 'error', title: 'Uncataloged shadow part override' },
57
+ 'KUI-L012': { severity: 'error', title: 'Private Kerf token assignment' },
58
+ 'KUI-L013': { severity: 'error', title: 'Uncataloged CSS value literal' },
59
+ 'KUI-L014': { severity: 'error', title: 'Wrong CSS value helper dimension' },
60
+ 'KUI-L015': { severity: 'error', title: 'Non-standalone CSS expression' },
61
+ 'KUI-L016': { severity: 'error', title: 'Forbidden declaration-list escape' },
62
+ 'KUI-L017': { severity: 'review', title: 'Exceptional spacing shorthand' },
63
+ 'KUI-L018': {
64
+ severity: 'review',
65
+ title: 'Loud fill override without its on-loud pair',
66
+ },
67
+ 'KUI-L019': {
68
+ severity: 'error',
69
+ title: 'Application CSS restyles a cataloged component',
70
+ },
71
+ 'KUI-L020': {
72
+ severity: 'error',
73
+ title: "Another component's private variable",
74
+ },
75
+ 'KUI-L021': {
76
+ severity: 'error',
77
+ title: 'Component token overridden where a typed prop exists',
78
+ },
79
+ 'KUI-L022': {
80
+ severity: 'error',
81
+ title: "Hook class restyles a cataloged component's root",
82
+ },
83
+ });
84
+
85
+ const ownershipAction =
86
+ "Configure the component through its typed props, variants, or public tokens; style only your own elements (in the component's context when needed). If no configuration covers the need, report the component gap to its package instead of overriding it.";
87
+
88
+ /** The doctor's per-rule repair action, where it is more specific than the default. */
89
+ export const UI_ANALYSIS_ACTIONS = Object.freeze({
90
+ 'KUI-L019': ownershipAction,
91
+ 'KUI-L020': ownershipAction,
92
+ 'KUI-L021':
93
+ 'Set the typed prop the diagnostic names on the component instead of overriding its token.',
94
+ 'KUI-L022': ownershipAction,
95
+ });
96
+
97
+ const adoptionRules = new Set([
98
+ 'KUI-L001',
99
+ 'KUI-L002',
100
+ 'KUI-L010',
101
+ 'KUI-L011',
102
+ 'KUI-L012',
103
+ 'KUI-L019',
104
+ 'KUI-L020',
105
+ 'KUI-L021',
106
+ 'KUI-L022',
107
+ ]);
108
+
109
+ const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
110
+ const spacingProperties = /^(?:margin|padding|gap|inset)(?:-|$)/;
111
+ const approvedSpacing = new Set([0, 4, 8, 16, 24]);
112
+ async function collectFiles(root, paths) {
113
+ const files = [];
114
+ const visit = async (path) => {
115
+ if (isUiTraversalExcluded(root, path)) return;
116
+ const details = await stat(path);
117
+ if (details.isFile()) {
118
+ if (sourceExtensions.has(extname(path)) || path.endsWith('.css'))
119
+ files.push(path);
120
+ return;
121
+ }
122
+ const entries = await readdir(path, { withFileTypes: true });
123
+ for (const entry of entries) {
124
+ const child = resolve(path, entry.name);
125
+ if (entry.isDirectory()) await visit(child);
126
+ else if (
127
+ sourceExtensions.has(extname(entry.name)) ||
128
+ entry.name.endsWith('.css')
129
+ )
130
+ files.push(child);
131
+ }
132
+ };
133
+ for (const path of paths?.length ? paths : [root])
134
+ await visit(resolve(root, path));
135
+ return files.sort();
136
+ }
137
+
138
+ function location(file, node, source) {
139
+ if (!source) {
140
+ const point = node.source?.start;
141
+ return {
142
+ file,
143
+ line: point?.line ?? 1,
144
+ column: point?.column ?? point?.offset ?? 1,
145
+ };
146
+ }
147
+ const point = source.getLineAndCharacterOfPosition(node.getStart(source));
148
+ return {
149
+ file,
150
+ line: point.line + 1,
151
+ column: point.character + 1,
152
+ };
153
+ }
154
+
155
+ function diagnostic(ruleId, at, message, evidence, chain, adoption = false) {
156
+ const rule = UI_ANALYSIS_RULES[ruleId];
157
+ return {
158
+ ruleId,
159
+ severity: adoption && adoptionRules.has(ruleId) ? 'review' : rule.severity,
160
+ message,
161
+ location: at,
162
+ evidence,
163
+ chain,
164
+ };
165
+ }
166
+
167
+ function isSuppressed(item, profile, root) {
168
+ const target = relative(root, item.location.file).replaceAll('\\', '/');
169
+ return (profile?.exceptions ?? []).some(
170
+ (exception) =>
171
+ exception.rules?.includes(item.ruleId) && exception.target === target,
172
+ );
173
+ }
174
+
175
+ function relativeStyleImports(file, source) {
176
+ const imports = [];
177
+ if (file.endsWith('.css')) {
178
+ let stylesheet;
179
+ try {
180
+ stylesheet = postcss.parse(source, { from: file });
181
+ } catch {
182
+ return imports;
183
+ }
184
+ stylesheet.walkAtRules('import', (rule) => {
185
+ const match = rule.params.match(
186
+ /^\s*(?:["']([^"']+)["']|url\(\s*(?:["']([^"']+)["']|([^\s)]+))\s*\))/,
187
+ );
188
+ const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
189
+ if (specifier?.startsWith('.'))
190
+ imports.push(resolve(dirname(file), specifier));
191
+ });
192
+ return imports;
193
+ }
194
+ const syntax = file.endsWith('.tsx')
195
+ ? ts.ScriptKind.TSX
196
+ : file.endsWith('.jsx')
197
+ ? ts.ScriptKind.JSX
198
+ : file.endsWith('.ts')
199
+ ? ts.ScriptKind.TS
200
+ : ts.ScriptKind.JS;
201
+ const module = ts.createSourceFile(
202
+ file,
203
+ source,
204
+ ts.ScriptTarget.Latest,
205
+ true,
206
+ syntax,
207
+ );
208
+ for (const statement of module.statements) {
209
+ if (
210
+ !ts.isImportDeclaration(statement) ||
211
+ !ts.isStringLiteral(statement.moduleSpecifier)
212
+ )
213
+ continue;
214
+ const specifier = statement.moduleSpecifier.text;
215
+ if (specifier.startsWith('.') && specifier.endsWith('.css'))
216
+ imports.push(resolve(dirname(file), specifier));
217
+ }
218
+ return imports;
219
+ }
220
+
221
+ function reachableStyleFacts(file, imports, styleFacts) {
222
+ const result = new Map();
223
+ for (const style of reachableStyleFiles(file, imports, styleFacts)) {
224
+ for (const [className, facts] of styleFacts.get(style)) {
225
+ const current = result.get(className) ?? { scroll: false, inset: false };
226
+ current.scroll ||= facts.scroll;
227
+ current.inset ||= facts.inset;
228
+ current.styled ??= facts.styled;
229
+ result.set(className, current);
230
+ }
231
+ }
232
+ return result;
233
+ }
234
+
235
+ function reachableStyleFiles(file, imports, styleFacts) {
236
+ const visited = new Set();
237
+ const visit = (style) => {
238
+ if (visited.has(style) || !styleFacts.has(style)) return;
239
+ visited.add(style);
240
+ for (const dependency of imports.get(style) ?? []) visit(dependency);
241
+ };
242
+ for (const style of imports.get(file) ?? []) visit(style);
243
+ return visited;
244
+ }
245
+
246
+ async function loadCatalogs(profileResult) {
247
+ const entries = [];
248
+ for (const catalog of profileResult.profile?.catalogs ?? []) {
249
+ const owner = profileResult.provenance[`$catalogs.${catalog.package}`];
250
+ if (!owner) continue;
251
+ try {
252
+ const artifact = JSON.parse(
253
+ await readFile(
254
+ resolve(dirname(owner), catalog.composition.path),
255
+ 'utf8',
256
+ ),
257
+ );
258
+ const selection = await loadSelectionFacts(owner, catalog);
259
+ for (const entry of artifact.entries ?? []) {
260
+ const selected = selection.get(entry.key);
261
+ entries.push(
262
+ selected
263
+ ? {
264
+ ...entry,
265
+ publicExports: entry.publicExports ?? selected.publicExports,
266
+ moduleImports: selected.moduleImports,
267
+ }
268
+ : entry,
269
+ );
270
+ }
271
+ } catch {
272
+ // Profile validation reports the precise catalog load failure.
273
+ }
274
+ }
275
+ return entries;
276
+ }
277
+
278
+ // The composition (v2) catalog carries no import facts, and several of its
279
+ // entries share a display name (the Kerf `Select` and the Web Awesome
280
+ // `wa-select` are both "Select"). The selection (v1) catalog is where each
281
+ // entry's real public exports and import subpaths live, so join it by key.
282
+ async function loadSelectionFacts(owner, catalog) {
283
+ const facts = new Map();
284
+ if (!catalog.selection?.path) return facts;
285
+ let artifact;
286
+ try {
287
+ artifact = JSON.parse(
288
+ await readFile(resolve(dirname(owner), catalog.selection.path), 'utf8'),
289
+ );
290
+ } catch {
291
+ return facts;
292
+ }
293
+ const packageName = artifact.package ?? catalog.package;
294
+ for (const entry of artifact.entries ?? []) {
295
+ const delivery = entry.delivery ?? {};
296
+ facts.set(`${packageName}:${entry.id}`, {
297
+ publicExports: entry.publicExports,
298
+ moduleImports: [delivery.browserImport, delivery.moduleImport].filter(
299
+ Boolean,
300
+ ),
301
+ });
302
+ }
303
+ return facts;
304
+ }
305
+
306
+ // A catalog entry's public exports mix the component render function with
307
+ // helpers its props consume (`Select` ships `uiColor`, `List` ships
308
+ // `px`/`rem`/`flex`). Only the render function stands for the entry, so only
309
+ // it may carry the entry's contracts. A component export is one whose name
310
+ // starts with an uppercase letter: JSX itself treats a lowercase tag as an
311
+ // intrinsic element, so a lowercase export can never be a component tag, and
312
+ // scripts/check-component-catalog.mjs enforces the converse for this package
313
+ // (every uppercase runtime export returns SafeHtml; no lowercase one does).
314
+ // eslint-plugin-kerfjs applies the same rule.
315
+ function isComponentExport(name) {
316
+ return /^[A-Z]/.test(name);
317
+ }
318
+
319
+ // Pick the catalog entry an imported (or namespace-accessed) export name
320
+ // refers to. Names are not unique across a catalog, so candidates are ranked:
321
+ // an entry whose own import subpath is the module wins, then an entry that
322
+ // declares the name as a public export, then catalog order. A helper export
323
+ // never resolves to an entry.
324
+ function resolveExportEntry(candidates, name, module) {
325
+ if (!isComponentExport(name)) return undefined;
326
+ const eligible = (candidates ?? []).filter(
327
+ (entry) =>
328
+ module === entry.package || module?.startsWith(`${entry.package}/`),
329
+ );
330
+ const exported = (entry) =>
331
+ (entry.publicExports ?? []).some(
332
+ (item) => (typeof item === 'string' ? item : item.name) === name,
333
+ );
334
+ return (
335
+ eligible.find((entry) => entry.moduleImports?.includes(module)) ??
336
+ eligible.find(exported) ??
337
+ eligible[0]
338
+ );
339
+ }
340
+
341
+ function catalogFacts(entries) {
342
+ const publicClasses = new Set();
343
+ const publicTokens = new Set();
344
+ const publicParts = new Map();
345
+ const classEntries = new Map();
346
+ const exportEntries = new Map();
347
+ const addExport = (name, entry) => {
348
+ const candidates = exportEntries.get(name) ?? [];
349
+ if (!candidates.includes(entry)) candidates.push(entry);
350
+ exportEntries.set(name, candidates);
351
+ };
352
+ for (const entry of entries) {
353
+ addExport(entry.name, entry);
354
+ for (const item of entry.publicExports ?? []) {
355
+ const name = typeof item === 'string' ? item : item.name;
356
+ if (isComponentExport(name)) addExport(name, entry);
357
+ }
358
+ for (const className of entry.boundaries?.publicClasses ?? []) {
359
+ publicClasses.add(className);
360
+ const owners = classEntries.get(className) ?? [];
361
+ owners.push(entry);
362
+ classEntries.set(className, owners);
363
+ }
364
+ for (const token of entry.boundaries?.publicTokens ?? [])
365
+ publicTokens.add(token);
366
+ for (const part of entry.boundaries?.publicParts ?? []) {
367
+ const roots = publicParts.get(part) ?? new Set();
368
+ if (entry.boundaries?.rootClass) roots.add(entry.boundaries.rootClass);
369
+ publicParts.set(part, roots);
370
+ }
371
+ }
372
+ return {
373
+ publicClasses,
374
+ publicTokens,
375
+ publicParts,
376
+ classEntries,
377
+ exportEntries,
378
+ ownership: componentOwnershipFacts(entries),
379
+ };
380
+ }
381
+
382
+ function spacingValues(value) {
383
+ const results = [];
384
+ for (const match of value.matchAll(/(-?\d*\.?\d+)(px|rem)\b/g)) {
385
+ const numeric = Number(match[1]) * (match[2] === 'rem' ? 16 : 1);
386
+ results.push({ text: match[0], pixels: numeric });
387
+ }
388
+ return results;
389
+ }
390
+
391
+ function escapeRegExp(value) {
392
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
393
+ }
394
+
395
+ // A declaration that restyles, as opposed to one that only sets a custom
396
+ // property: setting a component's public token on its host is configuration,
397
+ // which the token rules (KUI-L002, KUI-L012, KUI-L020, KUI-L021) judge.
398
+ function restylingDeclarations(rule) {
399
+ return (rule.nodes ?? []).filter(
400
+ (node) => node.type === 'decl' && !node.prop.startsWith('--'),
401
+ );
402
+ }
403
+
404
+ function inKeyframes(rule) {
405
+ return rule.parent?.type === 'atrule' && /keyframes$/i.test(rule.parent.name);
406
+ }
407
+
408
+ // The classes a rule makes its subject (the rightmost compound of each complex
409
+ // selector, outside :has()/:not()).
410
+ function subjectClasses(selector) {
411
+ return complexSelectorParts(selector).flatMap((parts) => {
412
+ const subject = parts.at(-1)?.compound;
413
+ return subject ? classNames(withoutRelationalArguments(subject)) : [];
414
+ });
415
+ }
416
+
417
+ function inspectComponentOwnership(
418
+ file,
419
+ rule,
420
+ facts,
421
+ diagnostics,
422
+ adoption,
423
+ isForeign,
424
+ ) {
425
+ if (inKeyframes(rule)) return;
426
+ const restyling = restylingDeclarations(rule);
427
+ if (restyling.length === 0) return;
428
+ const properties = [...new Set(restyling.map((decl) => decl.prop))];
429
+ for (const { entry, via, name } of restyledComponents(
430
+ rule.selector,
431
+ facts.ownership,
432
+ isForeign,
433
+ ))
434
+ diagnostics.push(
435
+ diagnostic(
436
+ 'KUI-L019',
437
+ location(file, rule),
438
+ `\`${name}\` makes ${componentLabel(entry)} the subject of an application rule (${properties.join(', ')}); components own their styles. Configure it through ${configurationFor(entry)}. To place your own content in its context, style your own element (\`${name} > .your-element\`). ${reportGap(entry)}`,
439
+ {
440
+ selector: rule.selector,
441
+ component: entry.key,
442
+ via,
443
+ target: name,
444
+ properties,
445
+ },
446
+ undefined,
447
+ adoption,
448
+ ),
449
+ );
450
+ }
451
+
452
+ function inspectVariableOwnership(
453
+ file,
454
+ decl,
455
+ facts,
456
+ diagnostics,
457
+ adoption,
458
+ isForeign,
459
+ ) {
460
+ const at = location(file, decl);
461
+ const privates = new Set([
462
+ ...(decl.prop.match(/--_[a-z0-9_-]+/gi) ?? []),
463
+ ...(decl.value.match(/--_[a-z0-9_-]+/gi) ?? []),
464
+ ]);
465
+ for (const variable of privates) {
466
+ const owner = privateVariableOwner(variable, facts.ownership);
467
+ if (owner === undefined || (owner && !isForeign(owner))) continue;
468
+ const written = decl.prop === variable;
469
+ diagnostics.push(
470
+ diagnostic(
471
+ 'KUI-L020',
472
+ at,
473
+ `\`${variable}\` is ${owner ? `${componentName(owner)}'s` : "a Kerf component's"} private variable; ${written ? 'writing it overrides' : 'reading it couples to'} that component's implementation. Configure it through ${configurationFor(owner)}. ${reportGap(owner)}`,
474
+ {
475
+ property: decl.prop,
476
+ value: decl.value,
477
+ variable,
478
+ ...(owner ? { component: owner.key } : {}),
479
+ access: written ? 'write' : 'read',
480
+ },
481
+ undefined,
482
+ adoption,
483
+ ),
484
+ );
485
+ }
486
+ if (!decl.prop.startsWith('--')) return;
487
+ const typed = typedPropForToken(decl.prop, facts.ownership);
488
+ if (!typed || !isForeign(typed.entry)) return;
489
+ const contract = (typed.entry.cssValueProps ?? []).find(
490
+ ({ path }) => path === typed.path,
491
+ );
492
+ const example = contract?.examples?.[0];
493
+ diagnostics.push(
494
+ diagnostic(
495
+ 'KUI-L021',
496
+ at,
497
+ `\`${decl.prop}\` is what ${typed.entry.name}'s typed \`${typed.path}\` prop sets; set the prop on ${typed.entry.name}${example ? ` (\`<${typed.entry.name} ${example} />\`)` : ''} instead of overriding the token. ${reportGap(typed.entry)}`,
498
+ {
499
+ property: decl.prop,
500
+ value: decl.value,
501
+ component: typed.entry.key,
502
+ prop: typed.path,
503
+ },
504
+ undefined,
505
+ adoption,
506
+ ),
507
+ );
508
+ }
509
+
510
+ async function inspectCss(
511
+ file,
512
+ source,
513
+ facts,
514
+ diagnostics,
515
+ cssFacts,
516
+ adoption = false,
517
+ siblingOnLoud = [],
518
+ isForeign = () => true,
519
+ ) {
520
+ let root;
521
+ try {
522
+ root = postcss.parse(source, { from: file });
523
+ } catch (error) {
524
+ diagnostics.push(
525
+ diagnostic(
526
+ 'KUI-L009',
527
+ { file, line: error.line ?? 1, column: error.column ?? 1 },
528
+ error.reason ?? error.message,
529
+ ),
530
+ );
531
+ return;
532
+ }
533
+ inspectLoudPairs(
534
+ file,
535
+ root,
536
+ (decl, message, evidence) =>
537
+ diagnostics.push(
538
+ diagnostic('KUI-L018', location(file, decl), message, evidence),
539
+ ),
540
+ siblingOnLoud,
541
+ );
542
+ root.walkRules((rule) => {
543
+ inspectComponentOwnership(
544
+ file,
545
+ rule,
546
+ facts,
547
+ diagnostics,
548
+ adoption,
549
+ isForeign,
550
+ );
551
+ if (!inKeyframes(rule) && restylingDeclarations(rule).length > 0)
552
+ for (const className of subjectClasses(rule.selector)) {
553
+ const record = cssFacts.get(className) ?? {
554
+ scroll: false,
555
+ inset: false,
556
+ };
557
+ record.styled ??= {
558
+ stylesheet: file,
559
+ line: rule.source?.start?.line ?? 1,
560
+ selector: rule.selector,
561
+ };
562
+ cssFacts.set(className, record);
563
+ }
564
+ const classes = [
565
+ ...rule.selector.matchAll(/\.([_a-zA-Z]+[_a-zA-Z0-9-]*)/g),
566
+ ].map((match) => match[1]);
567
+ const publicRootPattern = [...facts.publicClasses]
568
+ .map(
569
+ (className) => `\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
570
+ )
571
+ .join('|');
572
+ const privateDescendants = new Set();
573
+ if (publicRootPattern)
574
+ for (const selector of rule.selectors ?? [rule.selector]) {
575
+ const rootMatch = new RegExp(
576
+ `(?:${publicRootPattern})(?:[.#:[\\w-]|\\s)*(?:>|\\s)`,
577
+ ).exec(selector);
578
+ if (!rootMatch) continue;
579
+ const descendant = selector.slice(
580
+ rootMatch.index + rootMatch[0].length,
581
+ );
582
+ for (const match of descendant.matchAll(/\.(kui-[\w-]+)/g))
583
+ if (!facts.publicClasses.has(match[1]))
584
+ privateDescendants.add(match[1]);
585
+ }
586
+ for (const className of classes) {
587
+ if (privateDescendants.has(className))
588
+ diagnostics.push(
589
+ diagnostic(
590
+ 'KUI-L010',
591
+ location(file, rule),
592
+ `.${className} is a private Kerf descendant; prefer the owning component's prop or variant, or catalog a deliberate public extension point.`,
593
+ { selector: rule.selector, className },
594
+ undefined,
595
+ adoption,
596
+ ),
597
+ );
598
+ else if (
599
+ className.startsWith('kui-') &&
600
+ !facts.publicClasses.has(className)
601
+ )
602
+ diagnostics.push(
603
+ diagnostic(
604
+ 'KUI-L001',
605
+ location(file, rule),
606
+ `.${className} is not a cataloged public Kerf class; prefer a component prop or variant, or catalog the extension point.`,
607
+ { selector: rule.selector, className },
608
+ undefined,
609
+ adoption,
610
+ ),
611
+ );
612
+ }
613
+ for (const match of rule.selector.matchAll(/::part\(\s*([\w-]+)\s*\)/g)) {
614
+ const part = match[1];
615
+ const roots = facts.publicParts.get(part) ?? new Set();
616
+ const allowed = [...roots].some((className) =>
617
+ new RegExp(`\\.${escapeRegExp(className)}(?:[^\\w-]|$)`).test(
618
+ rule.selector.slice(0, match.index),
619
+ ),
620
+ );
621
+ if (!allowed)
622
+ diagnostics.push(
623
+ diagnostic(
624
+ 'KUI-L011',
625
+ location(file, rule),
626
+ `::part(${part}) is not a cataloged extension point; prefer the component's prop or variant, or add ${part} to boundaries.publicParts.`,
627
+ { selector: rule.selector, part },
628
+ undefined,
629
+ adoption,
630
+ ),
631
+ );
632
+ }
633
+ rule.walkDecls((decl) => {
634
+ inspectVariableOwnership(
635
+ file,
636
+ decl,
637
+ facts,
638
+ diagnostics,
639
+ adoption,
640
+ isForeign,
641
+ );
642
+ const at = location(file, decl);
643
+ const tokens = [
644
+ ...(decl.prop.match(/--kui-[a-z0-9-]+/g) ?? []),
645
+ ...(decl.value.match(/--kui-[a-z0-9-]+/g) ?? []),
646
+ ];
647
+ for (const token of new Set(tokens))
648
+ if (!facts.publicTokens.has(token)) {
649
+ const assignment = decl.prop === token;
650
+ diagnostics.push(
651
+ diagnostic(
652
+ assignment ? 'KUI-L012' : 'KUI-L002',
653
+ at,
654
+ assignment
655
+ ? `${token} has no public configuration contract; prefer the component's prop or variant, or catalog the token explicitly.`
656
+ : `${token} is not a cataloged public Kerf token; prefer a documented token or component configuration.`,
657
+ { property: decl.prop, value: decl.value, token },
658
+ undefined,
659
+ adoption,
660
+ ),
661
+ );
662
+ }
663
+ if (
664
+ spacingProperties.test(decl.prop) &&
665
+ !/var\(|calc\(|remify\(/.test(decl.value)
666
+ )
667
+ for (const value of spacingValues(decl.value))
668
+ if (!approvedSpacing.has(value.pixels))
669
+ diagnostics.push(
670
+ diagnostic(
671
+ 'KUI-L006',
672
+ at,
673
+ `${decl.prop}: ${value.text} is outside the approved Kerf spacing scale.`,
674
+ {
675
+ property: decl.prop,
676
+ value: decl.value,
677
+ pixels: value.pixels,
678
+ },
679
+ ),
680
+ );
681
+ for (const className of classes) {
682
+ const record = cssFacts.get(className) ?? {
683
+ scroll: false,
684
+ inset: false,
685
+ };
686
+ if (
687
+ /^overflow(?:-|$)/.test(decl.prop) &&
688
+ /\b(?:auto|scroll)\b/.test(decl.value)
689
+ )
690
+ record.scroll = true;
691
+ if (/^padding(?:-|$)/.test(decl.prop) && decl.value !== '0')
692
+ record.inset = true;
693
+ cssFacts.set(className, record);
694
+ }
695
+ });
696
+ });
697
+ }
698
+
699
+ function literalClasses(attribute) {
700
+ if (!attribute?.initializer) return { values: [], dynamic: false };
701
+ const value = attribute.initializer;
702
+ if (ts.isStringLiteral(value))
703
+ return { values: value.text.split(/\s+/).filter(Boolean), dynamic: false };
704
+ if (
705
+ ts.isJsxExpression(value) &&
706
+ value.expression &&
707
+ ts.isStringLiteralLike(value.expression)
708
+ )
709
+ return {
710
+ values: value.expression.text.split(/\s+/).filter(Boolean),
711
+ dynamic: false,
712
+ };
713
+ return { values: [], dynamic: true };
714
+ }
715
+
716
+ function cssPreferred(contract) {
717
+ return (
718
+ [
719
+ ...(contract.canonicalShorthands ?? []).map((item) => `\`${item}\``),
720
+ ...(contract.helpers ?? []).map((item) => `\`${item}()\``),
721
+ ].join(' or ') || 'a cataloged component prop'
722
+ );
723
+ }
724
+
725
+ function objectProperty(object, name) {
726
+ return object?.properties.find(
727
+ (property) =>
728
+ ts.isPropertyAssignment(property) &&
729
+ property.name.getText().replaceAll(/["']/g, '') === name,
730
+ );
731
+ }
732
+
733
+ function nestedCssValues(value, tail) {
734
+ if (!tail) return value ? [value] : [];
735
+ if (!value || !ts.isArrayLiteralExpression(value)) return [];
736
+ return value.elements.flatMap((element) => {
737
+ if (!ts.isObjectLiteralExpression(element)) return [];
738
+ const property = objectProperty(element, tail);
739
+ return property ? [property.initializer] : [];
740
+ });
741
+ }
742
+
743
+ function jsxCssValues(opening, path) {
744
+ const [head, tail] = path.split('[].');
745
+ const attribute = opening.attributes.properties.find(
746
+ (item) => ts.isJsxAttribute(item) && item.name.getText() === head,
747
+ );
748
+ if (!attribute?.initializer) return [];
749
+ const value = ts.isJsxExpression(attribute.initializer)
750
+ ? attribute.initializer.expression
751
+ : attribute.initializer;
752
+ return nestedCssValues(value, tail);
753
+ }
754
+
755
+ function callCssValues(call, path) {
756
+ const options = call.arguments[0];
757
+ if (!options || !ts.isObjectLiteralExpression(options)) return [];
758
+ const [head, tail] = path.split('[].');
759
+ const property = objectProperty(options, head);
760
+ return property ? nestedCssValues(property.initializer, tail) : [];
761
+ }
762
+
763
+ function cssLiteral(value) {
764
+ if (
765
+ ts.isStringLiteralLike(value) ||
766
+ ts.isNoSubstitutionTemplateLiteral(value)
767
+ )
768
+ return value.text;
769
+ return undefined;
770
+ }
771
+
772
+ function importedName(expression, helperImports, namespaces, packageName) {
773
+ let helper;
774
+ if (ts.isIdentifier(expression)) helper = helperImports.get(expression.text);
775
+ else if (
776
+ ts.isPropertyAccessExpression(expression) &&
777
+ ts.isIdentifier(expression.expression) &&
778
+ namespaces.has(expression.expression.text)
779
+ )
780
+ helper = {
781
+ imported: expression.name.text,
782
+ source: namespaces.get(expression.expression.text),
783
+ };
784
+ else helper = undefined;
785
+ if (!helper) return undefined;
786
+ return helper.source === packageName ||
787
+ helper.source.startsWith(`${packageName}/`)
788
+ ? helper.imported
789
+ : undefined;
790
+ }
791
+
792
+ function inspectCssValues(
793
+ file,
794
+ source,
795
+ entry,
796
+ valuesFor,
797
+ helperImports,
798
+ namespaces,
799
+ diagnostics,
800
+ ) {
801
+ for (const contract of entry.cssValueProps ?? []) {
802
+ const preferred = cssPreferred(contract);
803
+ for (const value of valuesFor(contract.path)) {
804
+ const at = location(file, value, source);
805
+ if (
806
+ contract.grammar === 'declarations' &&
807
+ contract.rawPolicy !== 'allow'
808
+ ) {
809
+ diagnostics.push(
810
+ diagnostic(
811
+ 'KUI-L016',
812
+ at,
813
+ `\`${entry.name}.${contract.path}\` is a forbidden declaration-list escape; use \`className\`, public tokens, or cataloged props.`,
814
+ { component: entry.key, path: contract.path },
815
+ ),
816
+ );
817
+ continue;
818
+ }
819
+ const literal = cssLiteral(value);
820
+ if (literal !== undefined) {
821
+ if (contract.exceptionalShorthands?.includes(literal)) {
822
+ diagnostics.push(
823
+ diagnostic(
824
+ 'KUI-L017',
825
+ at,
826
+ `\`${literal}\` is exceptional for \`${entry.name}.${contract.path}\`; prefer ${preferred} unless the off-scale choice is deliberate.`,
827
+ { component: entry.key, path: contract.path, value: literal },
828
+ ),
829
+ );
830
+ } else if (
831
+ !contract.shorthands?.includes(literal) &&
832
+ contract.rawPolicy !== 'allow'
833
+ ) {
834
+ diagnostics.push(
835
+ diagnostic(
836
+ 'KUI-L013',
837
+ at,
838
+ `\`${entry.name}.${contract.path}\` uses ${contract.grammar} grammar; replace raw \`${literal}\` with ${preferred}.`,
839
+ { component: entry.key, path: contract.path, value: literal },
840
+ ),
841
+ );
842
+ }
843
+ continue;
844
+ }
845
+ if (!ts.isCallExpression(value)) continue;
846
+ const helper = importedName(
847
+ value.expression,
848
+ helperImports,
849
+ namespaces,
850
+ entry.package,
851
+ );
852
+ if (!helper) continue;
853
+ if (contract.nonStandaloneHelpers?.includes(helper))
854
+ diagnostics.push(
855
+ diagnostic(
856
+ 'KUI-L015',
857
+ at,
858
+ `\`${helper}()\` is not standalone for \`${entry.name}.${contract.path}\`; wrap it with an accepted composer such as \`calc()\`.`,
859
+ { component: entry.key, path: contract.path, helper },
860
+ ),
861
+ );
862
+ else if (
863
+ !contract.helpers?.includes(helper) &&
864
+ contract.unsafeHelper !== helper
865
+ )
866
+ diagnostics.push(
867
+ diagnostic(
868
+ 'KUI-L014',
869
+ at,
870
+ `\`${helper}()\` has the wrong grammar for \`${entry.name}.${contract.path}\`; use ${preferred}.`,
871
+ { component: entry.key, path: contract.path, helper },
872
+ ),
873
+ );
874
+ }
875
+ }
876
+ }
877
+
878
+ function inspectTsx(
879
+ file,
880
+ sourceText,
881
+ facts,
882
+ cssFacts,
883
+ diagnostics,
884
+ adoption = false,
885
+ isForeign = () => true,
886
+ ) {
887
+ const source = ts.createSourceFile(
888
+ file,
889
+ sourceText,
890
+ ts.ScriptTarget.Latest,
891
+ true,
892
+ ts.ScriptKind.TSX,
893
+ );
894
+ const imports = new Map();
895
+ const helperImports = new Map();
896
+ const namespaces = new Map();
897
+ for (const statement of source.statements) {
898
+ if (
899
+ !ts.isImportDeclaration(statement) ||
900
+ !ts.isStringLiteral(statement.moduleSpecifier)
901
+ )
902
+ continue;
903
+ const module = statement.moduleSpecifier.text;
904
+ const importClause = statement.importClause;
905
+ if (importClause?.name)
906
+ helperImports.set(importClause.name.text, {
907
+ imported: 'default',
908
+ source: module,
909
+ });
910
+ const bindings = statement.importClause?.namedBindings;
911
+ if (bindings && ts.isNamespaceImport(bindings)) {
912
+ namespaces.set(bindings.name.text, module);
913
+ continue;
914
+ }
915
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
916
+ for (const item of bindings.elements) {
917
+ const exported = item.propertyName?.text ?? item.name.text;
918
+ helperImports.set(item.name.text, { imported: exported, source: module });
919
+ const entry = resolveExportEntry(
920
+ facts.exportEntries.get(exported),
921
+ exported,
922
+ module,
923
+ );
924
+ if (entry) imports.set(item.name.text, entry);
925
+ }
926
+ }
927
+ const stack = [];
928
+ const visit = (node) => {
929
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
930
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
931
+ const tag = opening.tagName.getText(source);
932
+ const namespaceEntry =
933
+ ts.isPropertyAccessExpression(opening.tagName) &&
934
+ ts.isIdentifier(opening.tagName.expression)
935
+ ? resolveExportEntry(
936
+ facts.exportEntries.get(opening.tagName.name.text),
937
+ opening.tagName.name.text,
938
+ namespaces.get(opening.tagName.expression.text),
939
+ )
940
+ : undefined;
941
+ const entry = imports.get(tag) ?? namespaceEntry;
942
+ const classAttribute = opening.attributes.properties.find(
943
+ (item) =>
944
+ ts.isJsxAttribute(item) &&
945
+ ['class', 'className'].includes(item.name.getText(source)),
946
+ );
947
+ const classes = literalClasses(classAttribute);
948
+ const at = location(file, opening, source);
949
+ if (entry)
950
+ inspectCssValues(
951
+ file,
952
+ source,
953
+ entry,
954
+ (path) => jsxCssValues(opening, path),
955
+ helperImports,
956
+ namespaces,
957
+ diagnostics,
958
+ );
959
+ if (entry && isForeign(entry))
960
+ for (const className of classes.values) {
961
+ if (facts.ownership.classOwners.has(className)) continue;
962
+ const styled = cssFacts.get(className)?.styled;
963
+ if (!styled) continue;
964
+ diagnostics.push(
965
+ diagnostic(
966
+ 'KUI-L022',
967
+ at,
968
+ `\`${className}\` is placed on ${entry.name}'s root and styled by ${basename(styled.stylesheet)}:${styled.line} (\`${styled.selector}\`), which restyles ${componentLabel(entry)}; components own their styles. Configure it through ${configurationFor(entry)}, or wrap it in your own element and style that. ${reportGap(entry)}`,
969
+ {
970
+ tag,
971
+ component: entry.key,
972
+ className,
973
+ stylesheet: styled.stylesheet,
974
+ line: styled.line,
975
+ selector: styled.selector,
976
+ },
977
+ undefined,
978
+ adoption,
979
+ ),
980
+ );
981
+ }
982
+ if (classes.dynamic)
983
+ diagnostics.push(
984
+ diagnostic(
985
+ 'KUI-L008',
986
+ at,
987
+ 'Dynamic class expression was left unclassified; review it against cataloged public classes.',
988
+ { tag },
989
+ ),
990
+ );
991
+ const owners = classes.values.flatMap(
992
+ (className) => facts.classEntries.get(className) ?? [],
993
+ );
994
+ for (const property of ['margin', 'border', 'padding']) {
995
+ const selfOwners = owners.filter(
996
+ (owner) => owner.layout?.geometry?.[property] === 'self',
997
+ );
998
+ if (new Set(selfOwners.map((owner) => owner.key)).size > 1)
999
+ diagnostics.push(
1000
+ diagnostic(
1001
+ 'KUI-L003',
1002
+ at,
1003
+ `One element combines multiple ${property} owners: ${selfOwners.map((owner) => owner.key).join(', ')}.`,
1004
+ { property, classes: classes.values },
1005
+ selfOwners.map((owner) => owner.key),
1006
+ ),
1007
+ );
1008
+ }
1009
+ const scroll = classes.values.some(
1010
+ (className) => cssFacts.get(className)?.scroll,
1011
+ );
1012
+ const inset = classes.values.some(
1013
+ (className) => cssFacts.get(className)?.inset,
1014
+ );
1015
+ const parent = stack.at(-1);
1016
+ if (scroll && parent?.scroll)
1017
+ diagnostics.push(
1018
+ diagnostic(
1019
+ 'KUI-L007',
1020
+ at,
1021
+ 'A scroll-owning element is nested inside another declared scroll owner.',
1022
+ { classes: classes.values },
1023
+ [parent.label, entry?.key ?? tag],
1024
+ ),
1025
+ );
1026
+ if (inset && parent?.inset)
1027
+ diagnostics.push(
1028
+ diagnostic(
1029
+ 'KUI-L004',
1030
+ at,
1031
+ 'Nested elements both add content padding; confirm that the repeated inset is intentional.',
1032
+ { classes: classes.values },
1033
+ [parent.label, entry?.key ?? tag],
1034
+ ),
1035
+ );
1036
+ stack.push({
1037
+ scroll: scroll || parent?.scroll,
1038
+ inset: inset || parent?.inset,
1039
+ label: entry?.key ?? tag,
1040
+ });
1041
+ if (ts.isJsxElement(node))
1042
+ for (const child of node.children) visit(child);
1043
+ stack.pop();
1044
+ return;
1045
+ }
1046
+ if (ts.isCallExpression(node)) {
1047
+ const entry = ts.isIdentifier(node.expression)
1048
+ ? imports.get(node.expression.text)
1049
+ : ts.isPropertyAccessExpression(node.expression) &&
1050
+ ts.isIdentifier(node.expression.expression)
1051
+ ? resolveExportEntry(
1052
+ facts.exportEntries.get(node.expression.name.text),
1053
+ node.expression.name.text,
1054
+ namespaces.get(node.expression.expression.text),
1055
+ )
1056
+ : undefined;
1057
+ if (entry)
1058
+ inspectCssValues(
1059
+ file,
1060
+ source,
1061
+ entry,
1062
+ (path) => callCssValues(node, path),
1063
+ helperImports,
1064
+ namespaces,
1065
+ diagnostics,
1066
+ );
1067
+ }
1068
+ ts.forEachChild(node, visit);
1069
+ };
1070
+ visit(source);
1071
+ }
1072
+
1073
+ export async function analyzeUiProject({
1074
+ root: requestedRoot = process.cwd(),
1075
+ paths,
1076
+ knownRules = [],
1077
+ adoption = false,
1078
+ profile: packageProfile = resolve(
1079
+ import.meta.dirname,
1080
+ '../ai/application-ui-profile.defaults.json',
1081
+ ),
1082
+ } = {}) {
1083
+ const root = resolve(requestedRoot);
1084
+ const files = await collectFiles(root, paths);
1085
+ const diagnosticDecisions = new Map();
1086
+ const contents = new Map(
1087
+ await Promise.all(
1088
+ files.map(async (file) => [file, await readFile(file, 'utf8')]),
1089
+ ),
1090
+ );
1091
+ const discoveredFiles = new Set(files);
1092
+ for (let index = 0; index < files.length; index += 1) {
1093
+ const file = files[index];
1094
+ for (const dependency of relativeStyleImports(file, contents.get(file))) {
1095
+ const projectPath = relative(root, dependency);
1096
+ if (
1097
+ discoveredFiles.has(dependency) ||
1098
+ projectPath === '..' ||
1099
+ projectPath.startsWith('../')
1100
+ )
1101
+ continue;
1102
+ try {
1103
+ const source = await readFile(dependency, 'utf8');
1104
+ discoveredFiles.add(dependency);
1105
+ files.push(dependency);
1106
+ contents.set(dependency, source);
1107
+ } catch {
1108
+ // TypeScript or the CSS toolchain reports missing imports; analysis stays read-only.
1109
+ }
1110
+ }
1111
+ }
1112
+ files.sort();
1113
+ const profileContexts = new Map();
1114
+ const loadContext = async (file) => {
1115
+ const startDirectory = file === root ? root : dirname(file);
1116
+ if (!profileContexts.has(startDirectory))
1117
+ profileContexts.set(
1118
+ startDirectory,
1119
+ (async () => {
1120
+ const profileResult = await loadApplicationUiProfile({
1121
+ workspaceRoot: root,
1122
+ startDirectory,
1123
+ packageProfile,
1124
+ knownRules: [...Object.keys(UI_ANALYSIS_RULES), ...knownRules],
1125
+ });
1126
+ const entries = await loadCatalogs(profileResult);
1127
+ return {
1128
+ profileResult,
1129
+ facts: catalogFacts(entries),
1130
+ startDirectory,
1131
+ };
1132
+ })(),
1133
+ );
1134
+ return profileContexts.get(startDirectory);
1135
+ };
1136
+ const contexts = new Map(
1137
+ await Promise.all(
1138
+ files.map(async (file) => [file, await loadContext(file)]),
1139
+ ),
1140
+ );
1141
+ if (!files.length) await loadContext(root);
1142
+ const imports = new Map(
1143
+ files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
1144
+ );
1145
+ // A stylesheet or source file belongs to the package whose manifest is
1146
+ // nearest it; entries of every other catalog package are foreign to it.
1147
+ const packageNames = new Map();
1148
+ const packageNameOf = async (directory) => {
1149
+ if (!packageNames.has(directory))
1150
+ packageNames.set(
1151
+ directory,
1152
+ (async () => {
1153
+ try {
1154
+ const manifest = JSON.parse(
1155
+ await readFile(resolve(directory, 'package.json'), 'utf8'),
1156
+ );
1157
+ if (typeof manifest.name === 'string') return manifest.name;
1158
+ } catch {
1159
+ // No readable manifest here; keep walking up.
1160
+ }
1161
+ const parent = dirname(directory);
1162
+ const inside = relative(root, parent);
1163
+ if (
1164
+ parent === directory ||
1165
+ inside === '..' ||
1166
+ inside.startsWith('../')
1167
+ )
1168
+ return undefined;
1169
+ return packageNameOf(parent);
1170
+ })(),
1171
+ );
1172
+ return packageNames.get(directory);
1173
+ };
1174
+ const foreignTo = async (file) => {
1175
+ const own = await packageNameOf(dirname(file));
1176
+ return (entry) => entry.package !== own;
1177
+ };
1178
+ const styleFacts = new Map();
1179
+ const recordDiagnostics = (items, context) => {
1180
+ for (const item of items) {
1181
+ const key = JSON.stringify([
1182
+ item.ruleId,
1183
+ item.location,
1184
+ item.message,
1185
+ item.evidence,
1186
+ item.chain,
1187
+ ]);
1188
+ const decision = diagnosticDecisions.get(key) ?? {
1189
+ item,
1190
+ active: false,
1191
+ suppressed: false,
1192
+ activeConsumers: new Set(),
1193
+ suppressedConsumers: new Set(),
1194
+ };
1195
+ const consumer =
1196
+ relative(root, context.startDirectory).replaceAll('\\', '/') || '.';
1197
+ if (isSuppressed(item, context.profileResult.profile, root)) {
1198
+ decision.suppressed = true;
1199
+ decision.suppressedConsumers.add(consumer);
1200
+ } else {
1201
+ decision.active = true;
1202
+ decision.activeConsumers.add(consumer);
1203
+ }
1204
+ diagnosticDecisions.set(key, decision);
1205
+ }
1206
+ };
1207
+ for (const file of files.filter((item) => item.endsWith('.css'))) {
1208
+ const context = contexts.get(file);
1209
+ const fileFacts = new Map();
1210
+ await inspectCss(
1211
+ file,
1212
+ contents.get(file),
1213
+ context.facts,
1214
+ [],
1215
+ fileFacts,
1216
+ adoption,
1217
+ );
1218
+ styleFacts.set(file, fileFacts);
1219
+ }
1220
+ const styleConsumers = new Map(
1221
+ [...styleFacts.keys()].map((file) => [file, new Set()]),
1222
+ );
1223
+ // KUI-L018 accepts an on-loud from another stylesheet the same entry loads:
1224
+ // every stylesheet reachable from one file (a source module's CSS imports,
1225
+ // or a stylesheet with its @imports) is loaded together.
1226
+ const onLoudByStyle = new Map();
1227
+ for (const style of styleFacts.keys()) {
1228
+ let parsed;
1229
+ try {
1230
+ parsed = postcss.parse(contents.get(style), { from: style });
1231
+ } catch {
1232
+ continue;
1233
+ }
1234
+ onLoudByStyle.set(
1235
+ style,
1236
+ collectLoudDeclarations(style, parsed).filter(
1237
+ ({ kind }) => kind === 'on-loud',
1238
+ ),
1239
+ );
1240
+ }
1241
+ const coLoadedStyles = new Map(
1242
+ [...styleFacts.keys()].map((style) => [style, new Set()]),
1243
+ );
1244
+ for (const file of files) {
1245
+ const loaded = new Set(reachableStyleFiles(file, imports, styleFacts));
1246
+ if (styleFacts.has(file)) loaded.add(file);
1247
+ for (const style of loaded)
1248
+ for (const sibling of loaded)
1249
+ if (sibling !== style) coLoadedStyles.get(style).add(sibling);
1250
+ }
1251
+ const siblingOnLoud = (style) =>
1252
+ [...coLoadedStyles.get(style)]
1253
+ .sort()
1254
+ .flatMap((sibling) => onLoudByStyle.get(sibling) ?? []);
1255
+ for (const file of files.filter((item) =>
1256
+ sourceExtensions.has(extname(item)),
1257
+ )) {
1258
+ const context = contexts.get(file);
1259
+ for (const style of reachableStyleFiles(file, imports, styleFacts))
1260
+ styleConsumers.get(style).add(context);
1261
+ const fileDiagnostics = [];
1262
+ inspectTsx(
1263
+ file,
1264
+ contents.get(file),
1265
+ context.facts,
1266
+ reachableStyleFacts(file, imports, styleFacts),
1267
+ fileDiagnostics,
1268
+ adoption,
1269
+ await foreignTo(file),
1270
+ );
1271
+ recordDiagnostics(fileDiagnostics, context);
1272
+ }
1273
+ for (const [file, consumers] of styleConsumers) {
1274
+ const applicableContexts = consumers.size
1275
+ ? consumers
1276
+ : new Set([contexts.get(file)]);
1277
+ for (const context of applicableContexts) {
1278
+ const fileDiagnostics = [];
1279
+ await inspectCss(
1280
+ file,
1281
+ contents.get(file),
1282
+ context.facts,
1283
+ fileDiagnostics,
1284
+ new Map(),
1285
+ adoption,
1286
+ siblingOnLoud(file),
1287
+ await foreignTo(file),
1288
+ );
1289
+ recordDiagnostics(fileDiagnostics, context);
1290
+ }
1291
+ }
1292
+
1293
+ const active = [...diagnosticDecisions.values()]
1294
+ .filter((decision) => decision.active)
1295
+ .map((decision) => {
1296
+ if (
1297
+ !decision.suppressedConsumers.size &&
1298
+ decision.activeConsumers.size <= 1
1299
+ )
1300
+ return decision.item;
1301
+ return {
1302
+ ...decision.item,
1303
+ evidence: {
1304
+ ...decision.item.evidence,
1305
+ consumers: {
1306
+ active: [...decision.activeConsumers].sort(),
1307
+ suppressed: [...decision.suppressedConsumers].sort(),
1308
+ },
1309
+ },
1310
+ };
1311
+ });
1312
+ const suppressed = [...diagnosticDecisions.values()].filter(
1313
+ (decision) => !decision.active && decision.suppressed,
1314
+ ).length;
1315
+ active.sort(
1316
+ (a, b) =>
1317
+ a.location.file.localeCompare(b.location.file) ||
1318
+ a.location.line - b.location.line ||
1319
+ a.ruleId.localeCompare(b.ruleId),
1320
+ );
1321
+ const portablePath = (file) => {
1322
+ if (!file || !isAbsolute(file)) return file;
1323
+ const path = relative(root, file).replaceAll('\\', '/');
1324
+ return path === '..' || path.startsWith('../')
1325
+ ? `<external>/${basename(file)}`
1326
+ : path;
1327
+ };
1328
+ const resolvedContexts = await Promise.all(profileContexts.values());
1329
+ const profileFiles = [
1330
+ ...new Set(
1331
+ resolvedContexts.flatMap(
1332
+ ({ profileResult }) => profileResult.files ?? [],
1333
+ ),
1334
+ ),
1335
+ ];
1336
+ const profilePackageRoot = dirname(profileFiles[0] ?? root);
1337
+ const portableMessage = (message) =>
1338
+ String(message)
1339
+ .replaceAll(root, '<repo-root>')
1340
+ .replaceAll(profilePackageRoot, '<package-root>');
1341
+ const profileDiagnostics = [
1342
+ ...new Map(
1343
+ resolvedContexts
1344
+ .flatMap(({ profileResult }) => profileResult.diagnostics ?? [])
1345
+ .map((item) => [
1346
+ JSON.stringify([item.code, item.source, item.path, item.message]),
1347
+ {
1348
+ ...item,
1349
+ source: portablePath(item.source),
1350
+ message: portableMessage(item.message),
1351
+ },
1352
+ ]),
1353
+ ).values(),
1354
+ ];
1355
+ return {
1356
+ schemaVersion: UI_ANALYSIS_SCHEMA_VERSION,
1357
+ tool: { name: '@kerfjs/ui analyzer', version: 1 },
1358
+ root: '.',
1359
+ files: files.map((file) => relative(root, file).replaceAll('\\', '/')),
1360
+ profile: {
1361
+ files: profileFiles.map(portablePath),
1362
+ diagnostics: profileDiagnostics,
1363
+ },
1364
+ diagnostics: active.map((item) => ({
1365
+ ...item,
1366
+ location: {
1367
+ ...item.location,
1368
+ file: portablePath(item.location.file),
1369
+ },
1370
+ ...(item.evidence?.stylesheet
1371
+ ? {
1372
+ evidence: {
1373
+ ...item.evidence,
1374
+ stylesheet: portablePath(item.evidence.stylesheet),
1375
+ },
1376
+ }
1377
+ : {}),
1378
+ })),
1379
+ summary: {
1380
+ errors:
1381
+ active.filter((item) => item.severity === 'error').length +
1382
+ profileDiagnostics.length,
1383
+ review: active.filter((item) => item.severity === 'review').length,
1384
+ suppressed,
1385
+ },
1386
+ };
1387
+ }
1388
+
1389
+ export function formatUiAnalysisText(report) {
1390
+ const lines = report.profile.diagnostics.map(
1391
+ (item) =>
1392
+ `${item.source ?? '<profile>'}:1:1 error ${item.code} ${item.message}`,
1393
+ );
1394
+ lines.push(
1395
+ ...report.diagnostics.map(
1396
+ (item) =>
1397
+ `${item.location.file}:${item.location.line}:${item.location.column} ${item.severity} ${item.ruleId} ${item.message}`,
1398
+ ),
1399
+ );
1400
+ lines.push(
1401
+ `Kerf UI analysis: ${report.summary.errors} error(s), ${report.summary.review} review finding(s), ${report.summary.suppressed} suppressed.`,
1402
+ );
1403
+ return lines.join('\n');
1404
+ }
1405
+
1406
+ export function formatUiAnalysisSarif(report) {
1407
+ return {
1408
+ version: '2.1.0',
1409
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
1410
+ runs: [
1411
+ {
1412
+ tool: {
1413
+ driver: {
1414
+ name: report.tool.name,
1415
+ version: String(report.tool.version),
1416
+ rules: Object.entries(UI_ANALYSIS_RULES).map(([id, rule]) => ({
1417
+ id,
1418
+ shortDescription: { text: rule.title },
1419
+ })),
1420
+ },
1421
+ },
1422
+ results: [
1423
+ ...report.profile.diagnostics.map((item) => ({
1424
+ ruleId: item.code,
1425
+ level: 'error',
1426
+ message: { text: item.message },
1427
+ locations: [
1428
+ {
1429
+ physicalLocation: {
1430
+ artifactLocation: { uri: item.source ?? '<profile>' },
1431
+ region: { startLine: 1, startColumn: 1 },
1432
+ },
1433
+ },
1434
+ ],
1435
+ properties: { path: item.path },
1436
+ })),
1437
+ ...report.diagnostics.map((item) => ({
1438
+ ruleId: item.ruleId,
1439
+ level: item.severity === 'error' ? 'error' : 'warning',
1440
+ message: { text: item.message },
1441
+ locations: [
1442
+ {
1443
+ physicalLocation: {
1444
+ artifactLocation: { uri: item.location.file },
1445
+ region: {
1446
+ startLine: item.location.line,
1447
+ startColumn: item.location.column,
1448
+ },
1449
+ },
1450
+ },
1451
+ ],
1452
+ properties: { evidence: item.evidence, chain: item.chain },
1453
+ })),
1454
+ ],
1455
+ },
1456
+ ],
1457
+ };
1458
+ }