@kerfjs/ui 5.0.0-beta.8 → 5.0.0-beta.81

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 (807) hide show
  1. package/README.md +546 -139
  2. package/ai/application-ui-diagnostic-ids-v1.json +113 -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 +4280 -457
  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 +520 -0
  20. package/ai/component-composition.d.ts +129 -0
  21. package/ai/component-composition.json +7401 -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 +96 -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 +100 -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 +91 -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 +106 -0
  44. package/ai/components/loading-spinner.md +73 -0
  45. package/ai/components/lucide-icon.md +97 -0
  46. package/ai/components/nav-stack.md +105 -0
  47. package/ai/components/pane.md +104 -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 +115 -0
  63. package/ai/components/skeleton.md +84 -0
  64. package/ai/components/spacer.md +86 -0
  65. package/ai/components/split-view.md +103 -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 +87 -0
  69. package/ai/components/tab-bar.md +113 -0
  70. package/ai/components/tab-navigator.md +95 -0
  71. package/ai/components/tabs.md +95 -0
  72. package/ai/components/text.md +87 -0
  73. package/ai/components/token-search-field.md +107 -0
  74. package/ai/components/toolbar-control-group.md +121 -0
  75. package/ai/components/toolbar-text.md +98 -0
  76. package/ai/components/toolbar.md +101 -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 +119 -0
  149. package/ai/public-api-signatures-v1.md +2821 -250
  150. package/ai/skill.md +354 -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 +429 -0
  155. package/analyzer/index.d.mts +53 -0
  156. package/analyzer/index.mjs +2346 -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 +54 -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} +5 -3
  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/nav-stack.js +11 -0
  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 +2 -1
  189. package/dist/browser/spacer.js +3 -0
  190. package/dist/browser/split-view.js +13 -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-navigator.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 +541 -0
  203. package/dist/catalog.js.map +1 -0
  204. package/dist/chip.d.ts +36 -0
  205. package/dist/chip.js +3 -0
  206. package/dist/chip.js.map +1 -0
  207. package/dist/chunk-2GOCOLQL.js +270 -0
  208. package/dist/chunk-2GOCOLQL.js.map +1 -0
  209. package/dist/chunk-3CXRQAYT.js +288 -0
  210. package/dist/chunk-3CXRQAYT.js.map +1 -0
  211. package/dist/chunk-3RE2JMZU.js +48 -0
  212. package/dist/chunk-3RE2JMZU.js.map +1 -0
  213. package/dist/chunk-3SXWV2IR.js +295 -0
  214. package/dist/chunk-3SXWV2IR.js.map +1 -0
  215. package/dist/chunk-3XPGJUUQ.js +37 -0
  216. package/dist/chunk-3XPGJUUQ.js.map +1 -0
  217. package/dist/chunk-4Y4L5YEI.js +66 -0
  218. package/dist/chunk-4Y4L5YEI.js.map +1 -0
  219. package/dist/chunk-5BZBIRQG.js +452 -0
  220. package/dist/chunk-5BZBIRQG.js.map +1 -0
  221. package/dist/chunk-5D3WDMIK.js +71 -0
  222. package/dist/chunk-5D3WDMIK.js.map +1 -0
  223. package/dist/chunk-62VMNBV2.js +779 -0
  224. package/dist/chunk-62VMNBV2.js.map +1 -0
  225. package/dist/chunk-75I3SNUJ.js +44 -0
  226. package/dist/chunk-75I3SNUJ.js.map +1 -0
  227. package/dist/chunk-7QQ3HE47.js +17 -0
  228. package/dist/chunk-7QQ3HE47.js.map +1 -0
  229. package/dist/chunk-7TE6XDFI.js +91 -0
  230. package/dist/chunk-7TE6XDFI.js.map +1 -0
  231. package/dist/chunk-A7DYEERU.js +59 -0
  232. package/dist/chunk-A7DYEERU.js.map +1 -0
  233. package/dist/chunk-AN5IWMTM.js +24 -0
  234. package/dist/chunk-AN5IWMTM.js.map +1 -0
  235. package/dist/chunk-ARZ4RSQO.js +48 -0
  236. package/dist/chunk-ARZ4RSQO.js.map +1 -0
  237. package/dist/chunk-BJLXE56E.js +135 -0
  238. package/dist/chunk-BJLXE56E.js.map +1 -0
  239. package/dist/chunk-BYHGRKC3.js +44 -0
  240. package/dist/chunk-BYHGRKC3.js.map +1 -0
  241. package/dist/chunk-CEJ5FK6G.js +288 -0
  242. package/dist/chunk-CEJ5FK6G.js.map +1 -0
  243. package/dist/chunk-CVKIADIS.js +172 -0
  244. package/dist/chunk-CVKIADIS.js.map +1 -0
  245. package/dist/chunk-DQDTZE4W.js +70 -0
  246. package/dist/chunk-DQDTZE4W.js.map +1 -0
  247. package/dist/chunk-EFDCALR3.js +66 -0
  248. package/dist/chunk-EFDCALR3.js.map +1 -0
  249. package/dist/chunk-FL6J644M.js +116 -0
  250. package/dist/chunk-FL6J644M.js.map +1 -0
  251. package/dist/chunk-GS2QD3N7.js +14 -0
  252. package/dist/chunk-GS2QD3N7.js.map +1 -0
  253. package/dist/chunk-H45DAZBL.js +491 -0
  254. package/dist/chunk-H45DAZBL.js.map +1 -0
  255. package/dist/chunk-H7ZJEI3H.js +52 -0
  256. package/dist/chunk-H7ZJEI3H.js.map +1 -0
  257. package/dist/chunk-HFLGWMG3.js +138 -0
  258. package/dist/chunk-HFLGWMG3.js.map +1 -0
  259. package/dist/chunk-HFQDSSED.js +188 -0
  260. package/dist/chunk-HFQDSSED.js.map +1 -0
  261. package/dist/{chunk-56RJXPHG.js → chunk-I4SDIMFA.js} +7 -4
  262. package/dist/chunk-I4SDIMFA.js.map +1 -0
  263. package/dist/chunk-IM3GKYOG.js +202 -0
  264. package/dist/chunk-IM3GKYOG.js.map +1 -0
  265. package/dist/chunk-J3AWE5I4.js +50 -0
  266. package/dist/chunk-J3AWE5I4.js.map +1 -0
  267. package/dist/chunk-JOR5LGYE.js +55 -0
  268. package/dist/chunk-JOR5LGYE.js.map +1 -0
  269. package/dist/chunk-JZM3G6RT.js +149 -0
  270. package/dist/chunk-JZM3G6RT.js.map +1 -0
  271. package/dist/{chunk-7BR3DO2A.js → chunk-LAZLBC2Q.js} +14 -5
  272. package/dist/chunk-LAZLBC2Q.js.map +1 -0
  273. package/dist/chunk-LQKY5S4F.js +506 -0
  274. package/dist/chunk-LQKY5S4F.js.map +1 -0
  275. package/dist/chunk-LRAIS3YB.js +158 -0
  276. package/dist/chunk-LRAIS3YB.js.map +1 -0
  277. package/dist/chunk-MXNIVEVL.js +48 -0
  278. package/dist/chunk-MXNIVEVL.js.map +1 -0
  279. package/dist/chunk-NJZMR7HM.js +135 -0
  280. package/dist/chunk-NJZMR7HM.js.map +1 -0
  281. package/dist/chunk-NMWDHZVM.js +35 -0
  282. package/dist/chunk-NMWDHZVM.js.map +1 -0
  283. package/dist/{chunk-YGN7OMLT.js → chunk-NS2TBYM4.js} +8 -5
  284. package/dist/chunk-NS2TBYM4.js.map +1 -0
  285. package/dist/chunk-NVKRZ5DI.js +222 -0
  286. package/dist/chunk-NVKRZ5DI.js.map +1 -0
  287. package/dist/chunk-NVRJ7C3W.js +83 -0
  288. package/dist/chunk-NVRJ7C3W.js.map +1 -0
  289. package/dist/chunk-O27D24CA.js +250 -0
  290. package/dist/chunk-O27D24CA.js.map +1 -0
  291. package/dist/chunk-PQQQXJGV.js +58 -0
  292. package/dist/chunk-PQQQXJGV.js.map +1 -0
  293. package/dist/chunk-RRCD3A6G.js +139 -0
  294. package/dist/chunk-RRCD3A6G.js.map +1 -0
  295. package/dist/chunk-S3ECMF23.js +33 -0
  296. package/dist/chunk-S3ECMF23.js.map +1 -0
  297. package/dist/chunk-S4ZGTDSG.js +18 -0
  298. package/dist/chunk-S4ZGTDSG.js.map +1 -0
  299. package/dist/chunk-VHXHLZ6R.js +69 -0
  300. package/dist/chunk-VHXHLZ6R.js.map +1 -0
  301. package/dist/chunk-VIGVNDXZ.js +165 -0
  302. package/dist/chunk-VIGVNDXZ.js.map +1 -0
  303. package/dist/chunk-VR7MWD2X.js +91 -0
  304. package/dist/chunk-VR7MWD2X.js.map +1 -0
  305. package/dist/chunk-Y6KT7ZTR.js +109 -0
  306. package/dist/chunk-Y6KT7ZTR.js.map +1 -0
  307. package/dist/chunk-YLJ6X3AC.js +34 -0
  308. package/dist/chunk-YLJ6X3AC.js.map +1 -0
  309. package/dist/collapsible-panel.d.ts +137 -0
  310. package/dist/collapsible-panel.js +135 -0
  311. package/dist/collapsible-panel.js.map +1 -0
  312. package/dist/content-item.d.ts +99 -0
  313. package/dist/content-item.js +4 -0
  314. package/dist/content-item.js.map +1 -0
  315. package/dist/css-values.d.ts +112 -0
  316. package/dist/css-values.js +3 -0
  317. package/dist/css-values.js.map +1 -0
  318. package/dist/device-class.js +34 -6
  319. package/dist/device-class.js.map +1 -1
  320. package/dist/disclosure-arrow.d.ts +6 -1
  321. package/dist/disclosure-arrow.js +2 -1
  322. package/dist/empty-state.d.ts +5 -2
  323. package/dist/empty-state.js +2 -1
  324. package/dist/flex-alignment-4ms8ZbV8.d.ts +5 -0
  325. package/dist/floating-toolbar.d.ts +51 -0
  326. package/dist/floating-toolbar.js +3 -0
  327. package/dist/floating-toolbar.js.map +1 -0
  328. package/dist/grid.d.ts +57 -0
  329. package/dist/grid.js +5 -0
  330. package/dist/grid.js.map +1 -0
  331. package/dist/help-tags-register.d.ts +13 -0
  332. package/dist/help-tags-register.js +9 -0
  333. package/dist/help-tags-register.js.map +1 -0
  334. package/dist/index.d.ts +40 -18
  335. package/dist/index.js +44 -23
  336. package/dist/{menu-action-row.d.ts → list-action-row.d.ts} +21 -9
  337. package/dist/list-action-row.js +8 -0
  338. package/dist/list-action-row.js.map +1 -0
  339. package/dist/list-header.d.ts +98 -0
  340. package/dist/list-header.js +10 -0
  341. package/dist/list-header.js.map +1 -0
  342. package/dist/list-inset-control.d.ts +22 -0
  343. package/dist/list-inset-control.js +3 -0
  344. package/dist/list-inset-control.js.map +1 -0
  345. package/dist/list-inset-text.d.ts +30 -0
  346. package/dist/list-inset-text.js +4 -0
  347. package/dist/list-inset-text.js.map +1 -0
  348. package/dist/list-item.d.ts +62 -0
  349. package/dist/list-item.js +8 -0
  350. package/dist/list-item.js.map +1 -0
  351. package/dist/list.d.ts +63 -0
  352. package/dist/list.js +6 -0
  353. package/dist/list.js.map +1 -0
  354. package/dist/loading-spinner.d.ts +12 -2
  355. package/dist/loading-spinner.js +2 -1
  356. package/dist/lucide-icon.d.ts +14 -2
  357. package/dist/lucide-icon.js +2 -1
  358. package/dist/nav-stack-B1Qc2e5q.d.ts +101 -0
  359. package/dist/nav-stack.d.ts +13 -38
  360. package/dist/nav-stack.js +10 -1
  361. package/dist/pane.d.ts +116 -0
  362. package/dist/pane.js +4 -0
  363. package/dist/pane.js.map +1 -0
  364. package/dist/panel-toolbar-CL9tYOkh.d.ts +72 -0
  365. package/dist/popup-menu-register.d.ts +9 -0
  366. package/dist/popup-menu-register.js +244 -0
  367. package/dist/popup-menu-register.js.map +1 -0
  368. package/dist/popup-menu.d.ts +100 -0
  369. package/dist/popup-menu.js +3 -0
  370. package/dist/popup-menu.js.map +1 -0
  371. package/dist/resizable-region.d.ts +32 -3
  372. package/dist/resizable-region.js +1 -1
  373. package/dist/row.d.ts +48 -0
  374. package/dist/row.js +6 -0
  375. package/dist/row.js.map +1 -0
  376. package/dist/segmented-control.d.ts +12 -9
  377. package/dist/segmented-control.js +1 -2
  378. package/dist/select-register.d.ts +5 -1
  379. package/dist/select-register.js +247 -3
  380. package/dist/select-register.js.map +1 -1
  381. package/dist/select.d.ts +103 -8
  382. package/dist/select.js +3 -1
  383. package/dist/semantic-content-BbzjvSu9.d.ts +14 -0
  384. package/dist/semantic-tone-CdW013KF.d.ts +4 -0
  385. package/dist/sides-BPSWde0A.d.ts +4 -0
  386. package/dist/skeleton.d.ts +14 -7
  387. package/dist/skeleton.js +2 -1
  388. package/dist/spacer.d.ts +18 -0
  389. package/dist/spacer.js +4 -0
  390. package/dist/spacer.js.map +1 -0
  391. package/dist/split-view.d.ts +44 -7
  392. package/dist/split-view.js +124 -10
  393. package/dist/split-view.js.map +1 -1
  394. package/dist/state-banner.d.ts +21 -7
  395. package/dist/state-banner.js +3 -1
  396. package/dist/styles/app-tab.css +224 -0
  397. package/dist/styles/badge.css +118 -0
  398. package/dist/styles/catalog/components/catalog-example-stack.css +9 -0
  399. package/dist/styles/catalog/components/catalog-example.css +168 -0
  400. package/dist/styles/catalog/components/catalog-stage.css +55 -0
  401. package/dist/styles/catalog/components/catalog.css +50 -0
  402. package/dist/styles/catalog.css +5 -0
  403. package/dist/styles/chip.css +138 -0
  404. package/dist/styles/collapsible-panel.css +580 -0
  405. package/dist/styles/components/actions/floating-toolbar/floating-toolbar.css +119 -0
  406. package/dist/styles/components/actions/popup-menu/popup-menu.css +285 -0
  407. package/dist/styles/components/actions/toolbar/toolbar.css +417 -0
  408. package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +891 -0
  409. package/dist/styles/components/actions/toolbar-text/toolbar-text.css +171 -0
  410. package/dist/styles/components/collections/list/list.css +133 -0
  411. package/dist/styles/components/collections/list-action-row/list-action-row.css +258 -0
  412. package/dist/styles/components/collections/list-header/list-header.css +229 -0
  413. package/dist/styles/components/collections/list-inset-control/list-inset-control.css +30 -0
  414. package/dist/styles/components/collections/list-inset-text/list-inset-text.css +29 -0
  415. package/dist/styles/components/collections/list-item/list-item.css +255 -0
  416. package/dist/styles/components/data-display/value-table/value-table.css +105 -0
  417. package/dist/styles/components/feedback/badge/badge.css +118 -0
  418. package/dist/styles/components/feedback/chip/chip.css +138 -0
  419. package/dist/styles/components/feedback/empty-state/empty-state.css +47 -0
  420. package/dist/styles/components/feedback/loading-spinner/loading-spinner.css +25 -0
  421. package/dist/styles/components/feedback/skeleton/skeleton.css +92 -0
  422. package/dist/styles/components/feedback/state-banner/state-banner.css +248 -0
  423. package/dist/styles/components/forms/segmented-control/segmented-control.css +196 -0
  424. package/dist/styles/components/forms/select/select.css +454 -0
  425. package/dist/styles/components/forms/token-search-field/token-search-field.css +392 -0
  426. package/dist/styles/components/layout/collapsible-panel/collapsible-panel.css +580 -0
  427. package/dist/styles/components/layout/grid/grid.css +85 -0
  428. package/dist/styles/components/layout/pane/pane.css +294 -0
  429. package/dist/styles/components/layout/resizable-region/resizable-region.css +545 -0
  430. package/dist/styles/components/layout/row/row.css +111 -0
  431. package/dist/styles/components/layout/spacer/spacer.css +11 -0
  432. package/dist/styles/components/layout/split-view/split-view.css +146 -0
  433. package/dist/styles/components/layout/workbench/workbench.css +1075 -0
  434. package/dist/styles/components/media/lucide-icon/lucide-icon.css +15 -0
  435. package/dist/styles/components/navigation/app-tab/app-tab.css +376 -0
  436. package/dist/styles/components/navigation/disclosure-arrow/disclosure-arrow.css +23 -0
  437. package/dist/styles/components/navigation/nav-stack/nav-stack.css +242 -0
  438. package/dist/styles/components/navigation/tab-bar/tab-bar.css +183 -0
  439. package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +237 -0
  440. package/dist/styles/components/surfaces/content-item/content-item.css +120 -0
  441. package/dist/styles/components/surfaces/sunken-panel/sunken-panel.css +34 -0
  442. package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +111 -0
  443. package/dist/styles/components/typography/text/text.css +158 -0
  444. package/dist/styles/content-item.css +120 -0
  445. package/dist/styles/document.css +33 -0
  446. package/dist/styles/floating-toolbar.css +119 -0
  447. package/dist/styles/foundation.css +187 -45
  448. package/dist/styles/grid.css +85 -0
  449. package/dist/styles/layout.css +3 -41
  450. package/dist/styles/list-action-row.css +258 -0
  451. package/dist/styles/list-header.css +229 -0
  452. package/dist/styles/list-inset-control.css +30 -0
  453. package/dist/styles/list-inset-text.css +29 -0
  454. package/dist/styles/list-item.css +255 -0
  455. package/dist/styles/list.css +133 -0
  456. package/dist/styles/loading-spinner.css +4 -0
  457. package/dist/styles/lucide-icon.css +9 -0
  458. package/dist/styles/nav-stack.css +180 -50
  459. package/dist/styles/pane.css +294 -0
  460. package/dist/styles/popup-menu.css +285 -0
  461. package/dist/styles/resizable-region.css +438 -4
  462. package/dist/styles/row.css +111 -0
  463. package/dist/styles/segmented-control.css +36 -9
  464. package/dist/styles/select.css +337 -21
  465. package/dist/styles/skeleton.css +64 -3
  466. package/dist/styles/spacer.css +11 -0
  467. package/dist/styles/split-view.css +114 -3
  468. package/dist/styles/state-banner.css +94 -19
  469. package/dist/styles/styles.css +13 -4
  470. package/dist/styles/sunken-panel.css +34 -0
  471. package/dist/styles/surface-scaffold.css +111 -0
  472. package/dist/styles/tab-bar.css +147 -9
  473. package/dist/styles/tab-navigator.css +237 -0
  474. package/dist/styles/text.css +158 -0
  475. package/dist/styles/token-search-field.css +167 -56
  476. package/dist/styles/toolbar-control-group.css +710 -30
  477. package/dist/styles/toolbar-text.css +139 -8
  478. package/dist/styles/toolbar.css +367 -11
  479. package/dist/styles/value-table.css +34 -4
  480. package/dist/styles/webawesome.css +388 -16
  481. package/dist/styles/workbench.css +988 -7
  482. package/dist/sunken-panel.d.ts +31 -0
  483. package/dist/sunken-panel.js +3 -0
  484. package/dist/sunken-panel.js.map +1 -0
  485. package/dist/surface-scaffold-register.d.ts +4 -0
  486. package/dist/surface-scaffold-register.js +59 -0
  487. package/dist/surface-scaffold-register.js.map +1 -0
  488. package/dist/surface-scaffold.d.ts +38 -0
  489. package/dist/surface-scaffold.js +3 -0
  490. package/dist/surface-scaffold.js.map +1 -0
  491. package/dist/tab-bar.d.ts +39 -6
  492. package/dist/tab-bar.js +1 -1
  493. package/dist/tab-navigator.d.ts +86 -0
  494. package/dist/tab-navigator.js +95 -0
  495. package/dist/tab-navigator.js.map +1 -0
  496. package/dist/text.d.ts +63 -0
  497. package/dist/text.js +3 -0
  498. package/dist/text.js.map +1 -0
  499. package/dist/token-search-field.d.ts +3 -43
  500. package/dist/token-search-field.js +2 -1
  501. package/dist/token-search-model-DTcTXWMS.d.ts +154 -0
  502. package/dist/token-search-model.d.ts +3 -0
  503. package/dist/token-search-model.js +3 -0
  504. package/dist/token-search-model.js.map +1 -0
  505. package/dist/toolbar-control-group.d.ts +89 -7
  506. package/dist/toolbar-control-group.js +3 -1
  507. package/dist/toolbar-text.d.ts +45 -9
  508. package/dist/toolbar-text.js +2 -1
  509. package/dist/toolbar.d.ts +59 -7
  510. package/dist/toolbar.js +1 -1
  511. package/dist/types-C9BubXfz.d.ts +117 -0
  512. package/dist/value-table.d.ts +11 -3
  513. package/dist/value-table.js +2 -1
  514. package/dist/webawesome.d.ts +8 -1
  515. package/dist/wire-catalog.d.ts +82 -0
  516. package/dist/wire-catalog.js +407 -0
  517. package/dist/wire-catalog.js.map +1 -0
  518. package/dist/wire-content-items.d.ts +11 -0
  519. package/dist/wire-content-items.js +114 -0
  520. package/dist/wire-content-items.js.map +1 -0
  521. package/dist/wire-nav-stack.d.ts +4 -1
  522. package/dist/wire-nav-stack.js +191 -15
  523. package/dist/wire-nav-stack.js.map +1 -1
  524. package/dist/wire-resizable-regions.d.ts +2 -1
  525. package/dist/wire-resizable-regions.js +2 -1
  526. package/dist/wire-scroll-dividers.d.ts +72 -0
  527. package/dist/wire-scroll-dividers.js +3 -0
  528. package/dist/wire-scroll-dividers.js.map +1 -0
  529. package/dist/wire-sidebar.d.ts +63 -0
  530. package/dist/wire-sidebar.js +231 -0
  531. package/dist/wire-sidebar.js.map +1 -0
  532. package/dist/wire-tab-bars.d.ts +19 -2
  533. package/dist/wire-tab-bars.js +1 -1
  534. package/dist/wire-tab-navigator.d.ts +11 -0
  535. package/dist/wire-tab-navigator.js +21 -0
  536. package/dist/wire-tab-navigator.js.map +1 -0
  537. package/dist/wire-token-search-fields.d.ts +65 -3
  538. package/dist/wire-token-search-fields.js +2 -1
  539. package/dist/wire-toolbar-visibility.d.ts +13 -0
  540. package/dist/wire-toolbar-visibility.js +3 -0
  541. package/dist/wire-toolbar-visibility.js.map +1 -0
  542. package/dist/wire-workbench.d.ts +116 -0
  543. package/dist/wire-workbench.js +6 -0
  544. package/dist/wire-workbench.js.map +1 -0
  545. package/dist/workbench.d.ts +246 -9
  546. package/dist/workbench.js +17 -15
  547. package/dist/workbench.js.map +1 -1
  548. package/docs/accessibility.md +497 -26
  549. package/docs/app-layouts.md +141 -10
  550. package/docs/apple-hig.md +1 -0
  551. package/docs/catalog.md +493 -0
  552. package/docs/collapsible-panel.md +382 -0
  553. package/docs/component-contract.md +736 -61
  554. package/docs/component-integration.md +75 -0
  555. package/docs/component-selection.md +619 -97
  556. package/docs/design/templates/empty-state/filtered-dark.svg +1 -0
  557. package/docs/design/templates/empty-state/filtered.svg +1 -0
  558. package/docs/design/templates/empty-state/with-action-dark.svg +1 -0
  559. package/docs/design/templates/empty-state/with-action.svg +1 -0
  560. package/docs/design/templates/empty-state-dark.svg +1 -0
  561. package/docs/design/templates/empty-state.svg +1 -0
  562. package/docs/design/templates/list/compact-dark.svg +1 -0
  563. package/docs/design/templates/list/compact.svg +1 -0
  564. package/docs/design/templates/list/stack-dark.svg +1 -0
  565. package/docs/design/templates/list/stack.svg +1 -0
  566. package/docs/design/templates/list-action-row/default-dark.svg +1 -0
  567. package/docs/design/templates/list-action-row/default.svg +1 -0
  568. package/docs/design/templates/list-action-row/selected-dark.svg +1 -0
  569. package/docs/design/templates/list-action-row/selected.svg +1 -0
  570. package/docs/design/templates/list-action-row-dark.svg +1 -0
  571. package/docs/design/templates/list-action-row.svg +1 -0
  572. package/docs/design/templates/list-dark.svg +1 -0
  573. package/docs/design/templates/list-header/action-dark.svg +1 -0
  574. package/docs/design/templates/list-header/action.svg +1 -0
  575. package/docs/design/templates/list-header/count-dark.svg +1 -0
  576. package/docs/design/templates/list-header/count.svg +1 -0
  577. package/docs/design/templates/list-header/inline-action-dark.svg +1 -0
  578. package/docs/design/templates/list-header/inline-action.svg +1 -0
  579. package/docs/design/templates/list-header/size-l-dark.svg +1 -0
  580. package/docs/design/templates/list-header/size-l.svg +1 -0
  581. package/docs/design/templates/list-header/size-m-dark.svg +1 -0
  582. package/docs/design/templates/list-header/size-m.svg +1 -0
  583. package/docs/design/templates/list-header/size-xl-dark.svg +1 -0
  584. package/docs/design/templates/list-header/size-xl.svg +1 -0
  585. package/docs/design/templates/list-header/size-xs-dark.svg +1 -0
  586. package/docs/design/templates/list-header/size-xs.svg +1 -0
  587. package/docs/design/templates/list-header/toggle-collapsed-dark.svg +1 -0
  588. package/docs/design/templates/list-header/toggle-collapsed.svg +1 -0
  589. package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -0
  590. package/docs/design/templates/list-header/toggle-expanded.svg +1 -0
  591. package/docs/design/templates/list-header-dark.svg +1 -0
  592. package/docs/design/templates/list-header.svg +1 -0
  593. package/docs/design/templates/list-item/default-dark.svg +1 -0
  594. package/docs/design/templates/list-item/default.svg +1 -0
  595. package/docs/design/templates/list-item/multiline-dark.svg +1 -0
  596. package/docs/design/templates/list-item/multiline.svg +1 -0
  597. package/docs/design/templates/list-item/selected-dark.svg +1 -0
  598. package/docs/design/templates/list-item/selected.svg +1 -0
  599. package/docs/design/templates/list-item/trailing-dark.svg +1 -0
  600. package/docs/design/templates/list-item/trailing.svg +1 -0
  601. package/docs/design/templates/list-item-dark.svg +1 -0
  602. package/docs/design/templates/list-item.svg +1 -0
  603. package/docs/design/templates/list.svg +1 -0
  604. package/docs/design/templates/segmented-control/equal-dark.svg +1 -0
  605. package/docs/design/templates/segmented-control/equal.svg +1 -0
  606. package/docs/design/templates/segmented-control/pill-small-dark.svg +1 -0
  607. package/docs/design/templates/segmented-control/pill-small.svg +1 -0
  608. package/docs/design/templates/segmented-control-dark.svg +1 -0
  609. package/docs/design/templates/segmented-control.svg +1 -0
  610. package/docs/design/templates/skeleton/avatar-dark.svg +1 -0
  611. package/docs/design/templates/skeleton/avatar.svg +1 -0
  612. package/docs/design/templates/skeleton/block-dark.svg +1 -0
  613. package/docs/design/templates/skeleton/block.svg +1 -0
  614. package/docs/design/templates/skeleton/lines-dark.svg +1 -0
  615. package/docs/design/templates/skeleton/lines.svg +1 -0
  616. package/docs/design/templates/skeleton-dark.svg +1 -0
  617. package/docs/design/templates/skeleton.svg +1 -0
  618. package/docs/design/templates/state-banner/danger-dark.svg +1 -0
  619. package/docs/design/templates/state-banner/danger.svg +1 -0
  620. package/docs/design/templates/state-banner/info-badge-dark.svg +1 -0
  621. package/docs/design/templates/state-banner/info-badge.svg +1 -0
  622. package/docs/design/templates/state-banner/info-dark.svg +1 -0
  623. package/docs/design/templates/state-banner/info.svg +1 -0
  624. package/docs/design/templates/state-banner/success-dark.svg +1 -0
  625. package/docs/design/templates/state-banner/success.svg +1 -0
  626. package/docs/design/templates/state-banner/warning-dark.svg +1 -0
  627. package/docs/design/templates/state-banner/warning.svg +1 -0
  628. package/docs/design/templates/state-banner-dark.svg +1 -0
  629. package/docs/design/templates/state-banner.svg +1 -0
  630. package/docs/design/templates/tab-bar/workspace-tabs-dark.svg +1 -0
  631. package/docs/design/templates/tab-bar/workspace-tabs.svg +1 -0
  632. package/docs/design/templates/tab-bar-dark.svg +1 -0
  633. package/docs/design/templates/tab-bar.svg +1 -0
  634. package/docs/design/templates/token-search-field/empty-dark.svg +1 -0
  635. package/docs/design/templates/token-search-field/empty.svg +1 -0
  636. package/docs/design/templates/token-search-field/tokens-dark.svg +1 -0
  637. package/docs/design/templates/token-search-field/tokens.svg +1 -0
  638. package/docs/design/templates/token-search-field-dark.svg +1 -0
  639. package/docs/design/templates/token-search-field.svg +1 -0
  640. package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -0
  641. package/docs/design/templates/toolbar/title-and-actions.svg +1 -0
  642. package/docs/design/templates/toolbar/width-visibility-320-dark.svg +1 -0
  643. package/docs/design/templates/toolbar/width-visibility-320.svg +1 -0
  644. package/docs/design/templates/toolbar/width-visibility-600-dark.svg +1 -0
  645. package/docs/design/templates/toolbar/width-visibility-600.svg +1 -0
  646. package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
  647. package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
  648. package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
  649. package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
  650. package/docs/design/templates/toolbar-control-group/icon-tile-brand-dark.svg +1 -0
  651. package/docs/design/templates/toolbar-control-group/icon-tile-brand.svg +1 -0
  652. package/docs/design/templates/toolbar-control-group/icon-tile-danger-dark.svg +1 -0
  653. package/docs/design/templates/toolbar-control-group/icon-tile-danger.svg +1 -0
  654. package/docs/design/templates/toolbar-control-group/icon-tile-neutral-dark.svg +1 -0
  655. package/docs/design/templates/toolbar-control-group/icon-tile-neutral.svg +1 -0
  656. package/docs/design/templates/toolbar-control-group/icon-tile-success-dark.svg +1 -0
  657. package/docs/design/templates/toolbar-control-group/icon-tile-success.svg +1 -0
  658. package/docs/design/templates/toolbar-control-group/icon-tile-warning-dark.svg +1 -0
  659. package/docs/design/templates/toolbar-control-group/icon-tile-warning.svg +1 -0
  660. package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
  661. package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
  662. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
  663. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
  664. package/docs/design/templates/toolbar-control-group-dark.svg +1 -0
  665. package/docs/design/templates/toolbar-control-group.svg +1 -0
  666. package/docs/design/templates/toolbar-dark.svg +1 -0
  667. package/docs/design/templates/toolbar-text/dark-tone-dark.svg +1 -0
  668. package/docs/design/templates/toolbar-text/dark-tone.svg +1 -0
  669. package/docs/design/templates/toolbar-text/default-dark.svg +1 -0
  670. package/docs/design/templates/toolbar-text/default.svg +1 -0
  671. package/docs/design/templates/toolbar-text/large-dark.svg +1 -0
  672. package/docs/design/templates/toolbar-text/large.svg +1 -0
  673. package/docs/design/templates/toolbar-text/small-dark.svg +1 -0
  674. package/docs/design/templates/toolbar-text/small.svg +1 -0
  675. package/docs/design/templates/toolbar-text-dark.svg +1 -0
  676. package/docs/design/templates/toolbar-text.svg +1 -0
  677. package/docs/design/templates/toolbar.svg +1 -0
  678. package/docs/design/templates/value-table/details-dark.svg +1 -0
  679. package/docs/design/templates/value-table/details.svg +1 -0
  680. package/docs/design/templates/value-table-dark.svg +1 -0
  681. package/docs/design/templates/value-table.svg +1 -0
  682. package/docs/design/templates.md +140 -0
  683. package/docs/design-philosophy.md +48 -20
  684. package/docs/device-class.md +10 -9
  685. package/docs/document-baseline.md +42 -0
  686. package/docs/examples/application-ui-profile.json +60 -0
  687. package/docs/examples/command-palette-adapter.tsx +69 -17
  688. package/docs/examples/component-catalog-extension.json +52 -0
  689. package/docs/examples/component-composition-extension.json +80 -0
  690. package/docs/layout.md +601 -59
  691. package/docs/nav-stack.md +154 -14
  692. package/docs/recipes.md +142 -55
  693. package/docs/split-view.md +77 -6
  694. package/docs/sunken-panel.md +93 -0
  695. package/docs/surface-scaffold.md +74 -0
  696. package/docs/tab-navigator.md +210 -0
  697. package/docs/type-contracts.md +135 -0
  698. package/docs/ui-analyzer.md +404 -0
  699. package/docs/ui-doctor.md +202 -0
  700. package/docs/ui-evaluator.md +139 -0
  701. package/docs/ux-demo.md +46 -16
  702. package/docs/webawesome-theme.md +188 -16
  703. package/docs/workbench.md +621 -12
  704. package/doctor/cli.mjs +120 -0
  705. package/doctor/component-style-checks.mjs +457 -0
  706. package/doctor/config.schema.json +94 -0
  707. package/doctor/eslint-diagnostics.d.mts +24 -0
  708. package/doctor/eslint-diagnostics.mjs +24 -0
  709. package/doctor/index.d.mts +122 -0
  710. package/doctor/index.mjs +1403 -0
  711. package/doctor/report.schema.json +118 -0
  712. package/evaluator/cli.mjs +89 -0
  713. package/evaluator/index.d.mts +90 -0
  714. package/evaluator/index.mjs +1203 -0
  715. package/evaluator/report.schema.json +152 -0
  716. package/llms.txt +510 -49
  717. package/package.json +266 -44
  718. package/scripts/component-catalog-extension-validation.mjs +28 -0
  719. package/traversal-exclusions.mjs +27 -0
  720. package/ux-demo/recipes/app-shell.tsx +205 -17
  721. package/ux-demo/recipes/collapsible-sidebar.tsx +260 -0
  722. package/ux-demo/recipes/compact-toolbar.tsx +135 -14
  723. package/ux-demo/recipes/composer-form.tsx +133 -21
  724. package/ux-demo/recipes/list-detail-dialog.tsx +286 -0
  725. package/ux-demo/recipes/list-workspace-states.tsx +183 -11
  726. package/ux-demo/recipes/loaders.ts +15 -5
  727. package/ux-demo/recipes/loading-inspector.tsx +204 -0
  728. package/ux-demo/recipes/mount-recipe.ts +28 -10
  729. package/ux-demo/recipes/navigation-sidebar.tsx +139 -13
  730. package/ux-demo/recipes/navigation-stack.tsx +81 -26
  731. package/ux-demo/recipes/types.ts +23 -2
  732. package/ux-demo/recipes/workspace-header.tsx +86 -5
  733. package/dist/browser/menu-action-row.js +0 -4
  734. package/dist/browser/menu-item.js +0 -4
  735. package/dist/browser/panel-header.js +0 -7
  736. package/dist/chunk-2RJBFNB6.js +0 -172
  737. package/dist/chunk-2RJBFNB6.js.map +0 -1
  738. package/dist/chunk-2Z5TWHKQ.js +0 -34
  739. package/dist/chunk-2Z5TWHKQ.js.map +0 -1
  740. package/dist/chunk-3YD6ZNRA.js +0 -34
  741. package/dist/chunk-3YD6ZNRA.js.map +0 -1
  742. package/dist/chunk-55DATV3A.js +0 -10
  743. package/dist/chunk-55DATV3A.js.map +0 -1
  744. package/dist/chunk-56RJXPHG.js.map +0 -1
  745. package/dist/chunk-7BR3DO2A.js.map +0 -1
  746. package/dist/chunk-7LDCTU7D.js +0 -134
  747. package/dist/chunk-7LDCTU7D.js.map +0 -1
  748. package/dist/chunk-AQNKJRFH.js +0 -79
  749. package/dist/chunk-AQNKJRFH.js.map +0 -1
  750. package/dist/chunk-B24C4A6T.js +0 -22
  751. package/dist/chunk-B24C4A6T.js.map +0 -1
  752. package/dist/chunk-BLK3UEEI.js +0 -23
  753. package/dist/chunk-BLK3UEEI.js.map +0 -1
  754. package/dist/chunk-BNQ2YDV4.js +0 -20
  755. package/dist/chunk-BNQ2YDV4.js.map +0 -1
  756. package/dist/chunk-DAFFDO33.js +0 -14
  757. package/dist/chunk-DAFFDO33.js.map +0 -1
  758. package/dist/chunk-H6URQGJI.js +0 -42
  759. package/dist/chunk-H6URQGJI.js.map +0 -1
  760. package/dist/chunk-I7INRJO4.js +0 -17
  761. package/dist/chunk-I7INRJO4.js.map +0 -1
  762. package/dist/chunk-LC3YSAZC.js +0 -39
  763. package/dist/chunk-LC3YSAZC.js.map +0 -1
  764. package/dist/chunk-M5JRI6MN.js +0 -27
  765. package/dist/chunk-M5JRI6MN.js.map +0 -1
  766. package/dist/chunk-MBR3TYNL.js +0 -14
  767. package/dist/chunk-MBR3TYNL.js.map +0 -1
  768. package/dist/chunk-MFKMHK5W.js +0 -10
  769. package/dist/chunk-MFKMHK5W.js.map +0 -1
  770. package/dist/chunk-MQIZAD4A.js +0 -227
  771. package/dist/chunk-MQIZAD4A.js.map +0 -1
  772. package/dist/chunk-PONXLV5U.js +0 -19
  773. package/dist/chunk-PONXLV5U.js.map +0 -1
  774. package/dist/chunk-RUNWEPR4.js +0 -10
  775. package/dist/chunk-RUNWEPR4.js.map +0 -1
  776. package/dist/chunk-W2FK7ZNY.js +0 -26
  777. package/dist/chunk-W2FK7ZNY.js.map +0 -1
  778. package/dist/chunk-WVPYVDXY.js +0 -15
  779. package/dist/chunk-WVPYVDXY.js.map +0 -1
  780. package/dist/chunk-YGN7OMLT.js.map +0 -1
  781. package/dist/chunk-YKUUZPED.js +0 -24
  782. package/dist/chunk-YKUUZPED.js.map +0 -1
  783. package/dist/menu-action-row.js +0 -5
  784. package/dist/menu-action-row.js.map +0 -1
  785. package/dist/menu-header.d.ts +0 -43
  786. package/dist/menu-header.js +0 -7
  787. package/dist/menu-header.js.map +0 -1
  788. package/dist/menu-item.d.ts +0 -33
  789. package/dist/menu-item.js +0 -5
  790. package/dist/menu-item.js.map +0 -1
  791. package/dist/panel-header.d.ts +0 -36
  792. package/dist/panel-header.js +0 -7
  793. package/dist/panel-header.js.map +0 -1
  794. package/dist/styles/menu-action-row.css +0 -160
  795. package/dist/styles/menu-header.css +0 -160
  796. package/dist/styles/menu-item.css +0 -123
  797. package/dist/styles/panel-header.css +0 -64
  798. package/dist/styles/tab-scaffold.css +0 -84
  799. package/dist/tab-scaffold.d.ts +0 -30
  800. package/dist/tab-scaffold.js +0 -16
  801. package/dist/tab-scaffold.js.map +0 -1
  802. package/dist/wire-tab-scaffold.d.ts +0 -11
  803. package/dist/wire-tab-scaffold.js +0 -16
  804. package/dist/wire-tab-scaffold.js.map +0 -1
  805. package/docs/tab-scaffold.md +0 -41
  806. package/ux-demo/recipes/master-detail-dialog.tsx +0 -45
  807. package/ux-demo/recipes/recipes.css +0 -355
