@kerfjs/ui 5.0.0-beta.56 → 5.0.0-beta.58

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 (287) hide show
  1. package/README.md +81 -37
  2. package/ai/application-ui-diagnostic-ids-v1.json +9 -1
  3. package/ai/application-ui-profile-sync.cjs +1 -1
  4. package/ai/application-ui-profile.defaults.json +63 -8
  5. package/ai/application-ui-profile.mjs +1 -1
  6. package/ai/component-catalog.json +211 -33
  7. package/ai/component-catalog.schema.json +2 -0
  8. package/ai/component-class-owners.cjs +83 -0
  9. package/ai/component-composition-extension.d.ts +7 -0
  10. package/ai/component-composition-extension.schema.json +7 -0
  11. package/ai/{component-catalog-v2-overrides.json → component-composition-rules.json} +2 -1
  12. package/ai/{component-catalog-v2.d.ts → component-composition.d.ts} +9 -5
  13. package/ai/{component-catalog-v2.json → component-composition.json} +155 -24
  14. package/ai/{component-catalog-v2.schema.json → component-composition.schema.json} +21 -5
  15. package/ai/components/README.md +170 -0
  16. package/ai/components/application-tabs.md +65 -0
  17. package/ai/components/badge.md +72 -0
  18. package/ai/components/collapsible-panel.md +102 -0
  19. package/ai/components/content-item.md +90 -0
  20. package/ai/components/disclosure-arrow.md +77 -0
  21. package/ai/components/document-baseline.md +75 -0
  22. package/ai/components/empty-state.md +80 -0
  23. package/ai/components/feedback.md +71 -0
  24. package/ai/components/floating-toolbar.md +80 -0
  25. package/ai/components/foundation.md +65 -0
  26. package/ai/components/grid.md +93 -0
  27. package/ai/components/headers.md +63 -0
  28. package/ai/components/layout.md +75 -0
  29. package/ai/components/list-action-row.md +91 -0
  30. package/ai/components/list-header.md +89 -0
  31. package/ai/components/list-inset-control.md +74 -0
  32. package/ai/components/list-inset-text.md +72 -0
  33. package/ai/components/list-item.md +88 -0
  34. package/ai/components/list.md +103 -0
  35. package/ai/components/loading-spinner.md +72 -0
  36. package/ai/components/lucide-icon.md +66 -0
  37. package/ai/components/nav-stack.md +103 -0
  38. package/ai/components/pane.md +100 -0
  39. package/ai/components/popup-menu.md +82 -0
  40. package/ai/components/recipe-app-shell.md +57 -0
  41. package/ai/components/recipe-collapsible-sidebar.md +57 -0
  42. package/ai/components/recipe-compact-toolbar.md +57 -0
  43. package/ai/components/recipe-composer-form.md +57 -0
  44. package/ai/components/recipe-list-detail-dialog.md +57 -0
  45. package/ai/components/recipe-list-workspace-states.md +57 -0
  46. package/ai/components/recipe-loading-inspector.md +57 -0
  47. package/ai/components/recipe-navigation-sidebar.md +57 -0
  48. package/ai/components/recipe-navigation-stack.md +57 -0
  49. package/ai/components/recipe-workspace-header.md +57 -0
  50. package/ai/components/resize.md +101 -0
  51. package/ai/components/row.md +96 -0
  52. package/ai/components/segmented-control.md +82 -0
  53. package/ai/components/select.md +107 -0
  54. package/ai/components/skeleton.md +84 -0
  55. package/ai/components/spacer.md +86 -0
  56. package/ai/components/split-view.md +102 -0
  57. package/ai/components/state-banner.md +92 -0
  58. package/ai/components/sunken-panel.md +79 -0
  59. package/ai/components/surface-scaffold.md +78 -0
  60. package/ai/components/tab-bar.md +112 -0
  61. package/ai/components/tab-scaffold.md +94 -0
  62. package/ai/components/tabs.md +90 -0
  63. package/ai/components/text.md +82 -0
  64. package/ai/components/token-search-field.md +101 -0
  65. package/ai/components/toolbar-control-group.md +102 -0
  66. package/ai/components/toolbar-text.md +76 -0
  67. package/ai/components/toolbar.md +95 -0
  68. package/ai/components/value-table.md +72 -0
  69. package/ai/components/wa-accordion-item.md +55 -0
  70. package/ai/components/wa-accordion.md +65 -0
  71. package/ai/components/wa-animated-image.md +55 -0
  72. package/ai/components/wa-animation.md +55 -0
  73. package/ai/components/wa-avatar.md +55 -0
  74. package/ai/components/wa-badge.md +55 -0
  75. package/ai/components/wa-breadcrumb-item.md +53 -0
  76. package/ai/components/wa-breadcrumb.md +55 -0
  77. package/ai/components/wa-button-group.md +59 -0
  78. package/ai/components/wa-button.md +55 -0
  79. package/ai/components/wa-callout.md +58 -0
  80. package/ai/components/wa-card.md +59 -0
  81. package/ai/components/wa-carousel-item.md +53 -0
  82. package/ai/components/wa-carousel.md +55 -0
  83. package/ai/components/wa-checkbox-group.md +55 -0
  84. package/ai/components/wa-checkbox.md +55 -0
  85. package/ai/components/wa-color-picker.md +55 -0
  86. package/ai/components/wa-comparison.md +55 -0
  87. package/ai/components/wa-copy-button.md +55 -0
  88. package/ai/components/wa-details.md +61 -0
  89. package/ai/components/wa-dialog.md +59 -0
  90. package/ai/components/wa-divider.md +53 -0
  91. package/ai/components/wa-drawer.md +55 -0
  92. package/ai/components/wa-dropdown-item.md +60 -0
  93. package/ai/components/wa-dropdown.md +60 -0
  94. package/ai/components/wa-format-bytes.md +53 -0
  95. package/ai/components/wa-format-date.md +53 -0
  96. package/ai/components/wa-format-number.md +53 -0
  97. package/ai/components/wa-icon.md +58 -0
  98. package/ai/components/wa-include.md +53 -0
  99. package/ai/components/wa-input.md +59 -0
  100. package/ai/components/wa-intersection-observer.md +53 -0
  101. package/ai/components/wa-known-date.md +53 -0
  102. package/ai/components/wa-markdown.md +53 -0
  103. package/ai/components/wa-mutation-observer.md +53 -0
  104. package/ai/components/wa-number-input.md +55 -0
  105. package/ai/components/wa-option.md +59 -0
  106. package/ai/components/wa-otp-input.md +53 -0
  107. package/ai/components/wa-page.md +53 -0
  108. package/ai/components/wa-pagination.md +55 -0
  109. package/ai/components/wa-popover.md +55 -0
  110. package/ai/components/wa-popup.md +53 -0
  111. package/ai/components/wa-progress-bar.md +53 -0
  112. package/ai/components/wa-progress-ring.md +53 -0
  113. package/ai/components/wa-qr-code.md +53 -0
  114. package/ai/components/wa-radio-group.md +59 -0
  115. package/ai/components/wa-radio.md +55 -0
  116. package/ai/components/wa-random-content.md +53 -0
  117. package/ai/components/wa-rating.md +55 -0
  118. package/ai/components/wa-relative-time.md +53 -0
  119. package/ai/components/wa-resize-observer.md +53 -0
  120. package/ai/components/wa-scroller.md +53 -0
  121. package/ai/components/wa-select.md +63 -0
  122. package/ai/components/wa-skeleton.md +53 -0
  123. package/ai/components/wa-slider.md +55 -0
  124. package/ai/components/wa-spinner.md +58 -0
  125. package/ai/components/wa-split-panel.md +58 -0
  126. package/ai/components/wa-switch.md +53 -0
  127. package/ai/components/wa-tab-group.md +69 -0
  128. package/ai/components/wa-tab-panel.md +60 -0
  129. package/ai/components/wa-tab.md +59 -0
  130. package/ai/components/wa-tag.md +59 -0
  131. package/ai/components/wa-textarea.md +53 -0
  132. package/ai/components/wa-time-input.md +55 -0
  133. package/ai/components/wa-toast-item.md +60 -0
  134. package/ai/components/wa-toast.md +60 -0
  135. package/ai/components/wa-tooltip.md +55 -0
  136. package/ai/components/wa-tree-item.md +55 -0
  137. package/ai/components/wa-tree.md +55 -0
  138. package/ai/components/wa-zoomable-frame.md +55 -0
  139. package/ai/components/workbench.md +114 -0
  140. package/ai/public-api-signatures-v1.md +174 -43
  141. package/ai/skill.md +100 -48
  142. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  143. package/analyzer/component-ownership.d.mts +56 -0
  144. package/analyzer/component-ownership.mjs +184 -0
  145. package/analyzer/index.d.mts +3 -0
  146. package/analyzer/index.mjs +274 -20
  147. package/analyzer/selectors.d.mts +21 -0
  148. package/analyzer/selectors.mjs +212 -0
  149. package/dist/browser/catalog.js +25 -0
  150. package/dist/browser/collapsible-panel.js +7 -0
  151. package/dist/browser/nav-stack.js +8 -0
  152. package/dist/browser/split-view.js +10 -0
  153. package/dist/browser/tab-scaffold.js +4 -0
  154. package/dist/browser/workbench.js +10 -0
  155. package/dist/catalog-resources.d.ts +2 -2
  156. package/dist/catalog.d.ts +3 -3
  157. package/dist/catalog.js +8 -7
  158. package/dist/catalog.js.map +1 -1
  159. package/dist/{chunk-QQOTQTDD.js → chunk-2CCMJTVY.js} +13 -10
  160. package/dist/chunk-2CCMJTVY.js.map +1 -0
  161. package/dist/{chunk-PK4N5QTO.js → chunk-553ASUK6.js} +2 -3
  162. package/dist/chunk-553ASUK6.js.map +1 -0
  163. package/dist/{chunk-G33XC2NB.js → chunk-6YFHCLHG.js} +2 -2
  164. package/dist/chunk-6YFHCLHG.js.map +1 -0
  165. package/dist/{chunk-ZIYOJFZD.js → chunk-7JUBXOIV.js} +17 -4
  166. package/dist/chunk-7JUBXOIV.js.map +1 -0
  167. package/dist/{chunk-T4UZTNZ4.js → chunk-EVYKTVBQ.js} +2 -11
  168. package/dist/chunk-EVYKTVBQ.js.map +1 -0
  169. package/dist/{chunk-AK67F6QN.js → chunk-EZVNC4J6.js} +4 -1
  170. package/dist/chunk-EZVNC4J6.js.map +1 -0
  171. package/dist/{chunk-MA6Z6EIM.js → chunk-F4QMLOXJ.js} +3 -2
  172. package/dist/chunk-F4QMLOXJ.js.map +1 -0
  173. package/dist/chunk-JCV4GXWB.js +249 -0
  174. package/dist/chunk-JCV4GXWB.js.map +1 -0
  175. package/dist/chunk-K47P7QZC.js +17 -0
  176. package/dist/chunk-K47P7QZC.js.map +1 -0
  177. package/dist/{chunk-MOBRFZH3.js → chunk-OUK6QRDR.js} +2 -12
  178. package/dist/chunk-OUK6QRDR.js.map +1 -0
  179. package/dist/{chunk-VNPQDCQZ.js → chunk-QOWG4ZCL.js} +3 -2
  180. package/dist/chunk-QOWG4ZCL.js.map +1 -0
  181. package/dist/{chunk-V65EOWNF.js → chunk-QPUPLNNB.js} +20 -8
  182. package/dist/chunk-QPUPLNNB.js.map +1 -0
  183. package/dist/{chunk-O3EXIBEZ.js → chunk-W4XKTST6.js} +8 -7
  184. package/dist/chunk-W4XKTST6.js.map +1 -0
  185. package/dist/{chunk-K6UNVKDK.js → chunk-XDKAYSVD.js} +3 -1
  186. package/dist/chunk-XDKAYSVD.js.map +1 -0
  187. package/dist/collapsible-panel.d.ts +9 -9
  188. package/dist/collapsible-panel.js +2 -2
  189. package/dist/collapsible-panel.js.map +1 -1
  190. package/dist/index.d.ts +2 -1
  191. package/dist/index.js +11 -9
  192. package/dist/list-action-row.js +2 -1
  193. package/dist/list-header.js +1 -1
  194. package/dist/list-item.js +2 -1
  195. package/dist/nav-stack.d.ts +13 -1
  196. package/dist/nav-stack.js +1 -1
  197. package/dist/pane.d.ts +20 -3
  198. package/dist/pane.js +1 -1
  199. package/dist/{panel-toolbar-C8sF-YZC.d.ts → panel-toolbar-DqwtBxQL.d.ts} +13 -13
  200. package/dist/select.js +1 -1
  201. package/dist/split-view.d.ts +3 -3
  202. package/dist/split-view.js +3 -2
  203. package/dist/split-view.js.map +1 -1
  204. package/dist/state-banner.js +1 -1
  205. package/dist/styles/app-tab.css +31 -0
  206. package/dist/styles/badge.css +19 -11
  207. package/dist/styles/collapsible-panel.css +13 -9
  208. package/dist/styles/floating-toolbar.css +25 -6
  209. package/dist/styles/list-header.css +0 -12
  210. package/dist/styles/list.css +4 -4
  211. package/dist/styles/nav-stack.css +46 -19
  212. package/dist/styles/pane.css +50 -0
  213. package/dist/styles/popup-menu.css +151 -0
  214. package/dist/styles/resizable-region.css +22 -10
  215. package/dist/styles/select.css +28 -14
  216. package/dist/styles/skeleton.css +30 -3
  217. package/dist/styles/split-view.css +10 -7
  218. package/dist/styles/state-banner.css +0 -19
  219. package/dist/styles/surface-scaffold.css +8 -3
  220. package/dist/styles/tab-bar.css +79 -28
  221. package/dist/styles/tab-scaffold.css +25 -1
  222. package/dist/styles/text.css +16 -0
  223. package/dist/styles/toolbar-control-group.css +121 -66
  224. package/dist/styles/toolbar-text.css +21 -0
  225. package/dist/styles/toolbar.css +8 -32
  226. package/dist/styles/workbench.css +18 -18
  227. package/dist/tab-scaffold.d.ts +11 -1
  228. package/dist/tab-scaffold.js +2 -0
  229. package/dist/tab-scaffold.js.map +1 -1
  230. package/dist/toolbar-control-group.d.ts +3 -1
  231. package/dist/toolbar-control-group.js +1 -1
  232. package/dist/toolbar-text.d.ts +7 -1
  233. package/dist/toolbar-text.js +1 -1
  234. package/dist/toolbar.d.ts +8 -1
  235. package/dist/toolbar.js +1 -1
  236. package/dist/{types-DXAGogOS.d.ts → types-BuHELdXR.d.ts} +1 -1
  237. package/dist/wire-catalog.d.ts +3 -1
  238. package/dist/wire-catalog.js +2 -0
  239. package/dist/wire-catalog.js.map +1 -1
  240. package/dist/wire-scroll-dividers.d.ts +69 -0
  241. package/dist/wire-scroll-dividers.js +3 -0
  242. package/dist/wire-scroll-dividers.js.map +1 -0
  243. package/dist/workbench.d.ts +17 -16
  244. package/dist/workbench.js +1 -1
  245. package/docs/app-layouts.md +9 -1
  246. package/docs/catalog.md +10 -5
  247. package/docs/collapsible-panel.md +22 -13
  248. package/docs/component-contract.md +147 -21
  249. package/docs/component-selection.md +21 -8
  250. package/docs/design/templates/tab-bar/workspace-tabs-dark.svg +1 -1
  251. package/docs/design/templates/tab-bar/workspace-tabs.svg +1 -1
  252. package/docs/design/templates/tab-bar-dark.svg +1 -1
  253. package/docs/design/templates/tab-bar.svg +1 -1
  254. package/docs/design-philosophy.md +30 -3
  255. package/docs/examples/application-ui-profile.json +4 -4
  256. package/docs/examples/{component-catalog-extension-v2.json → component-composition-extension.json} +3 -3
  257. package/docs/layout.md +82 -3
  258. package/docs/nav-stack.md +33 -3
  259. package/docs/split-view.md +10 -5
  260. package/docs/tab-scaffold.md +23 -1
  261. package/docs/type-contracts.md +4 -3
  262. package/docs/ui-analyzer.md +99 -5
  263. package/docs/ui-doctor.md +10 -0
  264. package/docs/workbench.md +31 -20
  265. package/doctor/index.mjs +6 -1
  266. package/llms.txt +85 -20
  267. package/package.json +17 -5
  268. package/ux-demo/recipes/collapsible-sidebar.tsx +0 -1
  269. package/ux-demo/recipes/composer-form.tsx +0 -1
  270. package/ux-demo/recipes/list-detail-dialog.tsx +0 -2
  271. package/ux-demo/recipes/list-workspace-states.tsx +0 -1
  272. package/ux-demo/recipes/loading-inspector.tsx +0 -1
  273. package/ux-demo/recipes/workspace-header.tsx +0 -1
  274. package/ai/component-catalog-extension-v2.d.ts +0 -7
  275. package/ai/component-catalog-extension-v2.schema.json +0 -7
  276. package/dist/chunk-AK67F6QN.js.map +0 -1
  277. package/dist/chunk-G33XC2NB.js.map +0 -1
  278. package/dist/chunk-K6UNVKDK.js.map +0 -1
  279. package/dist/chunk-MA6Z6EIM.js.map +0 -1
  280. package/dist/chunk-MOBRFZH3.js.map +0 -1
  281. package/dist/chunk-O3EXIBEZ.js.map +0 -1
  282. package/dist/chunk-PK4N5QTO.js.map +0 -1
  283. package/dist/chunk-QQOTQTDD.js.map +0 -1
  284. package/dist/chunk-T4UZTNZ4.js.map +0 -1
  285. package/dist/chunk-V65EOWNF.js.map +0 -1
  286. package/dist/chunk-VNPQDCQZ.js.map +0 -1
  287. package/dist/chunk-ZIYOJFZD.js.map +0 -1
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Downstream component-ownership facts for `kerf-ui-analyze` (`KUI-L019` –
3
+ * `KUI-L022`): the consumer-side form of the package's own
4
+ * `check:css-ownership` rules.
5
+ *
6
+ * Components own their styles and are configured, never overridden. An
7
+ * application or component package may style its own elements, including in a
8
+ * cataloged component's context (`.kui-toolbar > .my-widget` lives in the
9
+ * application's stylesheet), but it never makes another package's component
10
+ * the subject of a rule, writes that component's private variables, overrides
11
+ * a token its typed prop configures, or restyles it through a hook class on
12
+ * its root. Every fact here comes from the loaded composition catalogs, so
13
+ * third-party component packages get the same protection as `@kerfjs/ui`.
14
+ */
15
+ import {
16
+ classNames,
17
+ complexSelectorParts,
18
+ dataComponents,
19
+ subjectTypes,
20
+ withoutRelationalArguments,
21
+ } from './selectors.mjs';
22
+
23
+ const kebab = (value) =>
24
+ value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
25
+
26
+ function isWebAwesomeTagEntry(entry) {
27
+ return entry.source === 'webawesome' && /^wa-[a-z0-9-]+$/.test(entry.id);
28
+ }
29
+
30
+ /**
31
+ * Index the loaded catalog entries by every handle a stylesheet can use to
32
+ * reach them: public classes, `data-component` values, Web Awesome tags the
33
+ * package themes, private-variable prefixes, and component tokens a typed
34
+ * prop configures.
35
+ */
36
+ export function componentOwnershipFacts(entries) {
37
+ const classOwners = new Map();
38
+ const dataComponentOwners = new Map();
39
+ const tagOwners = new Map();
40
+ const privatePrefixes = [];
41
+ const typedTokens = new Map();
42
+ const setOnce = (map, key, entry) => {
43
+ if (!map.has(key)) map.set(key, entry);
44
+ };
45
+ for (const entry of entries) {
46
+ const boundaries = entry.boundaries ?? {};
47
+ for (const className of boundaries.publicClasses ?? []) {
48
+ setOnce(classOwners, className, entry);
49
+ if (
50
+ className.startsWith('kui-') &&
51
+ !className.includes('__') &&
52
+ !className.includes('--')
53
+ )
54
+ setOnce(dataComponentOwners, className.slice('kui-'.length), entry);
55
+ }
56
+ if (entry.id && !isWebAwesomeTagEntry(entry))
57
+ setOnce(dataComponentOwners, entry.id, entry);
58
+ if (isWebAwesomeTagEntry(entry)) setOnce(tagOwners, entry.id, entry);
59
+ // Every root-like public class names the component (Toolbar Control
60
+ // Group's first class is its action link, not the group).
61
+ const roots = [
62
+ ...new Set(
63
+ [boundaries.rootClass, ...(boundaries.publicClasses ?? [])].filter(
64
+ (name) => name && !name.includes('__') && !name.includes('--'),
65
+ ),
66
+ ),
67
+ ];
68
+ const tokens = new Set(boundaries.publicTokens ?? []);
69
+ for (const root of roots) {
70
+ privatePrefixes.push({ prefix: `--_${root}`, entry });
71
+ for (const contract of entry.cssValueProps ?? []) {
72
+ const [head] = contract.path.split('[].');
73
+ const token = `--${root}-${kebab(head)}`;
74
+ if (tokens.has(token))
75
+ setOnce(typedTokens, token, { entry, path: contract.path });
76
+ }
77
+ }
78
+ }
79
+ privatePrefixes.sort((a, b) => b.prefix.length - a.prefix.length);
80
+ return {
81
+ classOwners,
82
+ dataComponentOwners,
83
+ tagOwners,
84
+ privatePrefixes,
85
+ typedTokens,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The cataloged components a selector list makes its subject: the rightmost
91
+ * compound of each complex selector, with `:has()` / `:not()` arguments
92
+ * removed because keying on or excluding a component does not style it. A
93
+ * subject that is a `::part()` is left to the shadow-part rule (`KUI-L011`).
94
+ * Returns `{ entry, via, name }` records; `isForeign(entry)` decides which
95
+ * entries the stylesheet does not own.
96
+ */
97
+ export function restyledComponents(selectorList, facts, isForeign) {
98
+ const found = [];
99
+ const seen = new Set();
100
+ const add = (entry, via, name) => {
101
+ if (!entry || !isForeign(entry)) return;
102
+ const key = `${entry.key}|${via}|${name}`;
103
+ if (seen.has(key)) return;
104
+ seen.add(key);
105
+ found.push({ entry, via, name });
106
+ };
107
+ for (const parts of complexSelectorParts(selectorList)) {
108
+ const subject = parts.at(-1)?.compound;
109
+ if (!subject || /::part\(/i.test(subject)) continue;
110
+ const bare = withoutRelationalArguments(subject);
111
+ for (const className of classNames(bare))
112
+ add(facts.classOwners.get(className), 'class', `.${className}`);
113
+ for (const value of dataComponents(bare))
114
+ add(
115
+ facts.dataComponentOwners.get(value),
116
+ 'data-component',
117
+ `[data-component="${value}"]`,
118
+ );
119
+ // An application class on the subject scopes a Web Awesome tag to the
120
+ // application's own element; a component package never places one there.
121
+ if (classNames(bare).some((name) => !facts.classOwners.has(name))) continue;
122
+ for (const type of subjectTypes(subject) ?? [])
123
+ add(facts.tagOwners.get(type), 'tag', type);
124
+ }
125
+ return found;
126
+ }
127
+
128
+ /** The catalog entry whose private `--_<root>-*` variable `property` is. */
129
+ export function privateVariableOwner(property, facts) {
130
+ if (!property.startsWith('--_')) return undefined;
131
+ const owner = facts.privatePrefixes.find(
132
+ ({ prefix }) => property === prefix || property.startsWith(`${prefix}-`),
133
+ );
134
+ if (owner) return owner.entry;
135
+ return property.startsWith('--_kui-') ? null : undefined;
136
+ }
137
+
138
+ /** The typed prop that configures a component token, if one does. */
139
+ export function typedPropForToken(token, facts) {
140
+ return facts.typedTokens.get(token);
141
+ }
142
+
143
+ /** Every configuration seam an entry publishes, as prose. */
144
+ export function configurationFor(entry) {
145
+ if (!entry) return 'its documented props and variants';
146
+ if (isWebAwesomeTagEntry(entry))
147
+ return `<${entry.id}>'s documented attributes and ${entry.package}'s Web Awesome theme tokens`;
148
+ const props = (entry.cssValueProps ?? []).map(({ path }) => `\`${path}\``);
149
+ const rootClass = entry.boundaries?.rootClass;
150
+ const tokens = (entry.boundaries?.publicTokens ?? []).filter(
151
+ (token) => rootClass && token.startsWith(`--${rootClass}-`),
152
+ );
153
+ const seams = [
154
+ ...(props.length ? [`its props (${props.join(', ')})`] : []),
155
+ ...(tokens.length
156
+ ? [
157
+ `its public tokens (${tokens.map((item) => `\`${item}\``).join(', ')})`,
158
+ ]
159
+ : []),
160
+ ];
161
+ return seams.length
162
+ ? `${seams.join(' or ')}, or its documented variants`
163
+ : 'its documented props and variants';
164
+ }
165
+
166
+ /** The shared closing guidance: a missing seam is a component gap. */
167
+ export function reportGap(entry) {
168
+ const owner = entry?.package ?? '@kerfjs/ui';
169
+ return `If no configuration covers this need, report the component gap to ${owner} (open a feature request) instead of overriding it.`;
170
+ }
171
+
172
+ /** A component's short name: its export, or its Web Awesome tag. */
173
+ export function componentName(entry) {
174
+ if (!entry) return 'a Kerf component';
175
+ return isWebAwesomeTagEntry(entry) ? `<${entry.id}>` : entry.name;
176
+ }
177
+
178
+ /** A component's name with its catalog key. */
179
+ export function componentLabel(entry) {
180
+ if (!entry) return 'a Kerf component';
181
+ return isWebAwesomeTagEntry(entry)
182
+ ? `<${entry.id}>`
183
+ : `${entry.name} (${entry.key})`;
184
+ }
@@ -34,6 +34,9 @@ export const UI_ANALYSIS_RULES: Readonly<
34
34
  Record<`KUI-L${string}`, { severity: UiAnalysisSeverity; title: string }>
35
35
  >;
36
36
 
37
+ /** The doctor's repair action for rules whose fix is more specific than the default. */
38
+ export const UI_ANALYSIS_ACTIONS: Readonly<Record<`KUI-L${string}`, string>>;
39
+
37
40
  export function analyzeUiProject(options?: {
38
41
  root?: string;
39
42
  paths?: string[];
@@ -14,7 +14,22 @@ import ts from 'typescript';
14
14
 
15
15
  import { loadApplicationUiProfile } from '../ai/application-ui-profile.mjs';
16
16
  import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
17
+ import {
18
+ componentLabel,
19
+ componentName,
20
+ componentOwnershipFacts,
21
+ configurationFor,
22
+ privateVariableOwner,
23
+ reportGap,
24
+ restyledComponents,
25
+ typedPropForToken,
26
+ } from './component-ownership.mjs';
17
27
  import { collectLoudDeclarations, inspectLoudPairs } from './loud-pairs.mjs';
28
+ import {
29
+ classNames,
30
+ complexSelectorParts,
31
+ withoutRelationalArguments,
32
+ } from './selectors.mjs';
18
33
 
19
34
  export const UI_ANALYSIS_SCHEMA_VERSION = 1;
20
35
 
@@ -23,7 +38,10 @@ export const UI_ANALYSIS_RULES = Object.freeze({
23
38
  'KUI-L002': { severity: 'error', title: 'Unknown Kerf token' },
24
39
  'KUI-L003': { severity: 'error', title: 'Competing geometry owners' },
25
40
  'KUI-L004': { severity: 'review', title: 'Repeated content inset' },
26
- 'KUI-L005': { severity: 'review', title: 'Forced component dimension' },
41
+ // KUI-L005 ('Forced component dimension') is retired: KUI-L019 reports any
42
+ // restyle of a cataloged component, dimensions included, and L005's only
43
+ // other shape (a public class as an ancestor) sizes an application element.
44
+ // The id stays in the diagnostic registry so existing exceptions still load.
27
45
  'KUI-L006': {
28
46
  severity: 'review',
29
47
  title: 'Hard-coded spacing outside the approved scale',
@@ -46,6 +64,34 @@ export const UI_ANALYSIS_RULES = Object.freeze({
46
64
  severity: 'review',
47
65
  title: 'Loud fill override without its on-loud pair',
48
66
  },
67
+ 'KUI-L019': {
68
+ severity: 'error',
69
+ title: 'Application CSS restyles a cataloged component',
70
+ },
71
+ 'KUI-L020': {
72
+ severity: 'error',
73
+ title: "Another component's private variable",
74
+ },
75
+ 'KUI-L021': {
76
+ severity: 'error',
77
+ title: 'Component token overridden where a typed prop exists',
78
+ },
79
+ 'KUI-L022': {
80
+ severity: 'error',
81
+ title: "Hook class restyles a cataloged component's root",
82
+ },
83
+ });
84
+
85
+ const ownershipAction =
86
+ "Configure the component through its typed props, variants, or public tokens; style only your own elements (in the component's context when needed). If no configuration covers the need, report the component gap to its package instead of overriding it.";
87
+
88
+ /** The doctor's per-rule repair action, where it is more specific than the default. */
89
+ export const UI_ANALYSIS_ACTIONS = Object.freeze({
90
+ 'KUI-L019': ownershipAction,
91
+ 'KUI-L020': ownershipAction,
92
+ 'KUI-L021':
93
+ 'Set the typed prop the diagnostic names on the component instead of overriding its token.',
94
+ 'KUI-L022': ownershipAction,
49
95
  });
50
96
 
51
97
  const adoptionRules = new Set([
@@ -54,12 +100,14 @@ const adoptionRules = new Set([
54
100
  'KUI-L010',
55
101
  'KUI-L011',
56
102
  'KUI-L012',
103
+ 'KUI-L019',
104
+ 'KUI-L020',
105
+ 'KUI-L021',
106
+ 'KUI-L022',
57
107
  ]);
58
108
 
59
109
  const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
60
110
  const spacingProperties = /^(?:margin|padding|gap|inset)(?:-|$)/;
61
- const dimensionProperties =
62
- /^(?:width|height|min-width|max-width|min-height|max-height)$/;
63
111
  const approvedSpacing = new Set([0, 4, 8, 16, 24]);
64
112
  async function collectFiles(root, paths) {
65
113
  const files = [];
@@ -177,6 +225,7 @@ function reachableStyleFacts(file, imports, styleFacts) {
177
225
  const current = result.get(className) ?? { scroll: false, inset: false };
178
226
  current.scroll ||= facts.scroll;
179
227
  current.inset ||= facts.inset;
228
+ current.styled ??= facts.styled;
180
229
  result.set(className, current);
181
230
  }
182
231
  }
@@ -326,6 +375,7 @@ function catalogFacts(entries) {
326
375
  publicParts,
327
376
  classEntries,
328
377
  exportEntries,
378
+ ownership: componentOwnershipFacts(entries),
329
379
  };
330
380
  }
331
381
 
@@ -342,6 +392,121 @@ function escapeRegExp(value) {
342
392
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
343
393
  }
344
394
 
395
+ // A declaration that restyles, as opposed to one that only sets a custom
396
+ // property: setting a component's public token on its host is configuration,
397
+ // which the token rules (KUI-L002, KUI-L012, KUI-L020, KUI-L021) judge.
398
+ function restylingDeclarations(rule) {
399
+ return (rule.nodes ?? []).filter(
400
+ (node) => node.type === 'decl' && !node.prop.startsWith('--'),
401
+ );
402
+ }
403
+
404
+ function inKeyframes(rule) {
405
+ return rule.parent?.type === 'atrule' && /keyframes$/i.test(rule.parent.name);
406
+ }
407
+
408
+ // The classes a rule makes its subject (the rightmost compound of each complex
409
+ // selector, outside :has()/:not()).
410
+ function subjectClasses(selector) {
411
+ return complexSelectorParts(selector).flatMap((parts) => {
412
+ const subject = parts.at(-1)?.compound;
413
+ return subject ? classNames(withoutRelationalArguments(subject)) : [];
414
+ });
415
+ }
416
+
417
+ function inspectComponentOwnership(
418
+ file,
419
+ rule,
420
+ facts,
421
+ diagnostics,
422
+ adoption,
423
+ isForeign,
424
+ ) {
425
+ if (inKeyframes(rule)) return;
426
+ const restyling = restylingDeclarations(rule);
427
+ if (restyling.length === 0) return;
428
+ const properties = [...new Set(restyling.map((decl) => decl.prop))];
429
+ for (const { entry, via, name } of restyledComponents(
430
+ rule.selector,
431
+ facts.ownership,
432
+ isForeign,
433
+ ))
434
+ diagnostics.push(
435
+ diagnostic(
436
+ 'KUI-L019',
437
+ location(file, rule),
438
+ `\`${name}\` makes ${componentLabel(entry)} the subject of an application rule (${properties.join(', ')}); components own their styles. Configure it through ${configurationFor(entry)}. To place your own content in its context, style your own element (\`${name} > .your-element\`). ${reportGap(entry)}`,
439
+ {
440
+ selector: rule.selector,
441
+ component: entry.key,
442
+ via,
443
+ target: name,
444
+ properties,
445
+ },
446
+ undefined,
447
+ adoption,
448
+ ),
449
+ );
450
+ }
451
+
452
+ function inspectVariableOwnership(
453
+ file,
454
+ decl,
455
+ facts,
456
+ diagnostics,
457
+ adoption,
458
+ isForeign,
459
+ ) {
460
+ const at = location(file, decl);
461
+ const privates = new Set([
462
+ ...(decl.prop.match(/--_[a-z0-9_-]+/gi) ?? []),
463
+ ...(decl.value.match(/--_[a-z0-9_-]+/gi) ?? []),
464
+ ]);
465
+ for (const variable of privates) {
466
+ const owner = privateVariableOwner(variable, facts.ownership);
467
+ if (owner === undefined || (owner && !isForeign(owner))) continue;
468
+ const written = decl.prop === variable;
469
+ diagnostics.push(
470
+ diagnostic(
471
+ 'KUI-L020',
472
+ at,
473
+ `\`${variable}\` is ${owner ? `${componentName(owner)}'s` : "a Kerf component's"} private variable; ${written ? 'writing it overrides' : 'reading it couples to'} that component's implementation. Configure it through ${configurationFor(owner)}. ${reportGap(owner)}`,
474
+ {
475
+ property: decl.prop,
476
+ value: decl.value,
477
+ variable,
478
+ ...(owner ? { component: owner.key } : {}),
479
+ access: written ? 'write' : 'read',
480
+ },
481
+ undefined,
482
+ adoption,
483
+ ),
484
+ );
485
+ }
486
+ if (!decl.prop.startsWith('--')) return;
487
+ const typed = typedPropForToken(decl.prop, facts.ownership);
488
+ if (!typed || !isForeign(typed.entry)) return;
489
+ const contract = (typed.entry.cssValueProps ?? []).find(
490
+ ({ path }) => path === typed.path,
491
+ );
492
+ const example = contract?.examples?.[0];
493
+ diagnostics.push(
494
+ diagnostic(
495
+ 'KUI-L021',
496
+ at,
497
+ `\`${decl.prop}\` is what ${typed.entry.name}'s typed \`${typed.path}\` prop sets; set the prop on ${typed.entry.name}${example ? ` (\`<${typed.entry.name} ${example} />\`)` : ''} instead of overriding the token. ${reportGap(typed.entry)}`,
498
+ {
499
+ property: decl.prop,
500
+ value: decl.value,
501
+ component: typed.entry.key,
502
+ prop: typed.path,
503
+ },
504
+ undefined,
505
+ adoption,
506
+ ),
507
+ );
508
+ }
509
+
345
510
  async function inspectCss(
346
511
  file,
347
512
  source,
@@ -350,6 +515,7 @@ async function inspectCss(
350
515
  cssFacts,
351
516
  adoption = false,
352
517
  siblingOnLoud = [],
518
+ isForeign = () => true,
353
519
  ) {
354
520
  let root;
355
521
  try {
@@ -374,6 +540,27 @@ async function inspectCss(
374
540
  siblingOnLoud,
375
541
  );
376
542
  root.walkRules((rule) => {
543
+ inspectComponentOwnership(
544
+ file,
545
+ rule,
546
+ facts,
547
+ diagnostics,
548
+ adoption,
549
+ isForeign,
550
+ );
551
+ if (!inKeyframes(rule) && restylingDeclarations(rule).length > 0)
552
+ for (const className of subjectClasses(rule.selector)) {
553
+ const record = cssFacts.get(className) ?? {
554
+ scroll: false,
555
+ inset: false,
556
+ };
557
+ record.styled ??= {
558
+ stylesheet: file,
559
+ line: rule.source?.start?.line ?? 1,
560
+ selector: rule.selector,
561
+ };
562
+ cssFacts.set(className, record);
563
+ }
377
564
  const classes = [
378
565
  ...rule.selector.matchAll(/\.([_a-zA-Z]+[_a-zA-Z0-9-]*)/g),
379
566
  ].map((match) => match[1]);
@@ -444,6 +631,14 @@ async function inspectCss(
444
631
  );
445
632
  }
446
633
  rule.walkDecls((decl) => {
634
+ inspectVariableOwnership(
635
+ file,
636
+ decl,
637
+ facts,
638
+ diagnostics,
639
+ adoption,
640
+ isForeign,
641
+ );
447
642
  const at = location(file, decl);
448
643
  const tokens = [
449
644
  ...(decl.prop.match(/--kui-[a-z0-9-]+/g) ?? []),
@@ -496,22 +691,6 @@ async function inspectCss(
496
691
  if (/^padding(?:-|$)/.test(decl.prop) && decl.value !== '0')
497
692
  record.inset = true;
498
693
  cssFacts.set(className, record);
499
- if (
500
- facts.publicClasses.has(className) &&
501
- dimensionProperties.test(decl.prop)
502
- )
503
- diagnostics.push(
504
- diagnostic(
505
- 'KUI-L005',
506
- at,
507
- `.${className} forces ${decl.prop}; prefer the component's public sizing contract.`,
508
- {
509
- selector: rule.selector,
510
- property: decl.prop,
511
- value: decl.value,
512
- },
513
- ),
514
- );
515
694
  }
516
695
  });
