@kerfjs/ui 5.0.0-beta.97 → 5.0.0-beta.99

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 (416) hide show
  1. package/README.md +42 -1
  2. package/ai/application-ui-diagnostic-ids-v1.json +1 -0
  3. package/ai/component-catalog.json +487 -66
  4. package/ai/component-catalog.schema.json +1 -1
  5. package/ai/component-composition-rules.json +22 -2
  6. package/ai/component-composition.json +360 -33
  7. package/ai/component-composition.schema.json +1 -1
  8. package/ai/components/README.md +18 -17
  9. package/ai/components/anchored-surface.md +13 -0
  10. package/ai/components/application-tabs.md +1 -1
  11. package/ai/components/chip.md +9 -5
  12. package/ai/components/choice-chips.md +15 -8
  13. package/ai/components/collapsible-panel.md +3 -2
  14. package/ai/components/composer.md +83 -0
  15. package/ai/components/content-item.md +2 -2
  16. package/ai/components/corner-status.md +1 -1
  17. package/ai/components/document-baseline.md +3 -3
  18. package/ai/components/foundation.md +2 -2
  19. package/ai/components/layout.md +1 -1
  20. package/ai/components/list-action-row.md +5 -1
  21. package/ai/components/list-item.md +5 -1
  22. package/ai/components/list.md +19 -3
  23. package/ai/components/nav-stack.md +11 -2
  24. package/ai/components/pane.md +13 -4
  25. package/ai/components/popup-menu.md +4 -4
  26. package/ai/components/recipe-app-shell.md +3 -3
  27. package/ai/components/recipe-collapsible-sidebar.md +3 -3
  28. package/ai/components/recipe-compact-toolbar.md +1 -1
  29. package/ai/components/recipe-composer-form.md +3 -3
  30. package/ai/components/recipe-list-detail-dialog.md +2 -2
  31. package/ai/components/recipe-list-workspace-states.md +3 -3
  32. package/ai/components/recipe-loading-inspector.md +1 -1
  33. package/ai/components/recipe-navigation-sidebar.md +3 -3
  34. package/ai/components/recipe-navigation-stack.md +3 -3
  35. package/ai/components/recipe-workspace-header.md +3 -3
  36. package/ai/components/resize.md +2 -2
  37. package/ai/components/split-view.md +4 -2
  38. package/ai/components/sunken-panel.md +2 -2
  39. package/ai/components/surface-scaffold.md +15 -3
  40. package/ai/components/tab-bar.md +1 -0
  41. package/ai/components/tab-navigator.md +4 -0
  42. package/ai/components/toolbar-control-group.md +2 -0
  43. package/ai/components/toolbar-text.md +1 -0
  44. package/ai/components/toolbar.md +1 -0
  45. package/ai/components/workbench.md +1 -0
  46. package/ai/public-api-signatures-v1.md +504 -80
  47. package/ai/skill.md +41 -14
  48. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  49. package/analyzer/component-ownership.mjs +1 -1
  50. package/analyzer/index.mjs +41 -14
  51. package/dist/anchored-surface.d.ts +30 -2
  52. package/dist/anchored-surface.js +1 -1
  53. package/dist/app-tab.js +1 -1
  54. package/dist/browser/composer.js +4 -0
  55. package/dist/catalog-resources.d.ts +3 -2
  56. package/dist/catalog-resources.js +4 -4
  57. package/dist/catalog-resources.js.map +1 -1
  58. package/dist/catalog.d.ts +39 -8
  59. package/dist/catalog.js +55 -32
  60. package/dist/catalog.js.map +1 -1
  61. package/dist/chip.d.ts +41 -9
  62. package/dist/chip.js +1 -1
  63. package/dist/choice-chips.d.ts +40 -16
  64. package/dist/choice-chips.js +1 -1
  65. package/dist/chunk-34U4ANDJ.js +119 -0
  66. package/dist/chunk-34U4ANDJ.js.map +1 -0
  67. package/dist/{chunk-36TL6BCR.js → chunk-3GLYIIFR.js} +34 -6
  68. package/dist/chunk-3GLYIIFR.js.map +1 -0
  69. package/dist/{chunk-HWY4BSLS.js → chunk-3VXDSIBJ.js} +2 -2
  70. package/dist/{chunk-HWY4BSLS.js.map → chunk-3VXDSIBJ.js.map} +1 -1
  71. package/dist/chunk-3WYREXW4.js +120 -0
  72. package/dist/chunk-3WYREXW4.js.map +1 -0
  73. package/dist/chunk-AV7CPAPT.js +82 -0
  74. package/dist/chunk-AV7CPAPT.js.map +1 -0
  75. package/dist/{chunk-XK4E225T.js → chunk-B53PS2LE.js} +2 -2
  76. package/dist/{chunk-XK4E225T.js.map → chunk-B53PS2LE.js.map} +1 -1
  77. package/dist/{chunk-R42D2BAO.js → chunk-CA7LNX3J.js} +2 -2
  78. package/dist/{chunk-R42D2BAO.js.map → chunk-CA7LNX3J.js.map} +1 -1
  79. package/dist/{chunk-AYHC62OZ.js → chunk-CMETHPKR.js} +26 -29
  80. package/dist/chunk-CMETHPKR.js.map +1 -0
  81. package/dist/{chunk-Y43B6DS3.js → chunk-CVD5IZGB.js} +2 -2
  82. package/dist/{chunk-Y43B6DS3.js.map → chunk-CVD5IZGB.js.map} +1 -1
  83. package/dist/{chunk-6STQXRT4.js → chunk-CXZXHZTR.js} +121 -12
  84. package/dist/chunk-CXZXHZTR.js.map +1 -0
  85. package/dist/{chunk-GZE2XVKC.js → chunk-DZJ4GBSP.js} +3 -3
  86. package/dist/{chunk-GZE2XVKC.js.map → chunk-DZJ4GBSP.js.map} +1 -1
  87. package/dist/chunk-ELCMSQNJ.js +49 -0
  88. package/dist/chunk-ELCMSQNJ.js.map +1 -0
  89. package/dist/{chunk-P74VXMB6.js → chunk-EOO6PYGF.js} +7 -14
  90. package/dist/chunk-EOO6PYGF.js.map +1 -0
  91. package/dist/{chunk-PRG4CL3Q.js → chunk-FR76ZJQC.js} +21 -19
  92. package/dist/chunk-FR76ZJQC.js.map +1 -0
  93. package/dist/{chunk-QQ3TTWFH.js → chunk-G3J7JDP6.js} +1 -1
  94. package/dist/{chunk-QQ3TTWFH.js.map → chunk-G3J7JDP6.js.map} +1 -1
  95. package/dist/{chunk-IGTWXPTN.js → chunk-HGV7J4T6.js} +3 -3
  96. package/dist/{chunk-IGTWXPTN.js.map → chunk-HGV7J4T6.js.map} +1 -1
  97. package/dist/{chunk-YWQGSUF2.js → chunk-HLDRH23D.js} +3 -3
  98. package/dist/{chunk-YWQGSUF2.js.map → chunk-HLDRH23D.js.map} +1 -1
  99. package/dist/{chunk-PBT3CPPY.js → chunk-HQZYKJ56.js} +24 -4
  100. package/dist/chunk-HQZYKJ56.js.map +1 -0
  101. package/dist/{chunk-V6W56V3N.js → chunk-HZPHCSCS.js} +16 -3
  102. package/dist/chunk-HZPHCSCS.js.map +1 -0
  103. package/dist/{chunk-GJYQ27C2.js → chunk-INI4WDWC.js} +8 -1
  104. package/dist/chunk-INI4WDWC.js.map +1 -0
  105. package/dist/{chunk-U657IHFB.js → chunk-IQSRHXZR.js} +10 -4
  106. package/dist/chunk-IQSRHXZR.js.map +1 -0
  107. package/dist/{chunk-KQDU7Z7P.js → chunk-K4AT3V3J.js} +2 -1
  108. package/dist/chunk-K4AT3V3J.js.map +1 -0
  109. package/dist/{chunk-QIAPOWHH.js → chunk-KCS6NJ3O.js} +2 -2
  110. package/dist/{chunk-QIAPOWHH.js.map → chunk-KCS6NJ3O.js.map} +1 -1
  111. package/dist/{chunk-UKKRPP56.js → chunk-LOSRXZAH.js} +2 -2
  112. package/dist/{chunk-UKKRPP56.js.map → chunk-LOSRXZAH.js.map} +1 -1
  113. package/dist/{chunk-ROZLJULF.js → chunk-MF6ZUUJZ.js} +8 -3
  114. package/dist/chunk-MF6ZUUJZ.js.map +1 -0
  115. package/dist/{chunk-6IVNMC6R.js → chunk-MHMWQ7RB.js} +2 -2
  116. package/dist/{chunk-6IVNMC6R.js.map → chunk-MHMWQ7RB.js.map} +1 -1
  117. package/dist/{chunk-JYD7G4RG.js → chunk-PIVY32AE.js} +15 -5
  118. package/dist/chunk-PIVY32AE.js.map +1 -0
  119. package/dist/{chunk-BU23Z5HZ.js → chunk-PYBBYSZ3.js} +13 -18
  120. package/dist/chunk-PYBBYSZ3.js.map +1 -0
  121. package/dist/chunk-QDSZTQXL.js +28 -0
  122. package/dist/chunk-QDSZTQXL.js.map +1 -0
  123. package/dist/{chunk-YXOMI7ES.js → chunk-QIG4IGUP.js} +2 -2
  124. package/dist/{chunk-YXOMI7ES.js.map → chunk-QIG4IGUP.js.map} +1 -1
  125. package/dist/{chunk-6BRRUPJY.js → chunk-QROYYGWU.js} +3 -3
  126. package/dist/chunk-QROYYGWU.js.map +1 -0
  127. package/dist/{chunk-FOQK4F3L.js → chunk-RHD7RKGI.js} +28 -7
  128. package/dist/chunk-RHD7RKGI.js.map +1 -0
  129. package/dist/{chunk-BBU2SPRE.js → chunk-T4WJ5VGP.js} +4 -4
  130. package/dist/{chunk-BBU2SPRE.js.map → chunk-T4WJ5VGP.js.map} +1 -1
  131. package/dist/{chunk-6NOLL5KG.js → chunk-TGLUDQP2.js} +3 -3
  132. package/dist/{chunk-6NOLL5KG.js.map → chunk-TGLUDQP2.js.map} +1 -1
  133. package/dist/{chunk-HO6FU3G7.js → chunk-TLMMFQWM.js} +4 -4
  134. package/dist/{chunk-HO6FU3G7.js.map → chunk-TLMMFQWM.js.map} +1 -1
  135. package/dist/chunk-UMKV7YVQ.js +20 -0
  136. package/dist/chunk-UMKV7YVQ.js.map +1 -0
  137. package/dist/{chunk-M2NRCFRO.js → chunk-VFYO3UGZ.js} +2 -2
  138. package/dist/{chunk-M2NRCFRO.js.map → chunk-VFYO3UGZ.js.map} +1 -1
  139. package/dist/{chunk-YOKEL75M.js → chunk-WVS6CUCT.js} +2 -2
  140. package/dist/{chunk-YOKEL75M.js.map → chunk-WVS6CUCT.js.map} +1 -1
  141. package/dist/{chunk-MN3POJFN.js → chunk-YMUIQRWK.js} +2 -2
  142. package/dist/{chunk-MN3POJFN.js.map → chunk-YMUIQRWK.js.map} +1 -1
  143. package/dist/{chunk-Y3LC7JQO.js → chunk-YOA5W5ZH.js} +2 -2
  144. package/dist/{chunk-Y3LC7JQO.js.map → chunk-YOA5W5ZH.js.map} +1 -1
  145. package/dist/{chunk-44A6LP54.js → chunk-YQ2AC4E2.js} +2 -2
  146. package/dist/{chunk-44A6LP54.js.map → chunk-YQ2AC4E2.js.map} +1 -1
  147. package/dist/chunk-ZNROKDSZ.js +54 -0
  148. package/dist/chunk-ZNROKDSZ.js.map +1 -0
  149. package/dist/collapsible-panel.d.ts +3 -2
  150. package/dist/collapsible-panel.js +4 -3
  151. package/dist/collapsible-panel.js.map +1 -1
  152. package/dist/composer.d.ts +62 -0
  153. package/dist/composer.js +5 -0
  154. package/dist/composer.js.map +1 -0
  155. package/dist/corner-status.d.ts +1 -1
  156. package/dist/corner-status.js +1 -1
  157. package/dist/css-values.d.ts +9 -1
  158. package/dist/css-values.js +1 -1
  159. package/dist/disclosure-arrow.js +1 -1
  160. package/dist/empty-state.js +1 -1
  161. package/dist/grid.js +1 -1
  162. package/dist/icon-button.js +1 -1
  163. package/dist/icon-toggle-button.js +1 -1
  164. package/dist/index.d.ts +10 -5
  165. package/dist/index.js +40 -37
  166. package/dist/list-action-row.d.ts +10 -2
  167. package/dist/list-action-row.js +1 -1
  168. package/dist/list-header.js +1 -1
  169. package/dist/list-icon-tile-B8dr97OW.d.ts +18 -0
  170. package/dist/list-item.d.ts +10 -3
  171. package/dist/list-item.js +1 -1
  172. package/dist/list.d.ts +36 -4
  173. package/dist/list.js +1 -1
  174. package/dist/loading-spinner.js +1 -1
  175. package/dist/lucide-icon.js +1 -1
  176. package/dist/{nav-stack-BvSKY448.d.ts → nav-stack-VXTumzKa.d.ts} +53 -4
  177. package/dist/nav-stack.d.ts +3 -2
  178. package/dist/nav-stack.js +2 -1
  179. package/dist/pane.d.ts +40 -3
  180. package/dist/pane.js +1 -1
  181. package/dist/{panel-toolbar-CL9tYOkh.d.ts → panel-toolbar-vg6Ir5Dy.d.ts} +6 -1
  182. package/dist/popup-menu-register.d.ts +2 -1
  183. package/dist/popup-menu-register.js +97 -22
  184. package/dist/popup-menu-register.js.map +1 -1
  185. package/dist/popup-menu.js +1 -1
  186. package/dist/row.js +1 -1
  187. package/dist/scroll-clearance-BikHljTc.d.ts +14 -0
  188. package/dist/select-register.js +11 -0
  189. package/dist/select-register.js.map +1 -1
  190. package/dist/select.js +1 -1
  191. package/dist/skeleton.js +1 -1
  192. package/dist/spacer.js +1 -1
  193. package/dist/split-button.js +1 -1
  194. package/dist/split-view.d.ts +7 -5
  195. package/dist/split-view.js +6 -3
  196. package/dist/split-view.js.map +1 -1
  197. package/dist/state-banner.d.ts +6 -1
  198. package/dist/state-banner.js +1 -1
  199. package/dist/styles/anchored-surface.css +27 -1
  200. package/dist/styles/catalog/components/catalog-example-stack.css +5 -0
  201. package/dist/styles/catalog/components/catalog-example.css +33 -0
  202. package/dist/styles/catalog/components/catalog-stage.css +7 -0
  203. package/dist/styles/chip.css +70 -0
  204. package/dist/styles/components/actions/popup-menu/popup-menu.css +21 -4
  205. package/dist/styles/components/actions/toolbar/toolbar.css +19 -0
  206. package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +75 -1
  207. package/dist/styles/components/actions/toolbar-text/toolbar-text.css +75 -0
  208. package/dist/styles/components/collections/list/list.css +77 -7
  209. package/dist/styles/components/collections/list-action-row/list-action-row.css +177 -22
  210. package/dist/styles/components/collections/list-item/list-item.css +158 -11
  211. package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +27 -1
  212. package/dist/styles/components/feedback/chip/chip.css +70 -0
  213. package/dist/styles/components/feedback/state-banner/state-banner.css +24 -13
  214. package/dist/styles/components/forms/composer/composer.css +100 -0
  215. package/dist/styles/components/forms/select/select.css +1 -0
  216. package/dist/styles/components/forms/token-search-field/token-search-field.css +13 -2
  217. package/dist/styles/components/layout/pane/pane.css +81 -2
  218. package/dist/styles/components/navigation/nav-stack/nav-stack.css +84 -0
  219. package/dist/styles/components/navigation/tab-bar/tab-bar.css +15 -4
  220. package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +79 -4
  221. package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +195 -0
  222. package/dist/styles/composer.css +100 -0
  223. package/dist/styles/foundation.css +66 -0
  224. package/dist/styles/list-action-row.css +177 -22
  225. package/dist/styles/list-item.css +158 -11
  226. package/dist/styles/list.css +77 -7
  227. package/dist/styles/nav-stack.css +84 -0
  228. package/dist/styles/pane.css +81 -2
  229. package/dist/styles/popup-menu.css +21 -4
  230. package/dist/styles/select.css +1 -0
  231. package/dist/styles/state-banner.css +24 -13
  232. package/dist/styles/styles.css +1 -0
  233. package/dist/styles/surface-scaffold.css +195 -0
  234. package/dist/styles/tab-bar.css +15 -4
  235. package/dist/styles/tab-navigator.css +79 -4
  236. package/dist/styles/token-search-field.css +13 -2
  237. package/dist/styles/toolbar-control-group.css +75 -1
  238. package/dist/styles/toolbar-text.css +75 -0
  239. package/dist/styles/toolbar.css +19 -0
  240. package/dist/styles/webawesome.css +18 -0
  241. package/dist/surface-scaffold-register.d.ts +6 -1
  242. package/dist/surface-scaffold-register.js +89 -3
  243. package/dist/surface-scaffold-register.js.map +1 -1
  244. package/dist/surface-scaffold.d.ts +66 -7
  245. package/dist/surface-scaffold.js +1 -1
  246. package/dist/tab-navigator.d.ts +23 -2
  247. package/dist/tab-navigator.js +2 -0
  248. package/dist/tab-navigator.js.map +1 -1
  249. package/dist/token-search-field.d.ts +1 -1
  250. package/dist/token-search-field.js +1 -1
  251. package/dist/{token-search-model-DTcTXWMS.d.ts → token-search-model-aYR0YAei.d.ts} +32 -2
  252. package/dist/token-search-model.d.ts +1 -1
  253. package/dist/toolbar-control-group.js +1 -1
  254. package/dist/toolbar-text.js +1 -1
  255. package/dist/toolbar.d.ts +1 -0
  256. package/dist/value-table.js +1 -1
  257. package/dist/wire-catalog.d.ts +3 -2
  258. package/dist/wire-catalog.js +2 -2
  259. package/dist/wire-composers.d.ts +16 -0
  260. package/dist/wire-composers.js +3 -0
  261. package/dist/wire-composers.js.map +1 -0
  262. package/dist/wire-list-focus-scroll.d.ts +16 -0
  263. package/dist/wire-list-focus-scroll.js +3 -0
  264. package/dist/wire-list-focus-scroll.js.map +1 -0
  265. package/dist/wire-nav-stack.js +135 -0
  266. package/dist/wire-nav-stack.js.map +1 -1
  267. package/dist/wire-scroll-dividers.d.ts +9 -0
  268. package/dist/wire-scroll-dividers.js +1 -1
  269. package/dist/wire-tab-bars.js +1 -1
  270. package/dist/wire-tab-navigator.d.ts +3 -1
  271. package/dist/wire-tab-navigator.js +133 -1
  272. package/dist/wire-tab-navigator.js.map +1 -1
  273. package/dist/wire-token-search-fields.d.ts +1 -1
  274. package/dist/wire-token-search-fields.js +1 -1
  275. package/dist/workbench.d.ts +8 -3
  276. package/dist/workbench.js +2 -1
  277. package/docs/accessibility.md +132 -3
  278. package/docs/anchored-surface.md +46 -4
  279. package/docs/app-layouts.md +23 -2
  280. package/docs/apple-hig.md +1 -0
  281. package/docs/catalog.md +61 -9
  282. package/docs/collapsible-panel.md +9 -6
  283. package/docs/component-contract.md +46 -13
  284. package/docs/component-selection.md +147 -20
  285. package/docs/composer.md +143 -0
  286. package/docs/design/templates/choice-chips/multiple-dark.svg +1 -0
  287. package/docs/design/templates/choice-chips/multiple.svg +1 -0
  288. package/docs/design/templates/choice-chips-dark.svg +1 -1
  289. package/docs/design/templates/choice-chips.svg +1 -1
  290. package/docs/design/templates/composer/chat-dark.svg +1 -0
  291. package/docs/design/templates/composer/chat.svg +1 -0
  292. package/docs/design/templates/composer/prompt-dark.svg +1 -0
  293. package/docs/design/templates/composer/prompt.svg +1 -0
  294. package/docs/design/templates/composer-dark.svg +1 -0
  295. package/docs/design/templates/composer.svg +1 -0
  296. package/docs/design/templates/empty-state/filtered-dark.svg +1 -1
  297. package/docs/design/templates/empty-state/filtered.svg +1 -1
  298. package/docs/design/templates/empty-state/with-action-dark.svg +1 -1
  299. package/docs/design/templates/empty-state/with-action.svg +1 -1
  300. package/docs/design/templates/empty-state-dark.svg +1 -1
  301. package/docs/design/templates/empty-state.svg +1 -1
  302. package/docs/design/templates/list/compact-dark.svg +1 -1
  303. package/docs/design/templates/list/compact.svg +1 -1
  304. package/docs/design/templates/list-action-row/default-dark.svg +1 -1
  305. package/docs/design/templates/list-action-row/default.svg +1 -1
  306. package/docs/design/templates/list-action-row/icon-tile-dark.svg +1 -0
  307. package/docs/design/templates/list-action-row/icon-tile.svg +1 -0
  308. package/docs/design/templates/list-action-row/selected-dark.svg +1 -1
  309. package/docs/design/templates/list-action-row/selected.svg +1 -1
  310. package/docs/design/templates/list-action-row/trailing-menu-dark.svg +1 -1
  311. package/docs/design/templates/list-action-row/trailing-menu.svg +1 -1
  312. package/docs/design/templates/list-action-row-dark.svg +1 -1
  313. package/docs/design/templates/list-action-row.svg +1 -1
  314. package/docs/design/templates/list-dark.svg +1 -1
  315. package/docs/design/templates/list-header/size-l-dark.svg +1 -1
  316. package/docs/design/templates/list-header/size-l.svg +1 -1
  317. package/docs/design/templates/list-header/size-m-dark.svg +1 -1
  318. package/docs/design/templates/list-header/size-m.svg +1 -1
  319. package/docs/design/templates/list-header/size-xl-dark.svg +1 -1
  320. package/docs/design/templates/list-header/size-xl.svg +1 -1
  321. package/docs/design/templates/list-header-dark.svg +1 -1
  322. package/docs/design/templates/list-header.svg +1 -1
  323. package/docs/design/templates/list-item/icon-tile-dark.svg +1 -0
  324. package/docs/design/templates/list-item/icon-tile.svg +1 -0
  325. package/docs/design/templates/list-item/selected-dark.svg +1 -1
  326. package/docs/design/templates/list-item/selected.svg +1 -1
  327. package/docs/design/templates/list-item-dark.svg +1 -1
  328. package/docs/design/templates/list-item.svg +1 -1
  329. package/docs/design/templates/list.svg +1 -1
  330. package/docs/design/templates/segmented-control-dark.svg +1 -1
  331. package/docs/design/templates/segmented-control.svg +1 -1
  332. package/docs/design/templates/skeleton-dark.svg +1 -1
  333. package/docs/design/templates/skeleton.svg +1 -1
  334. package/docs/design/templates/state-banner/danger-dark.svg +1 -1
  335. package/docs/design/templates/state-banner/danger.svg +1 -1
  336. package/docs/design/templates/state-banner/info-badge-dark.svg +1 -1
  337. package/docs/design/templates/state-banner/info-badge.svg +1 -1
  338. package/docs/design/templates/state-banner/info-dark.svg +1 -1
  339. package/docs/design/templates/state-banner/info.svg +1 -1
  340. package/docs/design/templates/state-banner/strip-dark.svg +1 -1
  341. package/docs/design/templates/state-banner/strip.svg +1 -1
  342. package/docs/design/templates/state-banner/success-dark.svg +1 -1
  343. package/docs/design/templates/state-banner/success.svg +1 -1
  344. package/docs/design/templates/state-banner/warning-dark.svg +1 -1
  345. package/docs/design/templates/state-banner/warning.svg +1 -1
  346. package/docs/design/templates/state-banner-dark.svg +1 -1
  347. package/docs/design/templates/state-banner.svg +1 -1
  348. package/docs/design/templates/tab-bar-dark.svg +1 -1
  349. package/docs/design/templates/tab-bar.svg +1 -1
  350. package/docs/design/templates/token-search-field-dark.svg +1 -1
  351. package/docs/design/templates/token-search-field.svg +1 -1
  352. package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
  353. package/docs/design/templates/toolbar-control-group.svg +1 -1
  354. package/docs/design/templates/toolbar-dark.svg +1 -1
  355. package/docs/design/templates/toolbar-text/default-dark.svg +1 -1
  356. package/docs/design/templates/toolbar-text/default.svg +1 -1
  357. package/docs/design/templates/toolbar-text/large-dark.svg +1 -1
  358. package/docs/design/templates/toolbar-text/large.svg +1 -1
  359. package/docs/design/templates/toolbar-text-dark.svg +1 -1
  360. package/docs/design/templates/toolbar-text.svg +1 -1
  361. package/docs/design/templates/toolbar.svg +1 -1
  362. package/docs/design/templates/value-table/details-dark.svg +1 -1
  363. package/docs/design/templates/value-table/details.svg +1 -1
  364. package/docs/design/templates/value-table-dark.svg +1 -1
  365. package/docs/design/templates/value-table.svg +1 -1
  366. package/docs/design-philosophy.md +9 -1
  367. package/docs/examples/command-palette-adapter.tsx +3 -3
  368. package/docs/layout.md +147 -14
  369. package/docs/nav-stack.md +132 -8
  370. package/docs/recipes.md +9 -9
  371. package/docs/split-view.md +3 -3
  372. package/docs/sunken-panel.md +2 -2
  373. package/docs/surface-scaffold.md +138 -7
  374. package/docs/tab-navigator.md +38 -1
  375. package/docs/type-contracts.md +16 -16
  376. package/docs/ui-analyzer.md +3 -1
  377. package/docs/ui-doctor.md +71 -0
  378. package/docs/ux-demo.md +6 -6
  379. package/docs/webawesome-theme.md +7 -0
  380. package/docs/workbench.md +10 -6
  381. package/docs/writing.md +152 -0
  382. package/doctor/copy-capitalization-checks.mjs +591 -0
  383. package/doctor/index.mjs +17 -8
  384. package/llms.txt +105 -11
  385. package/package.json +15 -1
  386. package/ux-demo/recipes/app-shell.tsx +11 -11
  387. package/ux-demo/recipes/collapsible-sidebar.tsx +12 -11
  388. package/ux-demo/recipes/compact-toolbar.tsx +10 -10
  389. package/ux-demo/recipes/composer-form.tsx +8 -8
  390. package/ux-demo/recipes/list-detail-dialog.tsx +13 -13
  391. package/ux-demo/recipes/list-workspace-states.tsx +13 -13
  392. package/ux-demo/recipes/loading-inspector.tsx +10 -10
  393. package/ux-demo/recipes/navigation-sidebar.tsx +7 -7
  394. package/ux-demo/recipes/navigation-stack.tsx +5 -5
  395. package/ux-demo/recipes/workspace-header.tsx +5 -5
  396. package/dist/chunk-36TL6BCR.js.map +0 -1
  397. package/dist/chunk-4LOK4634.js +0 -21
  398. package/dist/chunk-4LOK4634.js.map +0 -1
  399. package/dist/chunk-6BRRUPJY.js.map +0 -1
  400. package/dist/chunk-6STQXRT4.js.map +0 -1
  401. package/dist/chunk-76PWEN5Y.js +0 -64
  402. package/dist/chunk-76PWEN5Y.js.map +0 -1
  403. package/dist/chunk-AYHC62OZ.js.map +0 -1
  404. package/dist/chunk-BU23Z5HZ.js.map +0 -1
  405. package/dist/chunk-DQDTZE4W.js +0 -70
  406. package/dist/chunk-DQDTZE4W.js.map +0 -1
  407. package/dist/chunk-FOQK4F3L.js.map +0 -1
  408. package/dist/chunk-GJYQ27C2.js.map +0 -1
  409. package/dist/chunk-JYD7G4RG.js.map +0 -1
  410. package/dist/chunk-KQDU7Z7P.js.map +0 -1
  411. package/dist/chunk-P74VXMB6.js.map +0 -1
  412. package/dist/chunk-PBT3CPPY.js.map +0 -1
  413. package/dist/chunk-PRG4CL3Q.js.map +0 -1
  414. package/dist/chunk-ROZLJULF.js.map +0 -1
  415. package/dist/chunk-U657IHFB.js.map +0 -1
  416. package/dist/chunk-V6W56V3N.js.map +0 -1