@@ -0,0 +1,2346 @@
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
+ classOwnerFor,
19
+ componentLabel,
20
+ componentName,
21
+ componentOwnershipFacts,
22
+ contextualComponents,
23
+ configurationFor,
24
+ ownsClassBlock,
25
+ privateVariableOwner,
26
+ reportGap,
27
+ restyledComponents,
28
+ typedPropForToken,
29
+ } from './component-ownership.mjs';
30
+ import { collectLoudDeclarations, inspectLoudPairs } from './loud-pairs.mjs';
31
+ import {
32
+ classNames,
33
+ complexSelectorParts,
34
+ withoutRelationalArguments,
35
+ } from './selectors.mjs';
36
+
37
+ export const UI_ANALYSIS_SCHEMA_VERSION = 1;
38
+
39
+ export const UI_ANALYSIS_RULES = Object.freeze({
40
+ 'KUI-L001': { severity: 'error', title: 'Private or unknown Kerf selector' },
41
+ 'KUI-L002': { severity: 'error', title: 'Unknown Kerf token' },
42
+ 'KUI-L003': { severity: 'error', title: 'Competing geometry owners' },
43
+ 'KUI-L004': { severity: 'review', title: 'Repeated content inset' },
44
+ // KUI-L005 ('Forced component dimension') is retired: KUI-L019 reports any
45
+ // restyle of a cataloged component, dimensions included, and L005's only
46
+ // other shape (a public class as an ancestor) sizes an application element.
47
+ // The id stays in the diagnostic registry so existing exceptions still load.
48
+ 'KUI-L006': {
49
+ severity: 'review',
50
+ title: 'Hard-coded spacing outside the approved scale',
51
+ },
52
+ 'KUI-L007': { severity: 'error', title: 'Nested scroll owners' },
53
+ 'KUI-L008': { severity: 'review', title: 'Dynamic class requires review' },
54
+ 'KUI-L009': { severity: 'error', title: 'Stylesheet could not be parsed' },
55
+ 'KUI-L010': {
56
+ severity: 'error',
57
+ title: 'Private Kerf descendant override',
58
+ },
59
+ 'KUI-L011': { severity: 'error', title: 'Uncataloged shadow part override' },
60
+ 'KUI-L012': { severity: 'error', title: 'Private Kerf token assignment' },
61
+ 'KUI-L013': { severity: 'error', title: 'Uncataloged CSS value literal' },
62
+ 'KUI-L014': { severity: 'error', title: 'Wrong CSS value helper dimension' },
63
+ 'KUI-L015': { severity: 'error', title: 'Non-standalone CSS expression' },
64
+ 'KUI-L016': { severity: 'error', title: 'Forbidden declaration-list escape' },
65
+ 'KUI-L017': { severity: 'review', title: 'Exceptional spacing shorthand' },
66
+ 'KUI-L018': {
67
+ severity: 'review',
68
+ title: 'Loud fill override without its on-loud pair',
69
+ },
70
+ 'KUI-L019': {
71
+ severity: 'error',
72
+ title: 'Application CSS restyles a cataloged component',
73
+ },
74
+ 'KUI-L020': {
75
+ severity: 'error',
76
+ title: "Another component's private variable",
77
+ },
78
+ 'KUI-L021': {
79
+ severity: 'error',
80
+ title: 'Component token overridden where a typed prop exists',
81
+ },
82
+ 'KUI-L022': {
83
+ severity: 'error',
84
+ title: "Hook class restyles a cataloged component's root",
85
+ },
86
+ 'KUI-L023': {
87
+ severity: 'error',
88
+ title: "Another component's public class is rendered as markup",
89
+ },
90
+ });
91
+
92
+ const ownershipAction =
93
+ "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.";
94
+
95
+ /** The doctor's per-rule repair action, where it is more specific than the default. */
96
+ export const UI_ANALYSIS_ACTIONS = Object.freeze({
97
+ 'KUI-L019': ownershipAction,
98
+ 'KUI-L020': ownershipAction,
99
+ 'KUI-L021':
100
+ 'Set the typed prop the diagnostic names on the component instead of overriding its token.',
101
+ 'KUI-L022': ownershipAction,
102
+ 'KUI-L023': ownershipAction,
103
+ });
104
+
105
+ const adoptionRules = new Set([
106
+ 'KUI-L001',
107
+ 'KUI-L002',
108
+ 'KUI-L010',
109
+ 'KUI-L011',
110
+ 'KUI-L012',
111
+ 'KUI-L019',
112
+ 'KUI-L020',
113
+ 'KUI-L021',
114
+ 'KUI-L022',
115
+ 'KUI-L023',
116
+ ]);
117
+
118
+ const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
119
+ function isTestModule(file) {
120
+ const path = file.replaceAll('\\', '/');
121
+ return (
122
+ /(?:^|\/)__tests__\//.test(path) ||
123
+ /\.(?:test|spec)\.(?:js|jsx|mjs|ts|tsx)$/.test(path)
124
+ );
125
+ }
126
+ const spacingProperties = /^(?:margin|padding|gap|inset)(?:-|$)/;
127
+ const approvedSpacing = new Set([0, 4, 8, 16, 24]);
128
+ async function collectFiles(root, paths) {
129
+ const files = [];
130
+ const visit = async (path) => {
131
+ if (isUiTraversalExcluded(root, path)) return;
132
+ const details = await stat(path);
133
+ if (details.isFile()) {
134
+ if (sourceExtensions.has(extname(path)) || path.endsWith('.css'))
135
+ files.push(path);
136
+ return;
137
+ }
138
+ const entries = await readdir(path, { withFileTypes: true });
139
+ for (const entry of entries) {
140
+ const child = resolve(path, entry.name);
141
+ if (entry.isDirectory()) await visit(child);
142
+ else if (
143
+ sourceExtensions.has(extname(entry.name)) ||
144
+ entry.name.endsWith('.css')
145
+ )
146
+ files.push(child);
147
+ }
148
+ };
149
+ for (const path of paths?.length ? paths : [root])
150
+ await visit(resolve(root, path));
151
+ return files.sort();
152
+ }
153
+
154
+ function location(file, node, source) {
155
+ if (!source) {
156
+ const point = node.source?.start;
157
+ return {
158
+ file,
159
+ line: point?.line ?? 1,
160
+ column: point?.column ?? point?.offset ?? 1,
161
+ };
162
+ }
163
+ const point = source.getLineAndCharacterOfPosition(node.getStart(source));
164
+ return {
165
+ file,
166
+ line: point.line + 1,
167
+ column: point.character + 1,
168
+ };
169
+ }
170
+
171
+ function diagnostic(ruleId, at, message, evidence, chain, adoption = false) {
172
+ const rule = UI_ANALYSIS_RULES[ruleId];
173
+ return {
174
+ ruleId,
175
+ severity: adoption && adoptionRules.has(ruleId) ? 'review' : rule.severity,
176
+ message,
177
+ location: at,
178
+ evidence,
179
+ chain,
180
+ };
181
+ }
182
+
183
+ function isSuppressed(item, profile, root) {
184
+ const target = relative(root, item.location.file).replaceAll('\\', '/');
185
+ return (profile?.exceptions ?? []).some(
186
+ (exception) =>
187
+ exception.rules?.includes(item.ruleId) && exception.target === target,
188
+ );
189
+ }
190
+
191
+ function relativeStyleImports(file, source) {
192
+ const imports = [];
193
+ if (file.endsWith('.css')) {
194
+ let stylesheet;
195
+ try {
196
+ stylesheet = postcss.parse(source, { from: file });
197
+ } catch {
198
+ return imports;
199
+ }
200
+ stylesheet.walkAtRules('import', (rule) => {
201
+ const match = rule.params.match(
202
+ /^\s*(?:["']([^"']+)["']|url\(\s*(?:["']([^"']+)["']|([^\s)]+))\s*\))/,
203
+ );
204
+ const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
205
+ if (specifier?.startsWith('.'))
206
+ imports.push(resolve(dirname(file), specifier));
207
+ });
208
+ return imports;
209
+ }
210
+ const syntax = file.endsWith('.tsx')
211
+ ? ts.ScriptKind.TSX
212
+ : file.endsWith('.jsx')
213
+ ? ts.ScriptKind.JSX
214
+ : file.endsWith('.ts')
215
+ ? ts.ScriptKind.TS
216
+ : ts.ScriptKind.JS;
217
+ const module = ts.createSourceFile(
218
+ file,
219
+ source,
220
+ ts.ScriptTarget.Latest,
221
+ true,
222
+ syntax,
223
+ );
224
+ for (const statement of module.statements) {
225
+ if (
226
+ !ts.isImportDeclaration(statement) ||
227
+ !ts.isStringLiteral(statement.moduleSpecifier)
228
+ )
229
+ continue;
230
+ const specifier = statement.moduleSpecifier.text;
231
+ if (specifier.startsWith('.') && specifier.endsWith('.css'))
232
+ imports.push(resolve(dirname(file), specifier));
233
+ }
234
+ return imports;
235
+ }
236
+
237
+ function reachableStyleFacts(file, imports, styleFacts) {
238
+ const result = new Map();
239
+ for (const style of reachableStyleFiles(file, imports, styleFacts)) {
240
+ for (const [className, facts] of styleFacts.get(style)) {
241
+ const current = result.get(className) ?? { scroll: false, inset: false };
242
+ current.scroll ||= facts.scroll;
243
+ current.inset ||= facts.inset;
244
+ current.styled ??= facts.styled;
245
+ result.set(className, current);
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+
251
+ function reachableStyleFiles(file, imports, styleFacts) {
252
+ const visited = new Set();
253
+ const visit = (style) => {
254
+ if (visited.has(style) || !styleFacts.has(style)) return;
255
+ visited.add(style);
256
+ for (const dependency of imports.get(style) ?? []) visit(dependency);
257
+ };
258
+ for (const style of imports.get(file) ?? []) visit(style);
259
+ return visited;
260
+ }
261
+
262
+ async function loadCatalogs(profileResult, ownership) {
263
+ const entries = [];
264
+ for (const catalog of profileResult.profile?.catalogs ?? []) {
265
+ const owner = profileResult.provenance[`$catalogs.${catalog.package}`];
266
+ if (!owner) continue;
267
+ try {
268
+ const artifact = JSON.parse(
269
+ await readFile(
270
+ resolve(dirname(owner), catalog.composition.path),
271
+ 'utf8',
272
+ ),
273
+ );
274
+ const selection = await loadSelectionFacts(owner, catalog);
275
+ const joined = new Set();
276
+ for (const entry of artifact.entries ?? []) {
277
+ const selected = selection.get(entry.key);
278
+ joined.add(entry.key);
279
+ const componentSource =
280
+ selected?.componentSource ??
281
+ (/\.[cm]?[jt]sx?$/.test(entry.source) ? entry.source : undefined);
282
+ entries.push(
283
+ selected
284
+ ? {
285
+ ...entry,
286
+ publicExports: entry.publicExports ?? selected.publicExports,
287
+ moduleImports: selected.moduleImports,
288
+ componentSource,
289
+ styleSources: entry.styleSources ?? selected.styleSources,
290
+ catalogDirectory: dirname(owner),
291
+ }
292
+ : { ...entry, componentSource, catalogDirectory: dirname(owner) },
293
+ );
294
+ }
295
+ for (const [key, selected] of selection) {
296
+ if (
297
+ ownership !== 'component' ||
298
+ joined.has(key) ||
299
+ !selected.componentSource
300
+ )
301
+ continue;
302
+ entries.push({
303
+ key,
304
+ package: catalog.package,
305
+ id: selected.id,
306
+ name: selected.name,
307
+ kind: selected.kind,
308
+ componentSource: selected.componentSource,
309
+ styleSources: selected.styleSources,
310
+ moduleImports: selected.moduleImports,
311
+ publicExports: selected.publicExports,
312
+ boundaries: {
313
+ publicClasses: selected.publicClasses ?? [],
314
+ rootElement: selected.rootElement,
315
+ },
316
+ catalogDirectory: dirname(owner),
317
+ });
318
+ }
319
+ } catch {
320
+ // Profile validation reports the precise catalog load failure.
321
+ }
322
+ }
323
+ return entries;
324
+ }
325
+
326
+ // The composition (v2) catalog carries no import facts, and several of its
327
+ // entries share a display name (the Kerf `Select` and the Web Awesome
328
+ // `wa-select` are both "Select"). The selection (v1) catalog is where each
329
+ // entry's real public exports and import subpaths live, so join it by key.
330
+ async function loadSelectionFacts(owner, catalog) {
331
+ const facts = new Map();
332
+ if (!catalog.selection?.path) return facts;
333
+ let artifact;
334
+ try {
335
+ artifact = JSON.parse(
336
+ await readFile(resolve(dirname(owner), catalog.selection.path), 'utf8'),
337
+ );
338
+ } catch {
339
+ return facts;
340
+ }
341
+ const packageName = artifact.package ?? catalog.package;
342
+ for (const entry of artifact.entries ?? []) {
343
+ const delivery = entry.delivery ?? {};
344
+ facts.set(`${packageName}:${entry.id}`, {
345
+ id: entry.id,
346
+ name: entry.name,
347
+ kind: entry.kind,
348
+ publicExports: entry.publicExports,
349
+ publicClasses: entry.publicClasses,
350
+ rootElement: entry.rootElement,
351
+ componentSource: entry.source,
352
+ styleSources: entry.styleSources,
353
+ moduleImports: [delivery.browserImport, delivery.moduleImport].filter(
354
+ Boolean,
355
+ ),
356
+ });
357
+ }
358
+ return facts;
359
+ }
360
+
361
+ // A catalog entry's public exports mix the component render function with
362
+ // helpers its props consume (`Select` ships `uiColor`, `List` ships
363
+ // `px`/`rem`/`flex`). Only the render function stands for the entry, so only
364
+ // it may carry the entry's contracts. A component export is one whose name
365
+ // starts with an uppercase letter: JSX itself treats a lowercase tag as an
366
+ // intrinsic element, so a lowercase export can never be a component tag, and
367
+ // scripts/check-component-catalog.mjs enforces the converse for this package
368
+ // (every uppercase runtime export returns SafeHtml; no lowercase one does).
369
+ // eslint-plugin-kerfjs applies the same rule.
370
+ function isComponentExport(name) {
371
+ return /^[A-Z]/.test(name);
372
+ }
373
+
374
+ // Pick the catalog entry an imported (or namespace-accessed) export name
375
+ // refers to. Names are not unique across a catalog, so candidates are ranked:
376
+ // an entry whose own import subpath is the module wins, then an entry that
377
+ // declares the name as a public export, then catalog order. A helper export
378
+ // never resolves to an entry.
379
+ function resolveExportEntry(
380
+ candidates,
381
+ name,
382
+ module,
383
+ file,
384
+ packageDirectory,
385
+ aliasSource,
386
+ ) {
387
+ if (!isComponentExport(name)) return undefined;
388
+ const localSource =
389
+ packageDirectory && (module?.startsWith('.') || aliasSource)
390
+ ? relative(
391
+ packageDirectory,
392
+ aliasSource ?? resolve(dirname(file), module),
393
+ )
394
+ .replaceAll('\\', '/')
395
+ .replace(/\.[cm]?[jt]sx?$/, '')
396
+ : undefined;
397
+ const eligible = (candidates ?? []).filter(
398
+ (entry) =>
399
+ module === entry.package ||
400
+ module?.startsWith(`${entry.package}/`) ||
401
+ (localSource &&
402
+ entry.componentSource?.replace(/\.[cm]?[jt]sx?$/, '') === localSource),
403
+ );
404
+ const exported = (entry) =>
405
+ (entry.publicExports ?? []).some(
406
+ (item) => (typeof item === 'string' ? item : item.name) === name,
407
+ );
408
+ return (
409
+ eligible.find((entry) => entry.moduleImports?.includes(module)) ??
410
+ eligible.find(exported) ??
411
+ eligible[0]
412
+ );
413
+ }
414
+
415
+ // Use the nearest project tsconfig without walking into a parent repository.
416
+ // TypeScript expands `extends` and resolves exact and wildcard `paths` here.
417
+ function compilerOptionsFor(file, root, cache) {
418
+ let directory = dirname(file);
419
+ const visited = [];
420
+ while (directory === root || !relative(root, directory).startsWith('..')) {
421
+ if (cache.has(directory)) break;
422
+ visited.push(directory);
423
+ const configPath = resolve(directory, 'tsconfig.json');
424
+ if (ts.sys.fileExists(configPath)) {
425
+ const config = ts.readConfigFile(configPath, ts.sys.readFile);
426
+ cache.set(
427
+ directory,
428
+ config.error
429
+ ? undefined
430
+ : ts.parseJsonConfigFileContent(
431
+ config.config,
432
+ ts.sys,
433
+ directory,
434
+ undefined,
435
+ configPath,
436
+ ).options,
437
+ );
438
+ break;
439
+ }
440
+ if (directory === root) break;
441
+ directory = dirname(directory);
442
+ }
443
+ const options = cache.get(directory);
444
+ for (const path of visited) cache.set(path, options);
445
+ return options;
446
+ }
447
+
448
+ function catalogFacts(entries) {
449
+ const publicClasses = new Set();
450
+ const publicTokens = new Set();
451
+ const publicParts = new Map();
452
+ const classEntries = new Map();
453
+ const blockEntries = new Map();
454
+ const exportEntries = new Map();
455
+ const addExport = (name, entry) => {
456
+ const candidates = exportEntries.get(name) ?? [];
457
+ if (!candidates.includes(entry)) candidates.push(entry);
458
+ exportEntries.set(name, candidates);
459
+ };
460
+ for (const entry of entries) {
461
+ addExport(entry.name, entry);
462
+ for (const item of entry.publicExports ?? []) {
463
+ const name = typeof item === 'string' ? item : item.name;
464
+ if (isComponentExport(name)) addExport(name, entry);
465
+ }
466
+ for (const className of new Set(
467
+ [
468
+ entry.boundaries?.rootClass,
469
+ ...(entry.boundaries?.publicClasses ?? []),
470
+ ].filter(Boolean),
471
+ )) {
472
+ publicClasses.add(className);
473
+ const owners = classEntries.get(className) ?? [];
474
+ owners.push(entry);
475
+ classEntries.set(className, owners);
476
+ const block = bemBlock(className);
477
+ const blockOwners = blockEntries.get(block) ?? [];
478
+ if (!blockOwners.includes(entry)) blockOwners.push(entry);
479
+ blockEntries.set(block, blockOwners);
480
+ }
481
+ for (const token of entry.boundaries?.publicTokens ?? [])
482
+ publicTokens.add(token);
483
+ for (const part of entry.boundaries?.publicParts ?? []) {
484
+ const roots = publicParts.get(part) ?? new Set();
485
+ if (entry.boundaries?.rootClass) roots.add(entry.boundaries.rootClass);
486
+ publicParts.set(part, roots);
487
+ }
488
+ }
489
+ return {
490
+ entries,
491
+ publicClasses,
492
+ publicTokens,
493
+ publicParts,
494
+ classEntries,
495
+ blockEntries,
496
+ exportEntries,
497
+ ownership: componentOwnershipFacts(entries),
498
+ };
499
+ }
500
+
501
+ const bemBlock = (name) => name.split(/__|--/, 1)[0];
502
+
503
+ function renderedLiteralClasses(file, source) {
504
+ const names = new Set();
505
+ const parsed = ts.createSourceFile(
506
+ file,
507
+ source,
508
+ ts.ScriptTarget.Latest,
509
+ true,
510
+ file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.JSX,
511
+ );
512
+ const visit = (node) => {
513
+ if (
514
+ ts.isJsxAttribute(node) &&
515
+ ['class', 'className'].includes(node.name.text)
516
+ ) {
517
+ const value =
518
+ node.initializer && ts.isStringLiteral(node.initializer)
519
+ ? node.initializer.text
520
+ : node.initializer &&
521
+ ts.isJsxExpression(node.initializer) &&
522
+ node.initializer.expression &&
523
+ ts.isStringLiteral(node.initializer.expression)
524
+ ? node.initializer.expression.text
525
+ : undefined;
526
+ for (const name of value?.split(/\s+/) ?? []) if (name) names.add(name);
527
+ }
528
+ ts.forEachChild(node, visit);
529
+ };
530
+ visit(parsed);
531
+ return names;
532
+ }
533
+
534
+ function inferOwnedClasses(entries, contents, imports) {
535
+ return entries.map((entry) => {
536
+ if (!entry.componentSource || !entry.catalogDirectory) return entry;
537
+ const sourceFile = resolve(entry.catalogDirectory, entry.componentSource);
538
+ const source = contents.get(sourceFile);
539
+ if (!source) return entry;
540
+ const styleFiles = entry.styleSources?.length
541
+ ? entry.styleSources.map((path) => resolve(entry.catalogDirectory, path))
542
+ : [...(imports.get(sourceFile) ?? [])];
543
+ const styleSources = styleFiles.map((file) =>
544
+ relative(entry.catalogDirectory, file).replaceAll('\\', '/'),
545
+ );
546
+ const styledBlocks = new Set();
547
+ for (const style of styleFiles) {
548
+ const css = contents.get(style);
549
+ if (!css) continue;
550
+ try {
551
+ postcss.parse(css, { from: style }).walkRules((rule) => {
552
+ for (const name of subjectClasses(rule.selector))
553
+ styledBlocks.add(bemBlock(name));
554
+ });
555
+ } catch {
556
+ // The stylesheet parser reports malformed CSS in the normal pass.
557
+ }
558
+ }
559
+ const inferred = [...renderedLiteralClasses(sourceFile, source)].filter(
560
+ (name) => styledBlocks.has(bemBlock(name)),
561
+ );
562
+ if (!inferred.length) return { ...entry, styleSources };
563
+ return {
564
+ ...entry,
565
+ styleSources,
566
+ boundaries: {
567
+ ...entry.boundaries,
568
+ publicClasses: [
569
+ ...new Set([...(entry.boundaries?.publicClasses ?? []), ...inferred]),
570
+ ],
571
+ },
572
+ };
573
+ });
574
+ }
575
+
576
+ async function ownershipGroupEntries(groups, root, contents) {
577
+ if (!groups?.length) return [];
578
+ const manifest = JSON.parse(
579
+ await readFile(resolve(root, 'package.json'), 'utf8'),
580
+ );
581
+ return Promise.all(
582
+ groups.map(async (group, index) => {
583
+ const classes = new Set();
584
+ for (const style of group.styleSources) {
585
+ const file = resolve(root, style);
586
+ let css = contents.get(file);
587
+ if (css === undefined)
588
+ try {
589
+ css = await readFile(file, 'utf8');
590
+ } catch {
591
+ // A missing configured file owns no classes.
592
+ }
593
+ if (!css) continue;
594
+ try {
595
+ postcss.parse(css, { from: file }).walkRules((rule) => {
596
+ for (const name of subjectClasses(rule.selector)) classes.add(name);
597
+ });
598
+ } catch {
599
+ // The normal stylesheet pass reports malformed CSS.
600
+ }
601
+ }
602
+ return {
603
+ key: `${manifest.name}:ownership-group-${index + 1}`,
604
+ package: manifest.name,
605
+ id: `ownership-group-${index + 1}`,
606
+ name: `Ownership group ${index + 1}`,
607
+ catalogDirectory: root,
608
+ styleSources: group.styleSources,
609
+ ownershipGroupSources: group.sources,
610
+ boundaries: { publicClasses: [...classes] },
611
+ };
612
+ }),
613
+ );
614
+ }
615
+
616
+ async function implicitOwnershipEntries(files, imports, root) {
617
+ const found = [];
618
+ const packages = new Map();
619
+ const packageAt = async (directory) => {
620
+ if (packages.has(directory)) return packages.get(directory);
621
+ let result;
622
+ try {
623
+ const manifest = JSON.parse(
624
+ await readFile(resolve(directory, 'package.json'), 'utf8'),
625
+ );
626
+ if (typeof manifest.name === 'string')
627
+ result = { name: manifest.name, directory };
628
+ } catch {
629
+ // Search the containing directory for a package manifest.
630
+ }
631
+ if (!result && directory !== root) {
632
+ const parent = dirname(directory);
633
+ if (!relative(root, parent).startsWith('..'))
634
+ result = await packageAt(parent);
635
+ }
636
+ packages.set(directory, result);
637
+ return result;
638
+ };
639
+ for (const file of files) {
640
+ if (!sourceExtensions.has(extname(file))) continue;
641
+ const styles = (imports.get(file) ?? []).filter((path) =>
642
+ files.includes(path),
643
+ );
644
+ if (!styles.length) continue;
645
+ const owner = await packageAt(dirname(file));
646
+ if (!owner) continue;
647
+ const source = relative(owner.directory, file).replaceAll('\\', '/');
648
+ const id = `module:${source}`;
649
+ found.push({
650
+ key: `${owner.name}:${id}`,
651
+ package: owner.name,
652
+ id,
653
+ name: basename(file).replace(/\.[^.]+$/, ''),
654
+ componentSource: source,
655
+ catalogDirectory: owner.directory,
656
+ styleSources: styles.map((style) =>
657
+ relative(owner.directory, style).replaceAll('\\', '/'),
658
+ ),
659
+ });
660
+ }
661
+ return found;
662
+ }
663
+
664
+ function spacingValues(value) {
665
+ const results = [];
666
+ for (const match of value.matchAll(/(-?\d*\.?\d+)(px|rem)\b/g)) {
667
+ const numeric = Number(match[1]) * (match[2] === 'rem' ? 16 : 1);
668
+ results.push({ text: match[0], pixels: numeric });
669
+ }
670
+ return results;
671
+ }
672
+
673
+ function escapeRegExp(value) {
674
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
675
+ }
676
+
677
+ // A declaration that restyles, as opposed to one that only sets a custom
678
+ // property: setting a component's public token on its host is configuration,
679
+ // which the token rules (KUI-L002, KUI-L012, KUI-L020, KUI-L021) judge.
680
+ function restylingDeclarations(rule) {
681
+ return (rule.nodes ?? []).filter(
682
+ (node) => node.type === 'decl' && !node.prop.startsWith('--'),
683
+ );
684
+ }
685
+
686
+ function inKeyframes(rule) {
687
+ return rule.parent?.type === 'atrule' && /keyframes$/i.test(rule.parent.name);
688
+ }
689
+
690
+ // The classes a rule makes its subject (the rightmost compound of each complex
691
+ // selector, outside :has()/:not()).
692
+ function subjectClasses(selector) {
693
+ return complexSelectorParts(selector).flatMap((parts) => {
694
+ const subject = parts.at(-1)?.compound;
695
+ return subject ? classNames(withoutRelationalArguments(subject)) : [];
696
+ });
697
+ }
698
+
699
+ function inspectComponentOwnership(
700
+ file,
701
+ rule,
702
+ facts,
703
+ diagnostics,
704
+ adoption,
705
+ isForeign,
706
+ ownershipEvidence,
707
+ ownershipContext,
708
+ ) {
709
+ if (inKeyframes(rule)) return;
710
+ const restyling = restylingDeclarations(rule);
711
+ if (restyling.length === 0) return;
712
+ const properties = [...new Set(restyling.map((decl) => decl.prop))];
713
+ const restyled = restyledComponents(
714
+ rule.selector,
715
+ facts.ownership,
716
+ isForeign,
717
+ ownershipEvidence,
718
+ );
719
+ for (const { entry, coOwners, via, name } of restyled)
720
+ diagnostics.push(
721
+ diagnostic(
722
+ 'KUI-L019',
723
+ location(file, rule),
724
+ coOwners
725
+ ? `\`${name}\` makes a class co-owned by ${coOwners.map((owner) => componentLabel(owner)).join(', ')} the subject of an application rule (${properties.join(', ')}); those components own their styles. Configure an owning component or style your own element instead.`
726
+ : via === 'descendant'
727
+ ? `\`${name}\` reaches an unclassed descendant inside ${componentLabel(entry)} (${properties.join(', ')}); components own their styles. Use a child combinator or an own class to target your element, or configure the child through ${configurationFor(entry)}. ${reportGap(entry)}`
728
+ : `\`${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)}`,
729
+ {
730
+ selector: rule.selector,
731
+ ...(coOwners
732
+ ? { components: coOwners.map((owner) => owner.key) }
733
+ : { component: entry.key }),
734
+ via,
735
+ target: name,
736
+ properties,
737
+ },
738
+ undefined,
739
+ adoption,
740
+ ),
741
+ );
742
+ if (ownershipContext === 'subject' || !ownershipEvidence?.ownPackage) return;
743
+ for (const { entry, name, position } of contextualComponents(
744
+ rule.selector,
745
+ facts.ownership,
746
+ isForeign,
747
+ ownershipEvidence.ownPackage,
748
+ ownershipContext === 'any-package',
749
+ )) {
750
+ if (position === 'subject') continue;
751
+ if (
752
+ restyled.some(
753
+ (item) => item.entry.key === entry.key && item.name === name,
754
+ )
755
+ )
756
+ continue;
757
+ diagnostics.push(
758
+ diagnostic(
759
+ 'KUI-L019',
760
+ location(file, rule),
761
+ `\`${name}\` uses ${componentLabel(entry)} as ${position} selector context (${properties.join(', ')}); foreign component handles cannot key this stylesheet in this ownership mode. Configure the component through ${configurationFor(entry)}. ${reportGap(entry)}`,
762
+ {
763
+ selector: rule.selector,
764
+ component: entry.key,
765
+ via: 'context',
766
+ position,
767
+ target: name,
768
+ properties,
769
+ },
770
+ undefined,
771
+ adoption,
772
+ ),
773
+ );
774
+ }
775
+ }
776
+
777
+ function inspectVariableOwnership(
778
+ file,
779
+ decl,
780
+ facts,
781
+ diagnostics,
782
+ adoption,
783
+ isForeign,
784
+ ) {
785
+ const at = location(file, decl);
786
+ const privates = new Set([
787
+ ...(decl.prop.match(/--_[a-z0-9_-]+/gi) ?? []),
788
+ ...(decl.value.match(/--_[a-z0-9_-]+/gi) ?? []),
789
+ ]);
790
+ for (const variable of privates) {
791
+ const owner = privateVariableOwner(variable, facts.ownership);
792
+ if (owner === undefined || (owner && !isForeign(owner))) continue;
793
+ const written = decl.prop === variable;
794
+ diagnostics.push(
795
+ diagnostic(
796
+ 'KUI-L020',
797
+ at,
798
+ `\`${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)}`,
799
+ {
800
+ property: decl.prop,
801
+ value: decl.value,
802
+ variable,
803
+ ...(owner ? { component: owner.key } : {}),
804
+ access: written ? 'write' : 'read',
805
+ },
806
+ undefined,
807
+ adoption,
808
+ ),
809
+ );
810
+ }
811
+ if (!decl.prop.startsWith('--')) return;
812
+ const typed = typedPropForToken(decl.prop, facts.ownership);
813
+ if (!typed || !isForeign(typed.entry)) return;
814
+ const contract = (typed.entry.cssValueProps ?? []).find(
815
+ ({ path }) => path === typed.path,
816
+ );
817
+ const example = contract?.examples?.[0];
818
+ diagnostics.push(
819
+ diagnostic(
820
+ 'KUI-L021',
821
+ at,
822
+ `\`${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)}`,
823
+ {
824
+ property: decl.prop,
825
+ value: decl.value,
826
+ component: typed.entry.key,
827
+ prop: typed.path,
828
+ },
829
+ undefined,
830
+ adoption,
831
+ ),
832
+ );
833
+ }
834
+
835
+ async function inspectCss(
836
+ file,
837
+ source,
838
+ facts,
839
+ diagnostics,
840
+ cssFacts,
841
+ adoption = false,
842
+ siblingOnLoud = [],
843
+ isForeign = () => true,
844
+ ownershipEvidence,
845
+ ownershipContext = 'subject',
846
+ ) {
847
+ let root;
848
+ try {
849
+ root = postcss.parse(source, { from: file });
850
+ } catch (error) {
851
+ diagnostics.push(
852
+ diagnostic(
853
+ 'KUI-L009',
854
+ { file, line: error.line ?? 1, column: error.column ?? 1 },
855
+ error.reason ?? error.message,
856
+ ),
857
+ );
858
+ return;
859
+ }
860
+ inspectLoudPairs(
861
+ file,
862
+ root,
863
+ (decl, message, evidence) =>
864
+ diagnostics.push(
865
+ diagnostic('KUI-L018', location(file, decl), message, evidence),
866
+ ),
867
+ siblingOnLoud,
868
+ );
869
+ root.walkRules((rule) => {
870
+ const subjects = subjectClasses(rule.selector);
871
+ inspectComponentOwnership(
872
+ file,
873
+ rule,
874
+ facts,
875
+ diagnostics,
876
+ adoption,
877
+ isForeign,
878
+ ownershipEvidence,
879
+ ownershipContext,
880
+ );
881
+ if (!inKeyframes(rule) && restylingDeclarations(rule).length > 0)
882
+ for (const className of subjects) {
883
+ const record = cssFacts.get(className) ?? {
884
+ scroll: false,
885
+ inset: false,
886
+ };
887
+ record.styled ??= {
888
+ stylesheet: file,
889
+ line: rule.source?.start?.line ?? 1,
890
+ selector: rule.selector,
891
+ };
892
+ cssFacts.set(className, record);
893
+ }
894
+ const classes = [
895
+ ...rule.selector.matchAll(/\.([_a-zA-Z]+[_a-zA-Z0-9-]*)/g),
896
+ ].map((match) => match[1]);
897
+ const publicRootPattern = [...facts.publicClasses]
898
+ .map(
899
+ (className) => `\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
900
+ )
901
+ .join('|');
902
+ const privateDescendants = new Set();
903
+ if (publicRootPattern)
904
+ for (const selector of rule.selectors ?? [rule.selector]) {
905
+ const rootMatch = new RegExp(
906
+ `(?:${publicRootPattern})(?:[.#:[\\w-]|\\s)*(?:>|\\s)`,
907
+ ).exec(selector);
908
+ if (!rootMatch) continue;
909
+ const descendant = selector.slice(
910
+ rootMatch.index + rootMatch[0].length,
911
+ );
912
+ for (const match of descendant.matchAll(/\.(kui-[\w-]+)/g))
913
+ if (!facts.publicClasses.has(match[1]))
914
+ privateDescendants.add(match[1]);
915
+ }
916
+ for (const className of classes) {
917
+ if (privateDescendants.has(className))
918
+ diagnostics.push(
919
+ diagnostic(
920
+ 'KUI-L010',
921
+ location(file, rule),
922
+ `.${className} is a private Kerf descendant; prefer the owning component's prop or variant, or catalog a deliberate public extension point.`,
923
+ { selector: rule.selector, className },
924
+ undefined,
925
+ adoption,
926
+ ),
927
+ );
928
+ else if (
929
+ className.startsWith('kui-') &&
930
+ !facts.publicClasses.has(className)
931
+ )
932
+ diagnostics.push(
933
+ diagnostic(
934
+ 'KUI-L001',
935
+ location(file, rule),
936
+ `.${className} is not a cataloged public Kerf class; prefer a component prop or variant, or catalog the extension point.`,
937
+ { selector: rule.selector, className },
938
+ undefined,
939
+ adoption,
940
+ ),
941
+ );
942
+ }
943
+ for (const match of rule.selector.matchAll(/::part\(\s*([\w-]+)\s*\)/g)) {
944
+ const part = match[1];
945
+ const roots = facts.publicParts.get(part) ?? new Set();
946
+ const allowed = [...roots].some((className) =>
947
+ new RegExp(`\\.${escapeRegExp(className)}(?:[^\\w-]|$)`).test(
948
+ rule.selector.slice(0, match.index),
949
+ ),
950
+ );
951
+ if (!allowed)
952
+ diagnostics.push(
953
+ diagnostic(
954
+ 'KUI-L011',
955
+ location(file, rule),
956
+ `::part(${part}) is not a cataloged extension point; prefer the component's prop or variant, or add ${part} to boundaries.publicParts.`,
957
+ { selector: rule.selector, part },
958
+ undefined,
959
+ adoption,
960
+ ),
961
+ );
962
+ }
963
+ rule.walkDecls((decl) => {
964
+ inspectVariableOwnership(
965
+ file,
966
+ decl,
967
+ facts,
968
+ diagnostics,
969
+ adoption,
970
+ isForeign,
971
+ );
972
+ const at = location(file, decl);
973
+ const tokens = [
974
+ ...(decl.prop.match(/--kui-[a-z0-9-]+/g) ?? []),
975
+ ...(decl.value.match(/--kui-[a-z0-9-]+/g) ?? []),
976
+ ];
977
+ for (const token of new Set(tokens))
978
+ if (!facts.publicTokens.has(token)) {
979
+ const assignment = decl.prop === token;
980
+ diagnostics.push(
981
+ diagnostic(
982
+ assignment ? 'KUI-L012' : 'KUI-L002',
983
+ at,
984
+ assignment
985
+ ? `${token} has no public configuration contract; prefer the component's prop or variant, or catalog the token explicitly.`
986
+ : `${token} is not a cataloged public Kerf token; prefer a documented token or component configuration.`,
987
+ { property: decl.prop, value: decl.value, token },
988
+ undefined,
989
+ adoption,
990
+ ),
991
+ );
992
+ }
993
+ if (
994
+ spacingProperties.test(decl.prop) &&
995
+ !/var\(|calc\(|remify\(/.test(decl.value)
996
+ )
997
+ for (const value of spacingValues(decl.value))
998
+ if (!approvedSpacing.has(value.pixels))
999
+ diagnostics.push(
1000
+ diagnostic(
1001
+ 'KUI-L006',
1002
+ at,
1003
+ `${decl.prop}: ${value.text} is outside the approved Kerf spacing scale.`,
1004
+ {
1005
+ property: decl.prop,
1006
+ value: decl.value,
1007
+ pixels: value.pixels,
1008
+ },
1009
+ ),
1010
+ );
1011
+ for (const className of subjects) {
1012
+ const record = cssFacts.get(className) ?? {
1013
+ scroll: false,
1014
+ inset: false,
1015
+ };
1016
+ if (
1017
+ /^overflow(?:-|$)/.test(decl.prop) &&
1018
+ /\b(?:auto|scroll)\b/.test(decl.value)
1019
+ )
1020
+ record.scroll = true;
1021
+ if (/^padding(?:-|$)/.test(decl.prop) && decl.value !== '0')
1022
+ record.inset = true;
1023
+ cssFacts.set(className, record);
1024
+ }
1025
+ });
1026
+ });
1027
+ }
1028
+
1029
+ function literalClasses(attribute) {
1030
+ if (!attribute?.initializer) return { values: [], dynamic: false };
1031
+ const value = attribute.initializer;
1032
+ if (ts.isStringLiteral(value))
1033
+ return { values: value.text.split(/\s+/).filter(Boolean), dynamic: false };
1034
+ if (
1035
+ ts.isJsxExpression(value) &&
1036
+ value.expression &&
1037
+ ts.isStringLiteralLike(value.expression)
1038
+ )
1039
+ return {
1040
+ values: value.expression.text.split(/\s+/).filter(Boolean),
1041
+ dynamic: false,
1042
+ };
1043
+ return { values: [], dynamic: true };
1044
+ }
1045
+
1046
+ function literalClassValues(expression) {
1047
+ if (!expression) return [];
1048
+ if (ts.isStringLiteralLike(expression))
1049
+ return expression.text.split(/\s+/).filter(Boolean);
1050
+ if (ts.isParenthesizedExpression(expression))
1051
+ return literalClassValues(expression.expression);
1052
+ if (ts.isConditionalExpression(expression))
1053
+ return [
1054
+ ...literalClassValues(expression.whenTrue),
1055
+ ...literalClassValues(expression.whenFalse),
1056
+ ];
1057
+ return [];
1058
+ }
1059
+
1060
+ function jsxOwnershipEvidence(style, facts, contents, root, compilerCache) {
1061
+ const hooks = new Map();
1062
+ const composedChildren = new Map();
1063
+ const composedDescendants = new Map();
1064
+ const ambiguousHooks = new Set();
1065
+ const ambiguousChildren = new Set();
1066
+ const ambiguousDescendants = new Set();
1067
+ const record = (map, ambiguous, key, entry) => {
1068
+ if (ambiguous.has(key)) return;
1069
+ if (map.has(key) && map.get(key).key !== entry.key) {
1070
+ map.delete(key);
1071
+ ambiguous.add(key);
1072
+ } else map.set(key, entry);
1073
+ };
1074
+ for (const owner of facts.entries) {
1075
+ if (!owner.componentSource || !owner.catalogDirectory) continue;
1076
+ const relativeStyle = relative(owner.catalogDirectory, style).replaceAll(
1077
+ '\\',
1078
+ '/',
1079
+ );
1080
+ if (!owner.styleSources?.includes(relativeStyle)) continue;
1081
+ const file = resolve(owner.catalogDirectory, owner.componentSource);
1082
+ const sourceText = contents.get(file);
1083
+ if (!sourceText) continue;
1084
+ const source = ts.createSourceFile(
1085
+ file,
1086
+ sourceText,
1087
+ ts.ScriptTarget.Latest,
1088
+ true,
1089
+ ts.ScriptKind.TSX,
1090
+ );
1091
+ const localComponents = new Map();
1092
+ for (const statement of source.statements) {
1093
+ if (
1094
+ ts.isFunctionDeclaration(statement) &&
1095
+ statement.name &&
1096
+ statement.body
1097
+ )
1098
+ localComponents.set(statement.name.text, statement.body);
1099
+ if (!ts.isVariableStatement(statement)) continue;
1100
+ for (const declaration of statement.declarationList.declarations)
1101
+ if (
1102
+ ts.isIdentifier(declaration.name) &&
1103
+ declaration.initializer &&
1104
+ (ts.isArrowFunction(declaration.initializer) ||
1105
+ ts.isFunctionExpression(declaration.initializer))
1106
+ )
1107
+ localComponents.set(
1108
+ declaration.name.text,
1109
+ declaration.initializer.body,
1110
+ );
1111
+ }
1112
+ const imports = new Map();
1113
+ const namespaces = new Map();
1114
+ const compilerOptions = compilerOptionsFor(file, root, compilerCache);
1115
+ const aliasSource = (module) =>
1116
+ module && !module.startsWith('.') && compilerOptions?.paths
1117
+ ? ts.resolveModuleName(module, file, compilerOptions, ts.sys)
1118
+ .resolvedModule?.resolvedFileName
1119
+ : undefined;
1120
+ for (const statement of source.statements) {
1121
+ if (
1122
+ !ts.isImportDeclaration(statement) ||
1123
+ !ts.isStringLiteral(statement.moduleSpecifier)
1124
+ )
1125
+ continue;
1126
+ const module = statement.moduleSpecifier.text;
1127
+ const bindings = statement.importClause?.namedBindings;
1128
+ if (bindings && ts.isNamespaceImport(bindings)) {
1129
+ namespaces.set(bindings.name.text, module);
1130
+ continue;
1131
+ }
1132
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
1133
+ for (const item of bindings.elements) {
1134
+ const exported = item.propertyName?.text ?? item.name.text;
1135
+ const entry = resolveExportEntry(
1136
+ facts.exportEntries.get(exported),
1137
+ exported,
1138
+ module,
1139
+ file,
1140
+ owner.catalogDirectory,
1141
+ aliasSource(module),
1142
+ );
1143
+ if (entry) imports.set(item.name.text, entry);
1144
+ }
1145
+ }
1146
+ const openingOf = (node) =>
1147
+ ts.isJsxElement(node) ? node.openingElement : node;
1148
+ const entryFor = (opening) => {
1149
+ const tag = opening.tagName.getText(source);
1150
+ if (imports.has(tag)) return imports.get(tag);
1151
+ if (
1152
+ ts.isPropertyAccessExpression(opening.tagName) &&
1153
+ ts.isIdentifier(opening.tagName.expression)
1154
+ ) {
1155
+ const module = namespaces.get(opening.tagName.expression.text);
1156
+ return resolveExportEntry(
1157
+ facts.exportEntries.get(opening.tagName.name.text),
1158
+ opening.tagName.name.text,
1159
+ module,
1160
+ file,
1161
+ owner.catalogDirectory,
1162
+ aliasSource(module),
1163
+ );
1164
+ }
1165
+ return undefined;
1166
+ };
1167
+ const classesOf = (opening) =>
1168
+ literalClasses(
1169
+ opening.attributes.properties.find(
1170
+ (item) =>
1171
+ ts.isJsxAttribute(item) &&
1172
+ ['class', 'className'].includes(item.name.getText(source)),
1173
+ ),
1174
+ ).values;
1175
+ const returnedJsx = (body) => {
1176
+ if (!ts.isBlock(body)) return [body];
1177
+ const expressions = [];
1178
+ const visitReturns = (node) => {
1179
+ if (ts.isReturnStatement(node)) {
1180
+ if (node.expression) expressions.push(node.expression);
1181
+ return;
1182
+ }
1183
+ if (
1184
+ node !== body &&
1185
+ (ts.isFunctionDeclaration(node) ||
1186
+ ts.isFunctionExpression(node) ||
1187
+ ts.isArrowFunction(node))
1188
+ )
1189
+ return;
1190
+ ts.forEachChild(node, visitReturns);
1191
+ };
1192
+ visitReturns(body);
1193
+ return expressions;
1194
+ };
1195
+ const localReturns = (opening, seen) => {
1196
+ const name = opening.tagName.getText(source);
1197
+ const body = localComponents.get(name);
1198
+ if (!body || seen.has(name)) return undefined;
1199
+ return { name, expressions: returnedJsx(body) };
1200
+ };
1201
+ const composedBelow = (node, found, seen = new Set()) => {
1202
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
1203
+ const opening = openingOf(node);
1204
+ const entry = entryFor(opening);
1205
+ if (entry) {
1206
+ for (const type of [
1207
+ entry.boundaries?.rootElement,
1208
+ ...(entry.boundaries?.descendantElements ?? []),
1209
+ ]) {
1210
+ if (!type) continue;
1211
+ const entries = found.get(type) ?? new Set();
1212
+ entries.add(entry);
1213
+ found.set(type, entries);
1214
+ }
1215
+ return;
1216
+ }
1217
+ const local = localReturns(opening, seen);
1218
+ if (local) {
1219
+ const next = new Set([...seen, local.name]);
1220
+ for (const expression of local.expressions)
1221
+ composedBelow(expression, found, next);
1222
+ return;
1223
+ }
1224
+ if (ts.isJsxElement(node))
1225
+ for (const child of node.children) composedBelow(child, found, seen);
1226
+ return;
1227
+ }
1228
+ if (ts.isJsxFragment(node)) {
1229
+ for (const child of node.children) composedBelow(child, found, seen);
1230
+ return;
1231
+ }
1232
+ ts.forEachChild(node, (child) => composedBelow(child, found, seen));
1233
+ };
1234
+ const expandedRoots = (node, seen = new Set()) => {
1235
+ if (ts.isJsxFragment(node))
1236
+ return node.children.flatMap((child) => expandedRoots(child, seen));
1237
+ if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
1238
+ const roots = [];
1239
+ ts.forEachChild(node, (child) =>
1240
+ roots.push(...expandedRoots(child, seen)),
1241
+ );
1242
+ return roots;
1243
+ }
1244
+ const local = localReturns(openingOf(node), seen);
1245
+ if (!local) return [node];
1246
+ const next = new Set([...seen, local.name]);
1247
+ return local.expressions.flatMap((expression) =>
1248
+ expandedRoots(expression, next),
1249
+ );
1250
+ };
1251
+ const visit = (node) => {
1252
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
1253
+ const opening = openingOf(node);
1254
+ const entry = entryFor(opening);
1255
+ if (entry)
1256
+ for (const name of classesOf(opening))
1257
+ record(hooks, ambiguousHooks, name, entry);
1258
+ if (ts.isJsxElement(node)) {
1259
+ const parentClasses = classesOf(opening);
1260
+ if (parentClasses.length) {
1261
+ const descendants = new Map();
1262
+ for (const child of node.children)
1263
+ composedBelow(child, descendants);
1264
+ for (const [type, entries] of descendants)
1265
+ for (const entry of entries)
1266
+ for (const name of parentClasses)
1267
+ record(
1268
+ composedDescendants,
1269
+ ambiguousDescendants,
1270
+ `${name}|${type}`,
1271
+ entry,
1272
+ );
1273
+ }
1274
+ const possible = new Map();
1275
+ let dynamic = false;
1276
+ for (const child of node.children) {
1277
+ if (ts.isJsxExpression(child) && child.expression) dynamic = true;
1278
+ if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
1279
+ continue;
1280
+ for (const root of expandedRoots(child)) {
1281
+ const childOpening = openingOf(root);
1282
+ const childEntry = entryFor(childOpening);
1283
+ const type =
1284
+ childEntry?.boundaries?.rootElement ??
1285
+ (childEntry
1286
+ ? undefined
1287
+ : childOpening.tagName.getText(source).toLowerCase());
1288
+ if (!type) continue;
1289
+ const candidates = possible.get(type) ?? [];
1290
+ candidates.push(childEntry);
1291
+ possible.set(type, candidates);
1292
+ }
1293
+ }
1294
+ if (!dynamic)
1295
+ for (const [type, candidates] of possible)
1296
+ if (
1297
+ candidates.length > 0 &&
1298
+ candidates.every(
1299
+ (candidate) => candidate?.key === candidates[0]?.key,
1300
+ ) &&
1301
+ candidates[0]
1302
+ )
1303
+ for (const name of parentClasses)
1304
+ record(
1305
+ composedChildren,
1306
+ ambiguousChildren,
1307
+ `${name}|${type}`,
1308
+ candidates[0],
1309
+ );
1310
+ }
1311
+ }
1312
+ ts.forEachChild(node, visit);
1313
+ };
1314
+ visit(source);
1315
+ }
1316
+ return { componentMode: true, hooks, composedChildren, composedDescendants };
1317
+ }
1318
+
1319
+ function cssPreferred(contract) {
1320
+ return (
1321
+ [
1322
+ ...(contract.canonicalShorthands ?? []).map((item) => `\`${item}\``),
1323
+ ...(contract.helpers ?? []).map((item) => `\`${item}()\``),
1324
+ ].join(' or ') || 'a cataloged component prop'
1325
+ );
1326
+ }
1327
+
1328
+ function objectProperty(object, name) {
1329
+ return object?.properties.find(
1330
+ (property) =>
1331
+ ts.isPropertyAssignment(property) &&
1332
+ property.name.getText().replaceAll(/["']/g, '') === name,
1333
+ );
1334
+ }
1335
+
1336
+ function nestedCssValues(value, tail) {
1337
+ if (!tail) return value ? [value] : [];
1338
+ if (!value || !ts.isArrayLiteralExpression(value)) return [];
1339
+ return value.elements.flatMap((element) => {
1340
+ if (!ts.isObjectLiteralExpression(element)) return [];
1341
+ const property = objectProperty(element, tail);
1342
+ return property ? [property.initializer] : [];
1343
+ });
1344
+ }
1345
+
1346
+ function jsxCssValues(opening, path) {
1347
+ const [head, tail] = path.split('[].');
1348
+ const attribute = opening.attributes.properties.find(
1349
+ (item) => ts.isJsxAttribute(item) && item.name.getText() === head,
1350
+ );
1351
+ if (!attribute?.initializer) return [];
1352
+ const value = ts.isJsxExpression(attribute.initializer)
1353
+ ? attribute.initializer.expression
1354
+ : attribute.initializer;
1355
+ return nestedCssValues(value, tail);
1356
+ }
1357
+
1358
+ function callCssValues(call, path) {
1359
+ const options = call.arguments[0];
1360
+ if (!options || !ts.isObjectLiteralExpression(options)) return [];
1361
+ const [head, tail] = path.split('[].');
1362
+ const property = objectProperty(options, head);
1363
+ return property ? nestedCssValues(property.initializer, tail) : [];
1364
+ }
1365
+
1366
+ function cssLiteral(value) {
1367
+ if (
1368
+ ts.isStringLiteralLike(value) ||
1369
+ ts.isNoSubstitutionTemplateLiteral(value)
1370
+ )
1371
+ return value.text;
1372
+ return undefined;
1373
+ }
1374
+
1375
+ function importedName(expression, helperImports, namespaces, packageName) {
1376
+ let helper;
1377
+ if (ts.isIdentifier(expression)) helper = helperImports.get(expression.text);
1378
+ else if (
1379
+ ts.isPropertyAccessExpression(expression) &&
1380
+ ts.isIdentifier(expression.expression) &&
1381
+ namespaces.has(expression.expression.text)
1382
+ )
1383
+ helper = {
1384
+ imported: expression.name.text,
1385
+ source: namespaces.get(expression.expression.text),
1386
+ };
1387
+ else helper = undefined;
1388
+ if (!helper) return undefined;
1389
+ return helper.source === packageName ||
1390
+ helper.source.startsWith(`${packageName}/`)
1391
+ ? helper.imported
1392
+ : undefined;
1393
+ }
1394
+
1395
+ function inspectCssValues(
1396
+ file,
1397
+ source,
1398
+ entry,
1399
+ valuesFor,
1400
+ helperImports,
1401
+ namespaces,
1402
+ diagnostics,
1403
+ ) {
1404
+ for (const contract of entry.cssValueProps ?? []) {
1405
+ const preferred = cssPreferred(contract);
1406
+ for (const value of valuesFor(contract.path)) {
1407
+ const at = location(file, value, source);
1408
+ if (
1409
+ contract.grammar === 'declarations' &&
1410
+ contract.rawPolicy !== 'allow'
1411
+ ) {
1412
+ diagnostics.push(
1413
+ diagnostic(
1414
+ 'KUI-L016',
1415
+ at,
1416
+ `\`${entry.name}.${contract.path}\` is a forbidden declaration-list escape; use \`className\`, public tokens, or cataloged props.`,
1417
+ { component: entry.key, path: contract.path },
1418
+ ),
1419
+ );
1420
+ continue;
1421
+ }
1422
+ const literal = cssLiteral(value);
1423
+ if (literal !== undefined) {
1424
+ if (contract.exceptionalShorthands?.includes(literal)) {
1425
+ diagnostics.push(
1426
+ diagnostic(
1427
+ 'KUI-L017',
1428
+ at,
1429
+ `\`${literal}\` is exceptional for \`${entry.name}.${contract.path}\`; prefer ${preferred} unless the off-scale choice is deliberate.`,
1430
+ { component: entry.key, path: contract.path, value: literal },
1431
+ ),
1432
+ );
1433
+ } else if (
1434
+ !contract.shorthands?.includes(literal) &&
1435
+ contract.rawPolicy !== 'allow'
1436
+ ) {
1437
+ diagnostics.push(
1438
+ diagnostic(
1439
+ 'KUI-L013',
1440
+ at,
1441
+ `\`${entry.name}.${contract.path}\` uses ${contract.grammar} grammar; replace raw \`${literal}\` with ${preferred}.`,
1442
+ { component: entry.key, path: contract.path, value: literal },
1443
+ ),
1444
+ );
1445
+ }
1446
+ continue;
1447
+ }
1448
+ if (!ts.isCallExpression(value)) continue;
1449
+ const helper = importedName(
1450
+ value.expression,
1451
+ helperImports,
1452
+ namespaces,
1453
+ entry.package,
1454
+ );
1455
+ if (!helper) continue;
1456
+ if (contract.nonStandaloneHelpers?.includes(helper))
1457
+ diagnostics.push(
1458
+ diagnostic(
1459
+ 'KUI-L015',
1460
+ at,
1461
+ `\`${helper}()\` is not standalone for \`${entry.name}.${contract.path}\`; wrap it with an accepted composer such as \`calc()\`.`,
1462
+ { component: entry.key, path: contract.path, helper },
1463
+ ),
1464
+ );
1465
+ else if (
1466
+ !contract.helpers?.includes(helper) &&
1467
+ contract.unsafeHelper !== helper
1468
+ )
1469
+ diagnostics.push(
1470
+ diagnostic(
1471
+ 'KUI-L014',
1472
+ at,
1473
+ `\`${helper}()\` has the wrong grammar for \`${entry.name}.${contract.path}\`; use ${preferred}.`,
1474
+ { component: entry.key, path: contract.path, helper },
1475
+ ),
1476
+ );
1477
+ }
1478
+ }
1479
+ }
1480
+
1481
+ function inspectTsx(
1482
+ file,
1483
+ sourceText,
1484
+ facts,
1485
+ cssFacts,
1486
+ diagnostics,
1487
+ adoption = false,
1488
+ isForeign = () => true,
1489
+ packageDirectory,
1490
+ compilerOptions,
1491
+ componentMode = false,
1492
+ ) {
1493
+ const source = ts.createSourceFile(
1494
+ file,
1495
+ sourceText,
1496
+ ts.ScriptTarget.Latest,
1497
+ true,
1498
+ ts.ScriptKind.TSX,
1499
+ );
1500
+ const imports = new Map();
1501
+ const helperImports = new Map();
1502
+ const namespaces = new Map();
1503
+ const aliasSources = new Map();
1504
+ const aliasSource = (module) => {
1505
+ if (!module || module.startsWith('.') || !compilerOptions?.paths)
1506
+ return undefined;
1507
+ if (!aliasSources.has(module))
1508
+ aliasSources.set(
1509
+ module,
1510
+ ts.resolveModuleName(module, file, compilerOptions, ts.sys)
1511
+ .resolvedModule?.resolvedFileName,
1512
+ );
1513
+ return aliasSources.get(module);
1514
+ };
1515
+ for (const statement of source.statements) {
1516
+ if (
1517
+ !ts.isImportDeclaration(statement) ||
1518
+ !ts.isStringLiteral(statement.moduleSpecifier)
1519
+ )
1520
+ continue;
1521
+ const module = statement.moduleSpecifier.text;
1522
+ const importClause = statement.importClause;
1523
+ if (importClause?.name)
1524
+ helperImports.set(importClause.name.text, {
1525
+ imported: 'default',
1526
+ source: module,
1527
+ });
1528
+ const bindings = statement.importClause?.namedBindings;
1529
+ if (bindings && ts.isNamespaceImport(bindings)) {
1530
+ namespaces.set(bindings.name.text, module);
1531
+ continue;
1532
+ }
1533
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
1534
+ for (const item of bindings.elements) {
1535
+ const exported = item.propertyName?.text ?? item.name.text;
1536
+ helperImports.set(item.name.text, { imported: exported, source: module });
1537
+ const entry = resolveExportEntry(
1538
+ facts.exportEntries.get(exported),
1539
+ exported,
1540
+ module,
1541
+ file,
1542
+ packageDirectory,
1543
+ aliasSource(module),
1544
+ );
1545
+ if (entry) imports.set(item.name.text, entry);
1546
+ }
1547
+ }
1548
+ const stack = [];
1549
+ const inspectBorrowedMarkup = !isTestModule(file);
1550
+ const borrowed = (className, node, via) => {
1551
+ if (!inspectBorrowedMarkup) return;
1552
+ const owners =
1553
+ facts.classEntries.get(className) ??
1554
+ (componentMode && bemBlock(className) !== className
1555
+ ? facts.blockEntries.get(bemBlock(className))
1556
+ : undefined) ??
1557
+ [];
1558
+ for (const owner of owners) {
1559
+ if (
1560
+ !isForeign(owner, className) ||
1561
+ owner.boundaries?.placeableClasses?.includes(className)
1562
+ )
1563
+ continue;
1564
+ diagnostics.push(
1565
+ diagnostic(
1566
+ 'KUI-L023',
1567
+ location(file, node, source),
1568
+ `\`.${className}\` is rendered in this module's ${via} but belongs to ${componentLabel(owner)}. Render that component or configure it through ${configurationFor(owner)}. ${reportGap(owner)}`,
1569
+ { className, component: owner.key, via },
1570
+ undefined,
1571
+ adoption,
1572
+ ),
1573
+ );
1574
+ }
1575
+ };
1576
+ const borrowedValues = (values, node, via) => {
1577
+ for (const className of new Set(values)) borrowed(className, node, via);
1578
+ };
1579
+ const templateGap = '\uE000';
1580
+ const rawHtmlClasses = (value) => {
1581
+ const classes = [];
1582
+ for (const match of value.matchAll(
1583
+ /<[a-z][^>]*\sclass\s*=\s*["']([^"']+)["']/gi,
1584
+ )) {
1585
+ const pieces = match[1].split(templateGap);
1586
+ for (const [index, piece] of pieces.entries()) {
1587
+ const words = piece.split(/\s+/).filter(Boolean);
1588
+ if (index > 0 && !/^\s/.test(piece)) words.shift();
1589
+ if (index < pieces.length - 1 && !/\s$/.test(piece)) words.pop();
1590
+ classes.push(...words);
1591
+ }
1592
+ }
1593
+ return classes;
1594
+ };
1595
+ const visit = (node) => {
1596
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
1597
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
1598
+ const tag = opening.tagName.getText(source);
1599
+ const namespaceEntry =
1600
+ ts.isPropertyAccessExpression(opening.tagName) &&
1601
+ ts.isIdentifier(opening.tagName.expression)
1602
+ ? resolveExportEntry(
1603
+ facts.exportEntries.get(opening.tagName.name.text),
1604
+ opening.tagName.name.text,
1605
+ namespaces.get(opening.tagName.expression.text),
1606
+ file,
1607
+ packageDirectory,
1608
+ aliasSource(namespaces.get(opening.tagName.expression.text)),
1609
+ )
1610
+ : undefined;
1611
+ const entry = imports.get(tag) ?? namespaceEntry;
1612
+ const classAttribute = opening.attributes.properties.find(
1613
+ (item) =>
1614
+ ts.isJsxAttribute(item) &&
1615
+ ['class', 'className'].includes(item.name.getText(source)),
1616
+ );
1617
+ const classes = literalClasses(classAttribute);
1618
+ const at = location(file, opening, source);
1619
+ if ((!entry && /^[a-z]/.test(tag)) || (componentMode && entry))
1620
+ borrowedValues(
1621
+ classAttribute?.initializer &&
1622
+ ts.isJsxExpression(classAttribute.initializer)
1623
+ ? literalClassValues(classAttribute.initializer.expression)
1624
+ : classes.values,
1625
+ classAttribute ?? opening,
1626
+ 'JSX markup',
1627
+ );
1628
+ if (entry)
1629
+ inspectCssValues(
1630
+ file,
1631
+ source,
1632
+ entry,
1633
+ (path) => jsxCssValues(opening, path),
1634
+ helperImports,
1635
+ namespaces,
1636
+ diagnostics,
1637
+ );
1638
+ if (entry && isForeign(entry))
1639
+ for (const className of classes.values) {
1640
+ // A class owned by the surrounding application is still a hook when
1641
+ // placed on another component's root. Only the component's own
1642
+ // public class is safe to style here.
1643
+ if (
1644
+ classOwnerFor(facts.ownership, className, componentMode)?.key ===
1645
+ entry.key
1646
+ )
1647
+ continue;
1648
+ const styled = cssFacts.get(className)?.styled;
1649
+ if (!styled) continue;
1650
+ diagnostics.push(
1651
+ diagnostic(
1652
+ 'KUI-L022',
1653
+ at,
1654
+ `\`${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)}`,
1655
+ {
1656
+ tag,
1657
+ component: entry.key,
1658
+ className,
1659
+ stylesheet: styled.stylesheet,
1660
+ line: styled.line,
1661
+ selector: styled.selector,
1662
+ },
1663
+ undefined,
1664
+ adoption,
1665
+ ),
1666
+ );
1667
+ }
1668
+ if (classes.dynamic)
1669
+ diagnostics.push(
1670
+ diagnostic(
1671
+ 'KUI-L008',
1672
+ at,
1673
+ 'Dynamic class expression was left unclassified; review it against cataloged public classes.',
1674
+ { tag },
1675
+ ),
1676
+ );
1677
+ const owners = classes.values.flatMap(
1678
+ (className) => facts.classEntries.get(className) ?? [],
1679
+ );
1680
+ for (const property of ['margin', 'border', 'padding']) {
1681
+ const selfOwners = owners.filter(
1682
+ (owner) => owner.layout?.geometry?.[property] === 'self',
1683
+ );
1684
+ if (new Set(selfOwners.map((owner) => owner.key)).size > 1)
1685
+ diagnostics.push(
1686
+ diagnostic(
1687
+ 'KUI-L003',
1688
+ at,
1689
+ `One element combines multiple ${property} owners: ${selfOwners.map((owner) => owner.key).join(', ')}.`,
1690
+ { property, classes: classes.values },
1691
+ selfOwners.map((owner) => owner.key),
1692
+ ),
1693
+ );
1694
+ }
1695
+ const scroll = classes.values.some(
1696
+ (className) => cssFacts.get(className)?.scroll,
1697
+ );
1698
+ const inset = classes.values.some(
1699
+ (className) => cssFacts.get(className)?.inset,
1700
+ );
1701
+ const parent = stack.at(-1);
1702
+ if (scroll && parent?.scroll)
1703
+ diagnostics.push(
1704
+ diagnostic(
1705
+ 'KUI-L007',
1706
+ at,
1707
+ 'A scroll-owning element is nested inside another declared scroll owner.',
1708
+ { classes: classes.values },
1709
+ [parent.label, entry?.key ?? tag],
1710
+ ),
1711
+ );
1712
+ if (inset && parent?.inset)
1713
+ diagnostics.push(
1714
+ diagnostic(
1715
+ 'KUI-L004',
1716
+ at,
1717
+ 'Nested elements both add content padding; confirm that the repeated inset is intentional.',
1718
+ { classes: classes.values },
1719
+ [parent.label, entry?.key ?? tag],
1720
+ ),
1721
+ );
1722
+ stack.push({
1723
+ scroll: scroll || parent?.scroll,
1724
+ inset: inset || parent?.inset,
1725
+ label: entry?.key ?? tag,
1726
+ });
1727
+ // JSX supplied through a prop (for example Toolbar.leading) is still
1728
+ // rendered markup and must be checked like ordinary children.
1729
+ ts.forEachChild(opening.attributes, visit);
1730
+ if (ts.isJsxElement(node))
1731
+ for (const child of node.children) visit(child);
1732
+ stack.pop();
1733
+ return;
1734
+ }
1735
+ if (ts.isCallExpression(node)) {
1736
+ if (ts.isPropertyAccessExpression(node.expression)) {
1737
+ const method = node.expression.name.text;
1738
+ const receiver = node.expression.expression;
1739
+ if (
1740
+ ts.isPropertyAccessExpression(receiver) &&
1741
+ receiver.name.text === 'classList'
1742
+ ) {
1743
+ const written =
1744
+ method === 'add'
1745
+ ? node.arguments
1746
+ : method === 'toggle'
1747
+ ? node.arguments.slice(0, 1)
1748
+ : method === 'replace'
1749
+ ? node.arguments.slice(1, 2)
1750
+ : [];
1751
+ for (const value of written)
1752
+ borrowedValues(literalClassValues(value), value, 'classList write');
1753
+ }
1754
+ if (
1755
+ method === 'setAttribute' &&
1756
+ node.arguments[0] &&
1757
+ ts.isStringLiteralLike(node.arguments[0]) &&
1758
+ node.arguments[0].text === 'class'
1759
+ )
1760
+ borrowedValues(
1761
+ literalClassValues(node.arguments[1]),
1762
+ node.arguments[1] ?? node,
1763
+ 'setAttribute write',
1764
+ );
1765
+ }
1766
+ const entry = ts.isIdentifier(node.expression)
1767
+ ? imports.get(node.expression.text)
1768
+ : ts.isPropertyAccessExpression(node.expression) &&
1769
+ ts.isIdentifier(node.expression.expression)
1770
+ ? resolveExportEntry(
1771
+ facts.exportEntries.get(node.expression.name.text),
1772
+ node.expression.name.text,
1773
+ namespaces.get(node.expression.expression.text),
1774
+ file,
1775
+ packageDirectory,
1776
+ aliasSource(namespaces.get(node.expression.expression.text)),
1777
+ )
1778
+ : undefined;
1779
+ if (entry)
1780
+ inspectCssValues(
1781
+ file,
1782
+ source,
1783
+ entry,
1784
+ (path) => callCssValues(node, path),
1785
+ helperImports,
1786
+ namespaces,
1787
+ diagnostics,
1788
+ );
1789
+ }
1790
+ if (
1791
+ ts.isBinaryExpression(node) &&
1792
+ [ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken].includes(
1793
+ node.operatorToken.kind,
1794
+ ) &&
1795
+ ts.isPropertyAccessExpression(node.left) &&
1796
+ node.left.name.text === 'className'
1797
+ )
1798
+ borrowedValues(
1799
+ literalClassValues(node.right),
1800
+ node.right,
1801
+ 'className assignment',
1802
+ );
1803
+ if (ts.isStringLiteralLike(node) && node.text.includes('<'))
1804
+ borrowedValues(rawHtmlClasses(node.text), node, 'raw HTML');
1805
+ if (ts.isTemplateExpression(node)) {
1806
+ const staticHtml = [
1807
+ node.head.text,
1808
+ ...node.templateSpans.map((span) => span.literal.text),
1809
+ ].join(templateGap);
1810
+ if (staticHtml.includes('<'))
1811
+ borrowedValues(rawHtmlClasses(staticHtml), node, 'raw HTML');
1812
+ }
1813
+ ts.forEachChild(node, visit);
1814
+ };
1815
+ visit(source);
1816
+ }
1817
+
1818
+ export async function analyzeUiProject({
1819
+ root: requestedRoot = process.cwd(),
1820
+ paths,
1821
+ knownRules = [],
1822
+ adoption = false,
1823
+ ownership = 'package',
1824
+ ownershipContext = 'subject',
1825
+ implicitComponentOwnership = false,
1826
+ ownershipGroups = [],
1827
+ profile: packageProfile = resolve(
1828
+ import.meta.dirname,
1829
+ '../ai/application-ui-profile.defaults.json',
1830
+ ),
1831
+ } = {}) {
1832
+ const root = resolve(requestedRoot);
1833
+ const files = await collectFiles(root, paths);
1834
+ const diagnosticDecisions = new Map();
1835
+ const contents = new Map(
1836
+ await Promise.all(
1837
+ files.map(async (file) => [file, await readFile(file, 'utf8')]),
1838
+ ),
1839
+ );
1840
+ const discoveredFiles = new Set(files);
1841
+ for (let index = 0; index < files.length; index += 1) {
1842
+ const file = files[index];
1843
+ for (const dependency of relativeStyleImports(file, contents.get(file))) {
1844
+ const projectPath = relative(root, dependency);
1845
+ if (
1846
+ discoveredFiles.has(dependency) ||
1847
+ projectPath === '..' ||
1848
+ projectPath.startsWith('../')
1849
+ )
1850
+ continue;
1851
+ try {
1852
+ const source = await readFile(dependency, 'utf8');
1853
+ discoveredFiles.add(dependency);
1854
+ files.push(dependency);
1855
+ contents.set(dependency, source);
1856
+ } catch {
1857
+ // TypeScript or the CSS toolchain reports missing imports; analysis stays read-only.
1858
+ }
1859
+ }
1860
+ }
1861
+ files.sort();
1862
+ const imports = new Map(
1863
+ files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
1864
+ );
1865
+ const implicitEntries =
1866
+ ownership === 'component' && implicitComponentOwnership
1867
+ ? await implicitOwnershipEntries(files, imports, root)
1868
+ : [];
1869
+ const groupEntries =
1870
+ ownership === 'component'
1871
+ ? await ownershipGroupEntries(ownershipGroups, root, contents)
1872
+ : [];
1873
+ const groupStyles = new Set(
1874
+ groupEntries.flatMap((entry) =>
1875
+ entry.styleSources.map((style) => resolve(entry.catalogDirectory, style)),
1876
+ ),
1877
+ );
1878
+ const profileContexts = new Map();
1879
+ const loadContext = async (file) => {
1880
+ const startDirectory = file === root ? root : dirname(file);
1881
+ if (!profileContexts.has(startDirectory))
1882
+ profileContexts.set(
1883
+ startDirectory,
1884
+ (async () => {
1885
+ const profileResult = await loadApplicationUiProfile({
1886
+ workspaceRoot: root,
1887
+ startDirectory,
1888
+ packageProfile,
1889
+ knownRules: [...Object.keys(UI_ANALYSIS_RULES), ...knownRules],
1890
+ });
1891
+ const entries = await loadCatalogs(profileResult, ownership);
1892
+ const catalogedSources = new Set(
1893
+ entries
1894
+ .filter((entry) => entry.componentSource)
1895
+ .map((entry) =>
1896
+ resolve(entry.catalogDirectory, entry.componentSource),
1897
+ ),
1898
+ );
1899
+ const ownershipEntries = [
1900
+ ...entries,
1901
+ ...groupEntries,
1902
+ ...implicitEntries.flatMap((entry) => {
1903
+ const source = resolve(
1904
+ entry.catalogDirectory,
1905
+ entry.componentSource,
1906
+ );
1907
+ if (catalogedSources.has(source)) return [];
1908
+ if (
1909
+ groupEntries.some((group) => {
1910
+ const relativeSource = relative(
1911
+ group.catalogDirectory,
1912
+ source,
1913
+ ).replaceAll('\\', '/');
1914
+ return group.ownershipGroupSources.some((path) =>
1915
+ path.endsWith('/')
1916
+ ? relativeSource.startsWith(path)
1917
+ : relativeSource === path,
1918
+ );
1919
+ })
1920
+ )
1921
+ return [];
1922
+ const styleSources = entry.styleSources.filter(
1923
+ (style) =>
1924
+ !groupStyles.has(resolve(entry.catalogDirectory, style)),
1925
+ );
1926
+ return styleSources.length ? [{ ...entry, styleSources }] : [];
1927
+ }),
1928
+ ];
1929
+ return {
1930
+ profileResult,
1931
+ facts: catalogFacts(
1932
+ ownership === 'component'
1933
+ ? inferOwnedClasses(ownershipEntries, contents, imports)
1934
+ : entries,
1935
+ ),
1936
+ startDirectory,
1937
+ };
1938
+ })(),
1939
+ );
1940
+ return profileContexts.get(startDirectory);
1941
+ };
1942
+ const contexts = new Map(
1943
+ await Promise.all(
1944
+ files.map(async (file) => [file, await loadContext(file)]),
1945
+ ),
1946
+ );
1947
+ if (!files.length) await loadContext(root);
1948
+ // A stylesheet or source file belongs to the package whose manifest is
1949
+ // nearest it; entries of every other catalog package are foreign to it.
1950
+ const packages = new Map();
1951
+ const compilerOptions = new Map();
1952
+ const packageOf = async (directory) => {
1953
+ if (!packages.has(directory))
1954
+ packages.set(
1955
+ directory,
1956
+ (async () => {
1957
+ try {
1958
+ const manifest = JSON.parse(
1959
+ await readFile(resolve(directory, 'package.json'), 'utf8'),
1960
+ );
1961
+ if (typeof manifest.name === 'string')
1962
+ return { name: manifest.name, directory };
1963
+ } catch {
1964
+ // No readable manifest here; keep walking up.
1965
+ }
1966
+ const parent = dirname(directory);
1967
+ const inside = relative(root, parent);
1968
+ if (
1969
+ parent === directory ||
1970
+ inside === '..' ||
1971
+ inside.startsWith('../')
1972
+ )
1973
+ return undefined;
1974
+ return packageOf(parent);
1975
+ })(),
1976
+ );
1977
+ return packages.get(directory);
1978
+ };
1979
+ const styledBlocksByFile = new Map();
1980
+ const styledBlocksIn = (style) => {
1981
+ if (!styledBlocksByFile.has(style)) {
1982
+ const blocks = new Set();
1983
+ const css = contents.get(style);
1984
+ if (css)
1985
+ try {
1986
+ postcss.parse(css, { from: style }).walkRules((rule) => {
1987
+ for (const name of subjectClasses(rule.selector))
1988
+ blocks.add(bemBlock(name));
1989
+ });
1990
+ } catch {
1991
+ // Malformed CSS is reported by the normal inspection pass.
1992
+ }
1993
+ styledBlocksByFile.set(style, blocks);
1994
+ }
1995
+ return styledBlocksByFile.get(style);
1996
+ };
1997
+ const foreignTo = async (file, facts) => {
1998
+ const own = await packageOf(dirname(file));
1999
+ const source = own && relative(own.directory, file).replaceAll('\\', '/');
2000
+ const ownedStyles = new Set(imports.get(file) ?? []);
2001
+ if (ownership === 'component' && !file.endsWith('.css'))
2002
+ for (const entry of facts.entries) {
2003
+ if (
2004
+ entry.package !== own?.name ||
2005
+ !entry.componentSource ||
2006
+ !entry.catalogDirectory ||
2007
+ resolve(entry.catalogDirectory, entry.componentSource) !== file
2008
+ )
2009
+ continue;
2010
+ for (const style of entry.styleSources ?? [])
2011
+ ownedStyles.add(resolve(entry.catalogDirectory, style));
2012
+ }
2013
+ return (entry, className) => {
2014
+ if (entry.package !== own?.name) return true;
2015
+ if (ownership !== 'component') return false;
2016
+ if (entry.ownershipGroupSources) {
2017
+ const relativeFile = relative(entry.catalogDirectory, file).replaceAll(
2018
+ '\\',
2019
+ '/',
2020
+ );
2021
+ if (file.endsWith('.css'))
2022
+ return !entry.styleSources?.includes(relativeFile);
2023
+ return !entry.ownershipGroupSources.some((path) =>
2024
+ path.endsWith('/')
2025
+ ? relativeFile.startsWith(path)
2026
+ : relativeFile === path,
2027
+ );
2028
+ }
2029
+ if (
2030
+ !file.endsWith('.css') &&
2031
+ className &&
2032
+ entry.catalogDirectory &&
2033
+ entry.styleSources?.some((style) => {
2034
+ const path = resolve(entry.catalogDirectory, style);
2035
+ return (
2036
+ ownedStyles.has(path) &&
2037
+ styledBlocksIn(path).has(bemBlock(className))
2038
+ );
2039
+ })
2040
+ )
2041
+ return false;
2042
+ const ownedSources = file.endsWith('.css')
2043
+ ? entry.styleSources
2044
+ : [entry.componentSource];
2045
+ return !ownedSources?.includes(source);
2046
+ };
2047
+ };
2048
+ const styleFacts = new Map();
2049
+ const recordDiagnostics = (items, context) => {
2050
+ for (const item of items) {
2051
+ const key = JSON.stringify([
2052
+ item.ruleId,
2053
+ item.location,
2054
+ item.message,
2055
+ item.evidence,
2056
+ item.chain,
2057
+ ]);
2058
+ const decision = diagnosticDecisions.get(key) ?? {
2059
+ item,
2060
+ active: false,
2061
+ suppressed: false,
2062
+ activeConsumers: new Set(),
2063
+ suppressedConsumers: new Set(),
2064
+ };
2065
+ const consumer =
2066
+ relative(root, context.startDirectory).replaceAll('\\', '/') || '.';
2067
+ if (isSuppressed(item, context.profileResult.profile, root)) {
2068
+ decision.suppressed = true;
2069
+ decision.suppressedConsumers.add(consumer);
2070
+ } else {
2071
+ decision.active = true;
2072
+ decision.activeConsumers.add(consumer);
2073
+ }
2074
+ diagnosticDecisions.set(key, decision);
2075
+ }
2076
+ };
2077
+ for (const file of files.filter((item) => item.endsWith('.css'))) {
2078
+ const context = contexts.get(file);
2079
+ const fileFacts = new Map();
2080
+ await inspectCss(
2081
+ file,
2082
+ contents.get(file),
2083
+ context.facts,
2084
+ [],
2085
+ fileFacts,
2086
+ adoption,
2087
+ );
2088
+ styleFacts.set(file, fileFacts);
2089
+ }
2090
+ const styleConsumers = new Map(
2091
+ [...styleFacts.keys()].map((file) => [file, new Set()]),
2092
+ );
2093
+ // KUI-L018 accepts an on-loud from another stylesheet the same entry loads:
2094
+ // every stylesheet reachable from one file (a source module's CSS imports,
2095
+ // or a stylesheet with its @imports) is loaded together.
2096
+ const onLoudByStyle = new Map();
2097
+ for (const style of styleFacts.keys()) {
2098
+ let parsed;
2099
+ try {
2100
+ parsed = postcss.parse(contents.get(style), { from: style });
2101
+ } catch {
2102
+ continue;
2103
+ }
2104
+ onLoudByStyle.set(
2105
+ style,
2106
+ collectLoudDeclarations(style, parsed).filter(
2107
+ ({ kind }) => kind === 'on-loud',
2108
+ ),
2109
+ );
2110
+ }
2111
+ const coLoadedStyles = new Map(
2112
+ [...styleFacts.keys()].map((style) => [style, new Set()]),
2113
+ );
2114
+ for (const file of files) {
2115
+ const loaded = new Set(reachableStyleFiles(file, imports, styleFacts));
2116
+ if (styleFacts.has(file)) loaded.add(file);
2117
+ for (const style of loaded)
2118
+ for (const sibling of loaded)
2119
+ if (sibling !== style) coLoadedStyles.get(style).add(sibling);
2120
+ }
2121
+ const siblingOnLoud = (style) =>
2122
+ [...coLoadedStyles.get(style)]
2123
+ .sort()
2124
+ .flatMap((sibling) => onLoudByStyle.get(sibling) ?? []);
2125
+ for (const file of files.filter((item) =>
2126
+ sourceExtensions.has(extname(item)),
2127
+ )) {
2128
+ const context = contexts.get(file);
2129
+ const own = await packageOf(dirname(file));
2130
+ for (const style of reachableStyleFiles(file, imports, styleFacts))
2131
+ styleConsumers.get(style).add(context);
2132
+ const fileDiagnostics = [];
2133
+ inspectTsx(
2134
+ file,
2135
+ contents.get(file),
2136
+ context.facts,
2137
+ reachableStyleFacts(file, imports, styleFacts),
2138
+ fileDiagnostics,
2139
+ adoption,
2140
+ await foreignTo(file, context.facts),
2141
+ own?.directory,
2142
+ compilerOptionsFor(file, root, compilerOptions),
2143
+ ownership === 'component',
2144
+ );
2145
+ recordDiagnostics(fileDiagnostics, context);
2146
+ }
2147
+ for (const [file, consumers] of styleConsumers) {
2148
+ const applicableContexts = consumers.size
2149
+ ? consumers
2150
+ : new Set([contexts.get(file)]);
2151
+ for (const context of applicableContexts) {
2152
+ const fileDiagnostics = [];
2153
+ const own = await packageOf(dirname(file));
2154
+ await inspectCss(
2155
+ file,
2156
+ contents.get(file),
2157
+ context.facts,
2158
+ fileDiagnostics,
2159
+ new Map(),
2160
+ adoption,
2161
+ siblingOnLoud(file),
2162
+ await foreignTo(file, context.facts),
2163
+ ownership === 'component'
2164
+ ? {
2165
+ ...jsxOwnershipEvidence(
2166
+ file,
2167
+ context.facts,
2168
+ contents,
2169
+ root,
2170
+ compilerOptions,
2171
+ ),
2172
+ ownPackage: own?.name,
2173
+ }
2174
+ : undefined,
2175
+ ownershipContext,
2176
+ );
2177
+ recordDiagnostics(fileDiagnostics, context);
2178
+ }
2179
+ }
2180
+
2181
+ const active = [...diagnosticDecisions.values()]
2182
+ .filter((decision) => decision.active)
2183
+ .map((decision) => {
2184
+ if (
2185
+ !decision.suppressedConsumers.size &&
2186
+ decision.activeConsumers.size <= 1
2187
+ )
2188
+ return decision.item;
2189
+ return {
2190
+ ...decision.item,
2191
+ evidence: {
2192
+ ...decision.item.evidence,
2193
+ consumers: {
2194
+ active: [...decision.activeConsumers].sort(),
2195
+ suppressed: [...decision.suppressedConsumers].sort(),
2196
+ },
2197
+ },
2198
+ };
2199
+ });
2200
+ const suppressed = [...diagnosticDecisions.values()].filter(
2201
+ (decision) => !decision.active && decision.suppressed,
2202
+ ).length;
2203
+ active.sort(
2204
+ (a, b) =>
2205
+ a.location.file.localeCompare(b.location.file) ||
2206
+ a.location.line - b.location.line ||
2207
+ a.ruleId.localeCompare(b.ruleId),
2208
+ );
2209
+ const portablePath = (file) => {
2210
+ if (!file || !isAbsolute(file)) return file;
2211
+ const path = relative(root, file).replaceAll('\\', '/');
2212
+ return path === '..' || path.startsWith('../')
2213
+ ? `<external>/${basename(file)}`
2214
+ : path;
2215
+ };
2216
+ const resolvedContexts = await Promise.all(profileContexts.values());
2217
+ const profileFiles = [
2218
+ ...new Set(
2219
+ resolvedContexts.flatMap(
2220
+ ({ profileResult }) => profileResult.files ?? [],
2221
+ ),
2222
+ ),
2223
+ ];
2224
+ const profilePackageRoot = dirname(profileFiles[0] ?? root);
2225
+ const portableMessage = (message) =>
2226
+ String(message)
2227
+ .replaceAll(root, '<repo-root>')
2228
+ .replaceAll(profilePackageRoot, '<package-root>');
2229
+ const profileDiagnostics = [
2230
+ ...new Map(
2231
+ resolvedContexts
2232
+ .flatMap(({ profileResult }) => profileResult.diagnostics ?? [])
2233
+ .map((item) => [
2234
+ JSON.stringify([item.code, item.source, item.path, item.message]),
2235
+ {
2236
+ ...item,
2237
+ source: portablePath(item.source),
2238
+ message: portableMessage(item.message),
2239
+ },
2240
+ ]),
2241
+ ).values(),
2242
+ ];
2243
+ return {
2244
+ schemaVersion: UI_ANALYSIS_SCHEMA_VERSION,
2245
+ tool: { name: '@kerfjs/ui analyzer', version: 1 },
2246
+ root: '.',
2247
+ files: files.map((file) => relative(root, file).replaceAll('\\', '/')),
2248
+ profile: {
2249
+ files: profileFiles.map(portablePath),
2250
+ diagnostics: profileDiagnostics,
2251
+ },
2252
+ diagnostics: active.map((item) => ({
2253
+ ...item,
2254
+ location: {
2255
+ ...item.location,
2256
+ file: portablePath(item.location.file),
2257
+ },
2258
+ ...(item.evidence?.stylesheet
2259
+ ? {
2260
+ evidence: {
2261
+ ...item.evidence,
2262
+ stylesheet: portablePath(item.evidence.stylesheet),
2263
+ },
2264
+ }
2265
+ : {}),
2266
+ })),
2267
+ summary: {
2268
+ errors:
2269
+ active.filter((item) => item.severity === 'error').length +
2270
+ profileDiagnostics.length,
2271
+ review: active.filter((item) => item.severity === 'review').length,
2272
+ suppressed,
2273
+ },
2274
+ };
2275
+ }
2276
+
2277
+ export function formatUiAnalysisText(report) {
2278
+ const lines = report.profile.diagnostics.map(
2279
+ (item) =>
2280
+ `${item.source ?? '<profile>'}:1:1 error ${item.code} ${item.message}`,
2281
+ );
2282
+ lines.push(
2283
+ ...report.diagnostics.map(
2284
+ (item) =>
2285
+ `${item.location.file}:${item.location.line}:${item.location.column} ${item.severity} ${item.ruleId} ${item.message}`,
2286
+ ),
2287
+ );
2288
+ lines.push(
2289
+ `Kerf UI analysis: ${report.summary.errors} error(s), ${report.summary.review} review finding(s), ${report.summary.suppressed} suppressed.`,
2290
+ );
2291
+ return lines.join('\n');
2292
+ }
2293
+
2294
+ export function formatUiAnalysisSarif(report) {
2295
+ return {
2296
+ version: '2.1.0',
2297
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
2298
+ runs: [
2299
+ {
2300
+ tool: {
2301
+ driver: {
2302
+ name: report.tool.name,
2303
+ version: String(report.tool.version),
2304
+ rules: Object.entries(UI_ANALYSIS_RULES).map(([id, rule]) => ({
2305
+ id,
2306
+ shortDescription: { text: rule.title },
2307
+ })),
2308
+ },
2309
+ },
2310
+ results: [
2311
+ ...report.profile.diagnostics.map((item) => ({
2312
+ ruleId: item.code,
2313
+ level: 'error',
2314
+ message: { text: item.message },
2315
+ locations: [
2316
+ {
2317
+ physicalLocation: {
2318
+ artifactLocation: { uri: item.source ?? '<profile>' },
2319
+ region: { startLine: 1, startColumn: 1 },
2320
+ },
2321
+ },
2322
+ ],
2323
+ properties: { path: item.path },
2324
+ })),
2325
+ ...report.diagnostics.map((item) => ({
2326
+ ruleId: item.ruleId,
2327
+ level: item.severity === 'error' ? 'error' : 'warning',
2328
+ message: { text: item.message },
2329
+ locations: [
2330
+ {
2331
+ physicalLocation: {
2332
+ artifactLocation: { uri: item.location.file },
2333
+ region: {
2334
+ startLine: item.location.line,
2335
+ startColumn: item.location.column,
2336
+ },
2337
+ },
2338
+ },
2339
+ ],
2340
+ properties: { evidence: item.evidence, chain: item.chain },
2341
+ })),
2342
+ ],
2343
+ },
2344
+ ],
2345
+ };
2346
+ }