517
696
  });
@@ -696,7 +875,15 @@ function inspectCssValues(
696
875
  }
697
876
  }
698
877
 
699
- function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
878
+ function inspectTsx(
879
+ file,
880
+ sourceText,
881
+ facts,
882
+ cssFacts,
883
+ diagnostics,
884
+ adoption = false,
885
+ isForeign = () => true,
886
+ ) {
700
887
  const source = ts.createSourceFile(
701
888
  file,
702
889
  sourceText,
@@ -769,6 +956,29 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
769
956
  namespaces,
770
957
  diagnostics,
771
958
  );
959
+ if (entry && isForeign(entry))
960
+ for (const className of classes.values) {
961
+ if (facts.ownership.classOwners.has(className)) continue;
962
+ const styled = cssFacts.get(className)?.styled;
963
+ if (!styled) continue;
964
+ diagnostics.push(
965
+ diagnostic(
966
+ 'KUI-L022',
967
+ at,
968
+ `\`${className}\` is placed on ${entry.name}'s root and styled by ${basename(styled.stylesheet)}:${styled.line} (\`${styled.selector}\`), which restyles ${componentLabel(entry)}; components own their styles. Configure it through ${configurationFor(entry)}, or wrap it in your own element and style that. ${reportGap(entry)}`,
969
+ {
970
+ tag,
971
+ component: entry.key,
972
+ className,
973
+ stylesheet: styled.stylesheet,
974
+ line: styled.line,
975
+ selector: styled.selector,
976
+ },
977
+ undefined,
978
+ adoption,
979
+ ),
980
+ );
981
+ }
772
982
  if (classes.dynamic)
773
983
  diagnostics.push(
774
984
  diagnostic(
@@ -932,6 +1142,39 @@ export async function analyzeUiProject({
932
1142
  const imports = new Map(
933
1143
  files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
934
1144
  );
1145
+ // A stylesheet or source file belongs to the package whose manifest is
1146
+ // nearest it; entries of every other catalog package are foreign to it.
1147
+ const packageNames = new Map();
1148
+ const packageNameOf = async (directory) => {
1149
+ if (!packageNames.has(directory))
1150
+ packageNames.set(
1151
+ directory,
1152
+ (async () => {
1153
+ try {
1154
+ const manifest = JSON.parse(
1155
+ await readFile(resolve(directory, 'package.json'), 'utf8'),
1156
+ );
1157
+ if (typeof manifest.name === 'string') return manifest.name;
1158
+ } catch {
1159
+ // No readable manifest here; keep walking up.
1160
+ }
1161
+ const parent = dirname(directory);
1162
+ const inside = relative(root, parent);
1163
+ if (
1164
+ parent === directory ||
1165
+ inside === '..' ||
1166
+ inside.startsWith('../')
1167
+ )
1168
+ return undefined;
1169
+ return packageNameOf(parent);
1170
+ })(),
1171
+ );
1172
+ return packageNames.get(directory);
1173
+ };
1174
+ const foreignTo = async (file) => {
1175
+ const own = await packageNameOf(dirname(file));
1176
+ return (entry) => entry.package !== own;
1177
+ };
935
1178
  const styleFacts = new Map();
936
1179
  const recordDiagnostics = (items, context) => {
937
1180
  for (const item of items) {
@@ -1022,6 +1265,8 @@ export async function analyzeUiProject({
1022
1265
  context.facts,
1023
1266
  reachableStyleFacts(file, imports, styleFacts),
1024
1267
  fileDiagnostics,
1268
+ adoption,
1269
+ await foreignTo(file),
1025
1270
  );
1026
1271
  recordDiagnostics(fileDiagnostics, context);
1027
1272
  }
@@ -1039,6 +1284,7 @@ export async function analyzeUiProject({
1039
1284
  new Map(),
1040
1285
  adoption,
1041
1286
  siblingOnLoud(file),
1287
+ await foreignTo(file),
1042
1288
  );
1043
1289
  recordDiagnostics(fileDiagnostics, context);
1044
1290
  }
@@ -1121,6 +1367,14 @@ export async function analyzeUiProject({
1121
1367
  ...item.location,
1122
1368
  file: portablePath(item.location.file),
1123
1369
  },
1370
+ ...(item.evidence?.stylesheet
1371
+ ? {
1372
+ evidence: {
1373
+ ...item.evidence,
1374
+ stylesheet: portablePath(item.evidence.stylesheet),
1375
+ },
1376
+ }
1377
+ : {}),
1124
1378
  })),
1125
1379
  summary: {
1126
1380
  errors:
@@ -0,0 +1,21 @@
1
+ export interface SelectorCompound {
2
+ combinator: '' | ' ' | '>' | '+' | '~';
3
+ compound: string;
4
+ }
5
+
6
+ export function ownsClass(rootClass: string, candidate: string): boolean;
7
+ export function complexSelectorParts(
8
+ selectorList: string,
9
+ ): SelectorCompound[][];
10
+ export function complexSelectors(selectorList: string): string[][];
11
+ export function kuiClasses(selector: string): string[];
12
+ export function classNames(selector: string): string[];
13
+ export function dataComponents(selector: string): string[];
14
+ export function withoutPseudoArguments(
15
+ compound: string,
16
+ names: string[],
17
+ ): string;
18
+ export function withoutRelationalArguments(compound: string): string;
19
+ export function pseudoArguments(compound: string, name: string): string[];
20
+ export function splitTopLevel(list: string): string[];
21
+ export function subjectTypes(compound: string): string[] | null;