@@ -0,0 +1,591 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, relative } from 'node:path';
3
+
4
+ import ts from 'typescript';
5
+
6
+ import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
7
+
8
+ // KUI-D040: visible labels that Kerf UI's capitalization rule (docs/writing.md)
9
+ // gives title style — button titles, menu items, segment and pop-up choices,
10
+ // tab labels, field labels, list/group headers (including catalog example
11
+ // labels), view/pane/dialog titles, panel toggle names, and region and
12
+ // landmark names — plus the accessible names of icon-only controls, checked
13
+ // against Apple's title-style rule. Only string and template literals passed
14
+ // to known title-style props are read (a template literal's static words, never
15
+ // its interpolated values), so prose, descriptions, placeholders, tooltips,
16
+ // checkbox/switch option text, status lines, LoadingSpinner and Skeleton
17
+ // labels, and dynamic data are never judged. The check reports lowercase words title style capitalizes; it
18
+ // does not report capitalized short words, because Apple capitalizes a
19
+ // preposition of any length inside a phrasal verb ("Log In to the Server").
20
+
21
+ const scriptExtensions = new Set(['.tsx', '.jsx']);
22
+
23
+ // Apple Style Guide, "capitalization": articles, coordinating conjunctions,
24
+ // `to` in infinitives, `as`, and prepositions of four letters or fewer stay
25
+ // lowercase unless they are the first or last word or follow a colon.
26
+ // `via`, `per`, and `vs` are added as other short prepositions.
27
+ export const TITLE_STYLE_MINOR_WORDS = Object.freeze(
28
+ new Set([
29
+ 'a',
30
+ 'an',
31
+ 'the',
32
+ 'and',
33
+ 'but',
34
+ 'or',
35
+ 'nor',
36
+ 'for',
37
+ 'yet',
38
+ 'so',
39
+ 'as',
40
+ 'at',
41
+ 'by',
42
+ 'from',
43
+ 'in',
44
+ 'into',
45
+ 'of',
46
+ 'off',
47
+ 'on',
48
+ 'onto',
49
+ 'out',
50
+ 'over',
51
+ 'to',
52
+ 'up',
53
+ 'with',
54
+ 'per',
55
+ 'via',
56
+ 'vs',
57
+ ]),
58
+ );
59
+
60
+ // Apple keeps the second part of these compounds lowercase.
61
+ const lowercaseCompoundTails = new Set(['built-in', 'plug-in']);
62
+ const longestTitle = 12;
63
+
64
+ const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
65
+ const startsLowercase = (word) => /^\p{Ll}/u.test(word);
66
+
67
+ /** A token title style leaves alone: a number, code, path, URL, or an intentionally lowercase-led brand such as iOS or macOS. */
68
+ function exempt(core) {
69
+ if (!/\p{L}/u.test(core)) return true;
70
+ if (/\d/.test(core)) return true;
71
+ if (/[./\\@_#=<>{}[\]|`~^*+]/.test(core)) return true;
72
+ // Lowercase-led mixed case is a deliberate brand or identifier spelling.
73
+ if (/^\p{Ll}[\p{L}'’-]*\p{Lu}/u.test(core)) return true;
74
+ return false;
75
+ }
76
+
77
+ /** Split a word into leading punctuation, its letters, and trailing punctuation. */
78
+ function parts(token) {
79
+ const match = /^([^\p{L}\p{N}]*)(.*?)([^\p{L}\p{N}]*)$/u.exec(token);
80
+ return { lead: match[1], core: match[2], trail: match[3] };
81
+ }
82
+
83
+ /**
84
+ * Check one visible label against Apple's title-style capitalization.
85
+ *
86
+ * Returns `null` when the text is title style, is a complete sentence (it
87
+ * ends with `.`, `!`, or `?`, so sentence style applies), or is too long to
88
+ * be a label. Otherwise returns the lowercase words to capitalize and the
89
+ * repaired text.
90
+ */
91
+ export function titleStyleViolation(text) {
92
+ const value = String(text).trim();
93
+ if (!value || /\n/.test(value)) return null;
94
+ // A complete sentence takes sentence style and ending punctuation; an
95
+ // ellipsis only marks a command that asks for more input.
96
+ if (/(?<![.…])[.!?]["'”’)\]]*$/u.test(value)) return null;
97
+ const tokens = value.split(/(\s+)/);
98
+ const words = tokens
99
+ .map((token, index) => ({ token, index }))
100
+ .filter(({ token }) => token.trim());
101
+ if (!words.some(({ token }) => /\p{L}/u.test(token))) return null;
102
+ if (words.length > longestTitle) return null;
103
+ const flagged = [];
104
+ let afterColon = false;
105
+ for (const [position, { token, index }] of words.entries()) {
106
+ const { lead, core, trail } = parts(token);
107
+ const first = position === 0 || afterColon;
108
+ const last = position === words.length - 1;
109
+ afterColon = /:$/.test(trail);
110
+ if (!core || exempt(core)) continue;
111
+ const segments = core.split('-');
112
+ const fixed = segments.map((segment, segmentIndex) => {
113
+ if (!segment || !startsLowercase(segment)) return segment;
114
+ if (segmentIndex === 0) {
115
+ const minor = TITLE_STYLE_MINOR_WORDS.has(segment.toLowerCase());
116
+ if (minor && !first && !last && segments.length === 1) return segment;
117
+ return capitalize(segment);
118
+ }
119
+ // Second word of a hyphenated compound, except Built-in and Plug-in,
120
+ // and except a single-letter prefix such as e-mail.
121
+ if (lowercaseCompoundTails.has(core.toLowerCase())) return segment;
122
+ if (segments[0].length === 1) return segment;
123
+ // A minor word inside a longer compound stays lowercase, as it would
124
+ // between separate words: Edge-to-Edge, Up-to-Date.
125
+ if (
126
+ segmentIndex < segments.length - 1 &&
127
+ TITLE_STYLE_MINOR_WORDS.has(segment.toLowerCase())
128
+ )
129
+ return segment;
130
+ return capitalize(segment);
131
+ });
132
+ const repaired = fixed.join('-');
133
+ if (repaired !== core) {
134
+ flagged.push(core);
135
+ tokens[index] = `${lead}${repaired}${trail}`;
136
+ }
137
+ }
138
+ if (!flagged.length) return null;
139
+ return { words: flagged, suggestion: tokens.join('') };
140
+ }
141
+
142
+ // Component props whose string is a visible title-style label, or the
143
+ // accessible name of an icon-only control (docs/writing.md gives those title
144
+ // style too: "More Actions", "Clear Search"). `props` names a direct string
145
+ // prop; `entries` names an array prop whose object entries carry the label
146
+ // under one of `keys` (walked through nested arrays such as a PopupMenu
147
+ // `submenu`).
148
+ const titleStyleProps = new Map([
149
+ ['ListHeader', { props: ['label'] }],
150
+ ['ListActionRow', { props: ['label'] }],
151
+ // A catalog example or stack label renders as a ListHeader and names the
152
+ // specimen's region, so it is a heading.
153
+ ['CatalogExample', { props: ['label'] }],
154
+ ['CatalogExampleStack', { props: ['label'] }],
155
+ ['EmptyState', { props: ['title'] }],
156
+ ['StateBanner', { props: ['title'] }],
157
+ ['SplitButton', { props: ['label', 'optionsLabel'] }],
158
+ ['Combobox', { props: ['label'] }],
159
+ ['IconButton', { props: ['label'] }],
160
+ ['IconToggleButton', { props: ['label'] }],
161
+ ['TokenSearchField', { props: ['clearLabel'] }],
162
+ // NavStack view titles and backText, plus the stack's region name and each
163
+ // view's toolbar and pane region names.
164
+ [
165
+ 'NavStack',
166
+ {
167
+ props: ['backText', 'label'],
168
+ entries: {
169
+ views: ['title', 'label', 'contentLabel'],
170
+ bottomToolbar: ['label'],
171
+ },
172
+ },
173
+ ],
174
+ ['SplitView', { props: ['backLabel'] }],
175
+ ['PopupMenu', { props: ['text', 'label'], entries: { items: ['label'] } }],
176
+ ['SegmentedControl', { entries: { choices: ['label'] } }],
177
+ ['Select', { entries: { choices: ['label'] } }],
178
+ ['TabNavigator', { entries: { tabs: ['label'] } }],
179
+ // Region and landmark names: the accessible name of a pane, toolbar,
180
+ // control group, list, tab bar, or layout region names it, so it is title
181
+ // style ("Ticket Workbench", "View Actions"). Structured-config props carry
182
+ // the same names on their nested panels, toolbars, and panes.
183
+ ['Pane', { props: ['label', 'contentLabel'] }],
184
+ [
185
+ 'Workbench',
186
+ {
187
+ props: ['label'],
188
+ entries: {
189
+ leftRail: ['label', 'contentLabel'],
190
+ rightRail: ['label', 'contentLabel'],
191
+ bottomDrawer: ['label', 'contentLabel'],
192
+ mainToolbar: ['label'],
193
+ mainBottomToolbar: ['label'],
194
+ mainPane: ['contentLabel'],
195
+ },
196
+ },
197
+ ],
198
+ ['Toolbar', { props: ['label'] }],
199
+ ['ToolbarControlGroup', { props: ['label'] }],
200
+ ['SunkenPanel', { props: ['ariaLabel'] }],
201
+ ['List', { props: ['ariaLabel'] }],
202
+ ['TabBar', { props: ['label'] }],
203
+ [
204
+ 'CollapsiblePanel',
205
+ {
206
+ props: ['label'],
207
+ entries: {
208
+ toolbar: ['label'],
209
+ bottomToolbar: ['label'],
210
+ pane: ['contentLabel'],
211
+ },
212
+ },
213
+ ],
214
+ ['FloatingToolbar', { props: ['label'] }],
215
+ ['ResizableRegion', { props: ['label'] }],
216
+ ]);
217
+ // Landmark elements and landmark roles: their `aria-label` names the region.
218
+ const landmarkTags = new Set([
219
+ 'section',
220
+ 'nav',
221
+ 'aside',
222
+ 'main',
223
+ 'header',
224
+ 'footer',
225
+ 'form',
226
+ 'search',
227
+ ]);
228
+ const landmarkRoles = new Set([
229
+ 'region',
230
+ 'navigation',
231
+ 'complementary',
232
+ 'main',
233
+ 'banner',
234
+ 'contentinfo',
235
+ 'form',
236
+ 'search',
237
+ ]);
238
+ // A raw Web Awesome field's or radio group's `label` is a field label. The
239
+ // option text of `wa-checkbox`, `wa-switch`, and `wa-radio` is sentence style
240
+ // and is not read.
241
+ const fieldLabelTags = new Set([
242
+ 'wa-input',
243
+ 'wa-select',
244
+ 'wa-textarea',
245
+ 'wa-radio-group',
246
+ ]);
247
+ const titleToolbarTextSizes = new Set(['xlarge', 'xlarge-fixed', 'large']);
248
+ const buttonTags = new Set(['button', 'wa-button']);
249
+ const titleSlots = new Set(['title', 'leading', 'header']);
250
+ const statusToolbarTextSizes = new Set(['small', 'xsmall']);
251
+
252
+ function tagName(node) {
253
+ const name = node.tagName;
254
+ if (ts.isIdentifier(name)) return name.text;
255
+ if (ts.isPropertyAccessExpression(name)) return name.name.text;
256
+ return name.getText();
257
+ }
258
+
259
+ function attribute(element, name) {
260
+ return element.attributes.properties.find(
261
+ (item) =>
262
+ ts.isJsxAttribute(item) &&
263
+ ts.isIdentifier(item.name) &&
264
+ item.name.text === name,
265
+ );
266
+ }
267
+
268
+ // A template literal's interpolation stands in the checked text as a token
269
+ // with no letters, so title style never judges the interpolated data, and a
270
+ // word that touches an interpolation (`${count}s`) carries the token's digit
271
+ // and is skipped too. The static words keep their positions, so a lowercase
272
+ // word that ends the label (`Edit ${name} shortcut`) is still the last word.
273
+ const holeToken = (index) => `\u2045${index}\u2046`;
274
+ const holePattern = /\u2045(\d+)\u2046/g;
275
+
276
+ /**
277
+ * A template literal with interpolations, read as a label whose static words
278
+ * are checked: `text` holds hole tokens for `title-style` judgment, `display`
279
+ * is the source spelling, and `restore` maps a repaired text back to source.
280
+ */
281
+ function templateLabel(node) {
282
+ const holes = node.templateSpans.map(
283
+ (span) => `\${${span.expression.getText()}}`,
284
+ );
285
+ const text =
286
+ node.head.text +
287
+ node.templateSpans
288
+ .map((span, index) => holeToken(index) + span.literal.text)
289
+ .join('');
290
+ return {
291
+ text,
292
+ node,
293
+ display: text.replace(holePattern, (_, index) => holes[Number(index)]),
294
+ restore: (value) =>
295
+ value.replace(holePattern, (_, index) => holes[Number(index)]),
296
+ };
297
+ }
298
+
299
+ function literalOf(node) {
300
+ if (!node) return undefined;
301
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
302
+ return node;
303
+ if (ts.isTemplateExpression(node)) return templateLabel(node);
304
+ if (ts.isJsxExpression(node)) return literalOf(node.expression);
305
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node))
306
+ return literalOf(node.expression);
307
+ return undefined;
308
+ }
309
+
310
+ function attributeValueNode(item) {
311
+ if (!item?.initializer) return undefined;
312
+ return ts.isJsxExpression(item.initializer)
313
+ ? item.initializer.expression
314
+ : item.initializer;
315
+ }
316
+
317
+ function textAttribute(element, name) {
318
+ const value = attributeValueNode(attribute(element, name));
319
+ return value
320
+ ? ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
321
+ ? value.text
322
+ : undefined
323
+ : undefined;
324
+ }
325
+
326
+ /** Whether a ToolbarText renders a title rather than a status or count. */
327
+ function toolbarTextIsTitle(element) {
328
+ if (attribute(element, 'headingLevel')) return true;
329
+ if (titleToolbarTextSizes.has(textAttribute(element, 'size') ?? ''))
330
+ return true;
331
+ // `title`, `leading`, or `header` slot of a toolbar, pane, or panel: the
332
+ // identity position a toolbar title occupies.
333
+ let parent = element.parent;
334
+ while (
335
+ parent &&
336
+ (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent))
337
+ )
338
+ parent = parent.parent;
339
+ if (
340
+ !parent ||
341
+ !(ts.isJsxAttribute(parent) || ts.isPropertyAssignment(parent)) ||
342
+ !ts.isIdentifier(parent.name) ||
343
+ !titleSlots.has(parent.name.text)
344
+ )
345
+ return false;
346
+ // A small `leading` text in a footer or bottom bar is a status line.
347
+ return !(
348
+ parent.name.text === 'leading' &&
349
+ statusToolbarTextSizes.has(textAttribute(element, 'size') ?? '')
350
+ );
351
+ }
352
+
353
+ function constBindings(sourceFile) {
354
+ const bindings = new Map();
355
+ const visit = (node) => {
356
+ if (
357
+ ts.isVariableDeclaration(node) &&
358
+ ts.isIdentifier(node.name) &&
359
+ node.initializer &&
360
+ ts.isVariableDeclarationList(node.parent) &&
361
+ node.parent.flags & ts.NodeFlags.Const
362
+ )
363
+ bindings.set(node.name.text, node.initializer);
364
+ ts.forEachChild(node, visit);
365
+ };
366
+ visit(sourceFile);
367
+ return bindings;
368
+ }
369
+
370
+ function unwrap(node, bindings, seen) {
371
+ let current = node;
372
+ while (current) {
373
+ if (
374
+ ts.isParenthesizedExpression(current) ||
375
+ ts.isAsExpression(current) ||
376
+ ts.isSatisfiesExpression(current) ||
377
+ ts.isJsxExpression(current)
378
+ ) {
379
+ current = current.expression;
380
+ continue;
381
+ }
382
+ if (
383
+ ts.isIdentifier(current) &&
384
+ bindings.has(current.text) &&
385
+ !seen.has(current.text)
386
+ ) {
387
+ seen.add(current.text);
388
+ current = bindings.get(current.text);
389
+ continue;
390
+ }
391
+ return current;
392
+ }
393
+ return current;
394
+ }
395
+
396
+ /** Collect string literals stored under `keys` in an array of object literals, recursively. */
397
+ function entryLiterals(node, keys, bindings, seen = new Set(), found = []) {
398
+ const value = unwrap(node, bindings, seen);
399
+ if (!value) return found;
400
+ if (ts.isArrayLiteralExpression(value)) {
401
+ for (const element of value.elements)
402
+ entryLiterals(
403
+ ts.isSpreadElement(element) ? element.expression : element,
404
+ keys,
405
+ bindings,
406
+ seen,
407
+ found,
408
+ );
409
+ return found;
410
+ }
411
+ if (ts.isObjectLiteralExpression(value))
412
+ for (const property of value.properties) {
413
+ if (!ts.isPropertyAssignment(property)) continue;
414
+ const key =
415
+ ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
416
+ ? property.name.text
417
+ : undefined;
418
+ if (key && keys.includes(key)) {
419
+ const literal = literalOf(property.initializer);
420
+ if (literal) found.push(literal);
421
+ } else if (key && key !== 'attributes')
422
+ entryLiterals(property.initializer, keys, bindings, seen, found);
423
+ }
424
+ return found;
425
+ }
426
+
427
+ /**
428
+ * A panel toolbar's `toggle: { action, name }` (Workbench, CollapsiblePanel,
429
+ * and catalog panels): `name` is the toggle group's accessible name and
430
+ * completes the composed "Show …" / "Hide …" label, so it is a title-style
431
+ * panel name. Requiring `action` keeps unrelated `toggle` objects out.
432
+ */
433
+ function panelToggleName(node, bindings) {
434
+ const name =
435
+ (ts.isPropertyAssignment(node) || ts.isJsxAttribute(node)) &&
436
+ (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))
437
+ ? node.name.text
438
+ : undefined;
439
+ if (name !== 'toggle') return undefined;
440
+ const value = unwrap(
441
+ ts.isJsxAttribute(node) ? attributeValueNode(node) : node.initializer,
442
+ bindings,
443
+ new Set(),
444
+ );
445
+ if (!value || !ts.isObjectLiteralExpression(value)) return undefined;
446
+ const property = (key) =>
447
+ value.properties.find(
448
+ (item) =>
449
+ ts.isPropertyAssignment(item) &&
450
+ (ts.isIdentifier(item.name) || ts.isStringLiteral(item.name)) &&
451
+ item.name.text === key,
452
+ );
453
+ if (!property('action')) return undefined;
454
+ return literalOf(property('name')?.initializer);
455
+ }
456
+
457
+ /** Visible title-style label literals rendered by one JSX/TSX source file. */
458
+ export function titleStyleLabels(fileName, source) {
459
+ const sourceFile = ts.createSourceFile(
460
+ fileName,
461
+ source,
462
+ ts.ScriptTarget.Latest,
463
+ true,
464
+ fileName.endsWith('.jsx') ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
465
+ );
466
+ const bindings = constBindings(sourceFile);
467
+ const labels = [];
468
+ const add = (literal, owner) => labels.push({ literal, owner });
469
+ const visit = (node) => {
470
+ if (ts.isJsxElement(node)) {
471
+ const name = tagName(node.openingElement);
472
+ if (buttonTags.has(name) || name === 'FieldLabel') {
473
+ const text = node.children
474
+ .filter((child) => ts.isJsxText(child))
475
+ .map((child) => child.text)
476
+ .join(' ')
477
+ .replace(/\s+/g, ' ')
478
+ .trim();
479
+ const onlyText = node.children.every(
480
+ (child) =>
481
+ ts.isJsxText(child) ||
482
+ (ts.isJsxSelfClosingElement(child) &&
483
+ /Icon$/.test(tagName(child))) ||
484
+ (ts.isJsxElement(child) &&
485
+ /Icon$/.test(tagName(child.openingElement))),
486
+ );
487
+ if (text && onlyText)
488
+ labels.push({
489
+ literal: { text, node: node.children.find(ts.isJsxText) },
490
+ owner: name === 'FieldLabel' ? 'FieldLabel' : `<${name}> text`,
491
+ });
492
+ // An icon-only button's accessible name is its label.
493
+ if (!text && onlyText && name !== 'FieldLabel') {
494
+ const literal = literalOf(
495
+ attributeValueNode(attribute(node.openingElement, 'aria-label')),
496
+ );
497
+ if (literal) add(literal, `<${name}> aria-label`);
498
+ }
499
+ }
500
+ }
501
+ const toggleName = panelToggleName(node, bindings);
502
+ if (toggleName) add(toggleName, 'panel toggle name');
503
+ if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
504
+ const name = tagName(node);
505
+ if (name === 'ToolbarText' && toolbarTextIsTitle(node)) {
506
+ const literal = literalOf(attributeValueNode(attribute(node, 'text')));
507
+ if (literal) add(literal, 'ToolbarText text');
508
+ }
509
+ if (
510
+ landmarkTags.has(name) ||
511
+ landmarkRoles.has(textAttribute(node, 'role') ?? '')
512
+ ) {
513
+ const literal = literalOf(
514
+ attributeValueNode(attribute(node, 'aria-label')),
515
+ );
516
+ if (literal) add(literal, `<${name}> aria-label`);
517
+ }
518
+ if (fieldLabelTags.has(name)) {
519
+ const literal = literalOf(attributeValueNode(attribute(node, 'label')));
520
+ if (literal) add(literal, `<${name}> label`);
521
+ }
522
+ const config = titleStyleProps.get(name);
523
+ if (config) {
524
+ for (const prop of config.props ?? []) {
525
+ const literal = literalOf(attributeValueNode(attribute(node, prop)));
526
+ if (literal) add(literal, `${name} ${prop}`);
527
+ }
528
+ for (const [prop, keys] of Object.entries(config.entries ?? {})) {
529
+ const value = attributeValueNode(attribute(node, prop));
530
+ if (value)
531
+ for (const literal of entryLiterals(value, keys, bindings))
532
+ add(literal, `${name} ${prop}`);
533
+ }
534
+ }
535
+ }
536
+ ts.forEachChild(node, visit);
537
+ };
538
+ visit(sourceFile);
539
+ return labels.map(({ literal, owner }) => {
540
+ const node = literal.node ?? literal;
541
+ const start = node.getStart(sourceFile);
542
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
543
+ return {
544
+ text: literal.text,
545
+ ...(literal.display === undefined
546
+ ? {}
547
+ : { display: literal.display, restore: literal.restore }),
548
+ owner,
549
+ line: line + 1,
550
+ column: character + 1,
551
+ };
552
+ });
553
+ }
554
+
555
+ const testModule =
556
+ /(?:^|\/)(?:__tests__\/|[^/]*\.(?:test|spec)\.[cm]?[jt]sx?$)/;
557
+
558
+ export async function checkCopyCapitalization(
559
+ packageRoot,
560
+ inputs,
561
+ selectedPaths,
562
+ ) {
563
+ const diagnostics = [];
564
+ for (const file of inputs) {
565
+ if (!scriptExtensions.has(extname(file))) continue;
566
+ if (isUiTraversalExcluded(packageRoot, file)) continue;
567
+ const path = relative(packageRoot, file).replaceAll('\\', '/');
568
+ if (testModule.test(path)) continue;
569
+ if (selectedPaths?.length && !selectedPaths.includes(path)) continue;
570
+ const source = await readFile(file, 'utf8');
571
+ for (const label of titleStyleLabels(file, source)) {
572
+ const violation = titleStyleViolation(label.text);
573
+ if (!violation) continue;
574
+ const shown = label.display ?? label.text;
575
+ const suggestion = label.restore
576
+ ? label.restore(violation.suggestion)
577
+ : violation.suggestion;
578
+ diagnostics.push({
579
+ id: 'KUI-D040',
580
+ severity: 'warning',
581
+ stage: 'analyzer',
582
+ message: `${label.owner} "${shown}" is not title-style capitalized (${violation.words.map((word) => `"${word}"`).join(', ')}).`,
583
+ location: { file: path, line: label.line, column: label.column },
584
+ action: `Use "${suggestion}": capitalize every word except articles, coordinating conjunctions, and prepositions of four letters or fewer, and always capitalize the first and last word. A complete sentence ends with punctuation and stays sentence style. Suppress KUI-D040 for a lowercase brand or product name.`,
585
+ documentation: '@kerfjs/ui/docs/writing.md#capitalization',
586
+ meaning: 'Visible label is not title-style capitalized',
587
+ });
588
+ }
589
+ }
590
+ return diagnostics;
591
+ }
package/doctor/index.mjs CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from '../analyzer/index.mjs';
29
29
  import { evaluateUi, UI_EVALUATION_RULES } from '../evaluator/index.mjs';
30
30
  import { checkComponentStyles } from './component-style-checks.mjs';
31
+ import { checkCopyCapitalization } from './copy-capitalization-checks.mjs';
31
32
  import {
32
33
  isForeignRuleDefinitionDiagnostic,
33
34
  projectConsumerCoreConfig,
@@ -66,6 +67,10 @@ export const UI_DOCTOR_RULES = Object.freeze({
66
67
  severity: 'error',
67
68
  title: 'Stylesheet selects another component class',
68
69
  },
70
+ 'KUI-D040': {
71
+ severity: 'warning',
72
+ title: 'Visible label is not title-style capitalized',
73
+ },
69
74
  });
70
75
 
71
76
  const sourceExtensions = new Set([
@@ -534,14 +539,13 @@ async function cacheKey(root, packageRoot, mode, paths, config) {
534
539
  }),
535
540
  )
536
541
  .update(await readFile(fileURLToPath(import.meta.url)));
537
- hash.update(
538
- await readFile(
539
- resolve(
540
- dirname(fileURLToPath(import.meta.url)),
541
- 'component-style-checks.mjs',
542
- ),
543
- ),
544
- );
542
+ for (const module of [
543
+ 'component-style-checks.mjs',
544
+ 'copy-capitalization-checks.mjs',
545
+ ])
546
+ hash.update(
547
+ await readFile(resolve(dirname(fileURLToPath(import.meta.url)), module)),
548
+ );
545
549
  for (const file of files)
546
550
  hash.update(portablePath(root, file)).update(await readFile(file));
547
551
  return hash.digest('hex');
@@ -1265,6 +1269,11 @@ export async function runUiDoctor({
1265
1269
  selectedPaths,
1266
1270
  root,
1267
1271
  )),
1272
+ ...(await checkCopyCapitalization(
1273
+ packageRoot,
1274
+ await collectInputs(packageRoot),
1275
+ selectedPaths,
1276
+ )),
1268
1277
  ],
1269
1278
  }))(),
1270
1279
  signal,