@treelocator/runtime 0.4.7 → 0.6.0

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 (166) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/dejitter/recorder.d.ts +7 -1
  41. package/dist/dejitter/recorder.js +64 -1
  42. package/dist/functions/cssRuleInspector.d.ts +83 -0
  43. package/dist/functions/cssRuleInspector.js +608 -0
  44. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  45. package/dist/functions/cssRuleInspector.test.js +439 -0
  46. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  47. package/dist/functions/deduplicateLabels.test.js +178 -0
  48. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  49. package/dist/functions/extractComputedStyles.d.ts +51 -0
  50. package/dist/functions/extractComputedStyles.js +447 -0
  51. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  52. package/dist/functions/extractComputedStyles.test.js +549 -0
  53. package/dist/functions/formatAncestryChain.d.ts +8 -0
  54. package/dist/functions/formatAncestryChain.js +21 -1
  55. package/dist/functions/formatAncestryChain.test.js +18 -0
  56. package/dist/functions/getUsableName.test.d.ts +1 -0
  57. package/dist/functions/getUsableName.test.js +219 -0
  58. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  59. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  60. package/dist/functions/mergeRects.test.js +210 -1
  61. package/dist/functions/namedSnapshots.d.ts +52 -0
  62. package/dist/functions/namedSnapshots.js +161 -0
  63. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  64. package/dist/functions/namedSnapshots.test.js +85 -0
  65. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  66. package/dist/functions/normalizeFilePath.test.js +66 -0
  67. package/dist/functions/parseDataId.test.d.ts +1 -0
  68. package/dist/functions/parseDataId.test.js +101 -0
  69. package/dist/hooks/getStorage.d.ts +3 -0
  70. package/dist/hooks/getStorage.js +17 -0
  71. package/dist/hooks/useEventListeners.d.ts +15 -0
  72. package/dist/hooks/useEventListeners.js +56 -0
  73. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  74. package/dist/hooks/useLocatorStorage.js +41 -0
  75. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  76. package/dist/hooks/useLocatorStorage.test.js +124 -0
  77. package/dist/hooks/useRecordingState.d.ts +43 -0
  78. package/dist/hooks/useRecordingState.js +387 -0
  79. package/dist/hooks/useSettings.d.ts +13 -0
  80. package/dist/hooks/useSettings.js +66 -0
  81. package/dist/index.d.ts +5 -2
  82. package/dist/index.js +4 -2
  83. package/dist/initRuntime.d.ts +3 -1
  84. package/dist/initRuntime.js +4 -1
  85. package/dist/mcpBridge.d.ts +61 -0
  86. package/dist/mcpBridge.js +534 -0
  87. package/dist/mcpBridge.test.d.ts +1 -0
  88. package/dist/mcpBridge.test.js +248 -0
  89. package/dist/output.css +20 -0
  90. package/dist/visualDiff/diff.d.ts +9 -0
  91. package/dist/visualDiff/diff.js +209 -0
  92. package/dist/visualDiff/diff.test.d.ts +1 -0
  93. package/dist/visualDiff/diff.test.js +253 -0
  94. package/dist/visualDiff/settle.d.ts +3 -0
  95. package/dist/visualDiff/settle.js +50 -0
  96. package/dist/visualDiff/settle.test.d.ts +1 -0
  97. package/dist/visualDiff/settle.test.js +65 -0
  98. package/dist/visualDiff/snapshot.d.ts +4 -0
  99. package/dist/visualDiff/snapshot.js +84 -0
  100. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  101. package/dist/visualDiff/snapshot.test.js +245 -0
  102. package/dist/visualDiff/types.d.ts +37 -0
  103. package/dist/visualDiff/types.js +1 -0
  104. package/package.json +2 -2
  105. package/scripts/wrapCSS.js +1 -1
  106. package/scripts/wrapImage.js +1 -1
  107. package/src/_generated_styles.ts +21 -1
  108. package/src/_generated_tree_icon.ts +1 -1
  109. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  110. package/src/adapters/createTreeNode.ts +12 -51
  111. package/src/adapters/detectFramework.test.ts +73 -0
  112. package/src/adapters/detectFramework.ts +28 -0
  113. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  114. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  115. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  116. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  117. package/src/adapters/react/findDebugSource.ts +5 -6
  118. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  119. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  120. package/src/adapters/react/reactAdapter.ts +1 -2
  121. package/src/adapters/resolveAdapter.ts +4 -14
  122. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  123. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  124. package/src/browserApi.test.ts +329 -0
  125. package/src/browserApi.ts +351 -4
  126. package/src/components/RecordingPillButton.tsx +301 -0
  127. package/src/components/RecordingResults.tsx +114 -13
  128. package/src/components/Runtime.tsx +176 -621
  129. package/src/components/SettingsPanel.tsx +339 -0
  130. package/src/consoleCapture.ts +113 -0
  131. package/src/dejitter/recorder.ts +67 -3
  132. package/src/functions/cssRuleInspector.test.ts +517 -0
  133. package/src/functions/cssRuleInspector.ts +708 -0
  134. package/src/functions/deduplicateLabels.test.ts +115 -0
  135. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  136. package/src/functions/extractComputedStyles.test.ts +681 -0
  137. package/src/functions/extractComputedStyles.ts +768 -0
  138. package/src/functions/formatAncestryChain.test.ts +23 -1
  139. package/src/functions/formatAncestryChain.ts +22 -1
  140. package/src/functions/getUsableName.test.ts +242 -0
  141. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  142. package/src/functions/mergeRects.test.ts +111 -1
  143. package/src/functions/namedSnapshots.test.ts +106 -0
  144. package/src/functions/namedSnapshots.ts +232 -0
  145. package/src/functions/normalizeFilePath.test.ts +80 -0
  146. package/src/functions/parseDataId.test.ts +125 -0
  147. package/src/hooks/getStorage.ts +26 -0
  148. package/src/hooks/useEventListeners.ts +97 -0
  149. package/src/hooks/useLocatorStorage.test.ts +127 -0
  150. package/src/hooks/useLocatorStorage.ts +60 -0
  151. package/src/hooks/useRecordingState.ts +516 -0
  152. package/src/hooks/useSettings.ts +83 -0
  153. package/src/index.ts +10 -5
  154. package/src/initRuntime.ts +5 -0
  155. package/src/mcpBridge.test.ts +260 -0
  156. package/src/mcpBridge.ts +677 -0
  157. package/src/visualDiff/diff.test.ts +167 -0
  158. package/src/visualDiff/diff.ts +242 -0
  159. package/src/visualDiff/settle.test.ts +77 -0
  160. package/src/visualDiff/settle.ts +62 -0
  161. package/src/visualDiff/snapshot.test.ts +200 -0
  162. package/src/visualDiff/snapshot.ts +119 -0
  163. package/src/visualDiff/types.ts +40 -0
  164. package/tsconfig.json +3 -1
  165. package/vitest.config.ts +18 -0
  166. package/jest.config.ts +0 -195
@@ -0,0 +1,608 @@
1
+ /**
2
+ * CSS Rule Inspector
3
+ *
4
+ * Walks all loaded stylesheets, finds every rule that matches an element,
5
+ * calculates specificity, and determines which rule "wins" for each property.
6
+ * Designed to produce AI-friendly output for debugging CSS issues.
7
+ */
8
+
9
+ // ── Types ──────────────────────────────────────────────────────────────
10
+
11
+ // [ids, classes, elements]
12
+
13
+ // ── Specificity ────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Calculate CSS specificity for a selector string.
17
+ *
18
+ * Returns [ids, classes, elements] where:
19
+ * - ids: count of #id selectors
20
+ * - classes: count of .class, [attr], :pseudo-class (except :not, :is, :where, :has)
21
+ * - elements: count of element, ::pseudo-element
22
+ *
23
+ * This is a lightweight parser that handles the vast majority of real-world
24
+ * selectors without pulling in a full CSS parser.
25
+ */
26
+ export function calculateSpecificity(selector) {
27
+ // Split compound selectors (comma-separated) — return max specificity.
28
+ // splitOnCommas is depth-aware (parens, brackets, strings).
29
+ const parts = splitOnCommas(selector);
30
+ if (parts.length > 1) {
31
+ let max = [0, 0, 0];
32
+ for (const part of parts) {
33
+ const s = calculateSpecificity(part.trim());
34
+ if (compareSpecificity(s, max) > 0) {
35
+ max = s;
36
+ }
37
+ }
38
+ return max;
39
+ }
40
+ let ids = 0;
41
+ let classes = 0;
42
+ let elements = 0;
43
+ let s = selector.trim();
44
+
45
+ // Normalize legacy single-colon pseudo-elements to double-colon so they
46
+ // are correctly counted as element-level specificity (0,0,1) instead of
47
+ // being mis-classified as pseudo-classes (0,1,0).
48
+ s = s.replace(/:(before|after|first-line|first-letter)\b/gi, "::$1");
49
+
50
+ // Extract :not(), :is(), :has(), :where() — depth-aware so nested pseudos work.
51
+ // Process in reverse so removing each one doesn't shift earlier offsets.
52
+ const pseudos = extractFunctionalPseudos(s);
53
+ for (let i = pseudos.length - 1; i >= 0; i--) {
54
+ const p = pseudos[i];
55
+ if (p.name !== "where") {
56
+ // :not(), :is(), :has() take the specificity of their argument.
57
+ const inner = calculateSpecificity(p.arg);
58
+ ids += inner[0];
59
+ classes += inner[1];
60
+ elements += inner[2];
61
+ }
62
+ // Remove this pseudo from the working selector — :where() contributes zero.
63
+ s = s.slice(0, p.start) + s.slice(p.end);
64
+ }
65
+
66
+ // Replace string literals (attribute values) with placeholders so chars
67
+ // inside them (like commas, brackets) don't get counted.
68
+ s = stripStrings(s);
69
+
70
+ // Replace bracketed attribute selectors with a single placeholder so the
71
+ // contents (e.g. `[data-foo="a.b#c"]`) don't get counted as classes/IDs.
72
+ // Each [..] block contributes one to the class column.
73
+ let attrCount = 0;
74
+ s = s.replace(/\[[^\]]*\]/g, () => {
75
+ attrCount++;
76
+ return " ";
77
+ });
78
+ classes += attrCount;
79
+
80
+ // Count IDs: #foo
81
+ const idMatches = s.match(/#[a-zA-Z_-][\w-]*/g);
82
+ if (idMatches) ids += idMatches.length;
83
+
84
+ // Count pseudo-elements: ::before, ::after, etc.
85
+ const pseudoElementMatches = s.match(/::[a-zA-Z_-][\w-]*/g);
86
+ if (pseudoElementMatches) elements += pseudoElementMatches.length;
87
+
88
+ // Remove pseudo-elements so they don't interfere
89
+ s = s.replace(/::[a-zA-Z_-][\w-]*/g, " ");
90
+
91
+ // Remove IDs so they don't interfere with class counting
92
+ s = s.replace(/#[a-zA-Z_-][\w-]*/g, " ");
93
+
94
+ // Count classes: .foo
95
+ const classMatches = s.match(/\.[a-zA-Z_-][\w-]*/g);
96
+ if (classMatches) classes += classMatches.length;
97
+
98
+ // Count pseudo-classes: :hover, :first-child, etc.
99
+ const pseudoClassMatches = s.match(/:[a-zA-Z_-][\w-]*/g);
100
+ if (pseudoClassMatches) classes += pseudoClassMatches.length;
101
+
102
+ // Remove everything we've counted to isolate element selectors
103
+ s = s.replace(/\.[a-zA-Z_-][\w-]*/g, " ");
104
+ s = s.replace(/:[a-zA-Z_-][\w-]*/g, " ");
105
+
106
+ // Count element selectors: div, span, etc. (not combinators or *)
107
+ const elementMatches = s.match(/(?:^|[\s>+~])([a-zA-Z][\w-]*)/g);
108
+ if (elementMatches) elements += elementMatches.length;
109
+ return [ids, classes, elements];
110
+ }
111
+
112
+ /**
113
+ * Find all `:not(...)`, `:is(...)`, `:has(...)`, `:where(...)` segments in a
114
+ * selector with depth-aware parenthesis matching. Handles nested cases like
115
+ * `:not(:is(.a, .b))` and string literals containing parens.
116
+ */
117
+ function extractFunctionalPseudos(selector) {
118
+ const result = [];
119
+ const re = /:(not|is|has|where)\(/gi;
120
+ let match;
121
+ while ((match = re.exec(selector)) !== null) {
122
+ const name = match[1].toLowerCase();
123
+ const start = match.index;
124
+ const argStart = match.index + match[0].length;
125
+
126
+ // Walk forward from argStart, tracking depth and string state, to find
127
+ // the matching close paren.
128
+ let depth = 1;
129
+ let i = argStart;
130
+ let inString = null;
131
+ while (i < selector.length && depth > 0) {
132
+ const ch = selector[i];
133
+ if (inString) {
134
+ if (ch === "\\") {
135
+ i += 2;
136
+ continue;
137
+ }
138
+ if (ch === inString) inString = null;
139
+ } else if (ch === '"' || ch === "'") {
140
+ inString = ch;
141
+ } else if (ch === "(") {
142
+ depth++;
143
+ } else if (ch === ")") {
144
+ depth--;
145
+ if (depth === 0) break;
146
+ }
147
+ i++;
148
+ }
149
+ if (depth !== 0) {
150
+ // Unmatched paren — bail out and return what we have
151
+ break;
152
+ }
153
+ const arg = selector.slice(argStart, i);
154
+ result.push({
155
+ name,
156
+ arg,
157
+ start,
158
+ end: i + 1
159
+ });
160
+ // Skip past the close paren so the regex doesn't recurse into nested pseudos
161
+ re.lastIndex = i + 1;
162
+ }
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * Replace string literal contents with spaces to neutralize embedded
168
+ * special characters (commas, brackets, etc.) without changing offsets.
169
+ */
170
+ function stripStrings(s) {
171
+ let out = "";
172
+ let inString = null;
173
+ for (let i = 0; i < s.length; i++) {
174
+ const ch = s[i];
175
+ if (inString) {
176
+ if (ch === "\\" && i + 1 < s.length) {
177
+ out += " ";
178
+ i++;
179
+ continue;
180
+ }
181
+ if (ch === inString) {
182
+ inString = null;
183
+ out += ch;
184
+ continue;
185
+ }
186
+ out += " ";
187
+ } else if (ch === '"' || ch === "'") {
188
+ inString = ch;
189
+ out += ch;
190
+ } else {
191
+ out += ch;
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+
197
+ /**
198
+ * Compare two specificity tuples.
199
+ * Returns positive if a > b, negative if a < b, 0 if equal.
200
+ */
201
+ export function compareSpecificity(a, b) {
202
+ if (a[0] !== b[0]) return a[0] - b[0];
203
+ if (a[1] !== b[1]) return a[1] - b[1];
204
+ return a[2] - b[2];
205
+ }
206
+
207
+ /**
208
+ * Format specificity tuple as a human-readable string.
209
+ */
210
+ export function formatSpecificity(s) {
211
+ return `(${s[0]},${s[1]},${s[2]})`;
212
+ }
213
+
214
+ // ── Rule collection ────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Describe the source of a stylesheet for display.
218
+ */
219
+ function describeSource(sheet) {
220
+ if (sheet.href) {
221
+ // For remote stylesheets, show just the filename
222
+ try {
223
+ const url = new URL(sheet.href);
224
+ return url.pathname.split("/").pop() || sheet.href;
225
+ } catch {
226
+ return sheet.href;
227
+ }
228
+ }
229
+ if (sheet.ownerNode instanceof HTMLStyleElement) {
230
+ const id = sheet.ownerNode.id;
231
+ if (id) return `<style#${id}>`;
232
+ // Check for data attributes that might hint at CSS-in-JS
233
+ const dataset = sheet.ownerNode.dataset;
234
+ if (dataset.emotion) return `<style emotion="${dataset.emotion}">`;
235
+ if (dataset.styledComponents) return `<style styled-components>`;
236
+ return "<style>";
237
+ }
238
+ return "unknown";
239
+ }
240
+
241
+ /**
242
+ * Describe an element for display: tagName.class1.class2#id
243
+ */
244
+ export function describeElement(element) {
245
+ let desc = element.tagName.toLowerCase();
246
+ if (element.id) {
247
+ desc += `#${element.id}`;
248
+ }
249
+ if (element.classList.length > 0) {
250
+ desc += "." + Array.from(element.classList).join(".");
251
+ }
252
+ return desc;
253
+ }
254
+
255
+ /**
256
+ * Compute the specificity of the branch(es) of a selector list that actually
257
+ * match the element. `selectorText` may be comma-separated like
258
+ * `.btn, #app` — `element.matches()` succeeds if any branch matches, but
259
+ * specificity must come from the matching branch (not the max of all of them).
260
+ */
261
+ function specificityForMatchingBranches(selectorText, element) {
262
+ const branches = splitOnCommas(selectorText);
263
+ let best = null;
264
+ for (const branch of branches) {
265
+ const trimmed = branch.trim();
266
+ if (!trimmed) continue;
267
+ try {
268
+ if (element.matches(trimmed)) {
269
+ const spec = calculateSpecificity(trimmed);
270
+ if (!best || compareSpecificity(spec, best) > 0) {
271
+ best = spec;
272
+ }
273
+ }
274
+ } catch {
275
+ // Skip branches that error in matches() (rare invalid syntax)
276
+ }
277
+ }
278
+
279
+ // Fallback: should be unreachable since the caller already verified
280
+ // element.matches(selectorText), but stay safe.
281
+ return best ?? calculateSpecificity(selectorText);
282
+ }
283
+
284
+ /**
285
+ * Walk a list of CSS rules (handles @media, @supports, @layer nesting).
286
+ * Mutates `results` and `nextIndex` (a counter object) so that every rule
287
+ * gets a monotonically increasing source-order index.
288
+ */
289
+ function walkRules(rules, element, source, results, nextIndex) {
290
+ // Feature-detect the rule constructors so we don't crash in environments
291
+ // (older jsdom, some embedded browsers) where they're undefined.
292
+ const SupportsRuleCtor = typeof CSSSupportsRule !== "undefined" ? CSSSupportsRule : null;
293
+ const MediaRuleCtor = typeof CSSMediaRule !== "undefined" ? CSSMediaRule : null;
294
+ const StyleRuleCtor = typeof CSSStyleRule !== "undefined" ? CSSStyleRule : null;
295
+ const ImportRuleCtor = typeof CSSImportRule !== "undefined" ? CSSImportRule : null;
296
+ for (let i = 0; i < rules.length; i++) {
297
+ const rule = rules[i];
298
+ if (StyleRuleCtor && rule instanceof StyleRuleCtor) {
299
+ // Check if the element matches this selector
300
+ try {
301
+ if (element.matches(rule.selectorText)) {
302
+ // Use the specificity of the *matching branch*, not the whole list.
303
+ const specificity = specificityForMatchingBranches(rule.selectorText, element);
304
+ const style = rule.style;
305
+ for (let j = 0; j < style.length; j++) {
306
+ const property = style[j];
307
+ const value = style.getPropertyValue(property);
308
+ const important = style.getPropertyPriority(property) === "important";
309
+ results.push({
310
+ selector: rule.selectorText,
311
+ property,
312
+ value,
313
+ specificity,
314
+ important,
315
+ source,
316
+ sourceIndex: nextIndex.value++
317
+ });
318
+ }
319
+ }
320
+ } catch {
321
+ // Invalid selector — skip
322
+ }
323
+ } else if (MediaRuleCtor && rule instanceof MediaRuleCtor) {
324
+ // Only recurse if the media query is currently active.
325
+ const mq = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(rule.conditionText) : null;
326
+ if (!mq || mq.matches) {
327
+ walkRules(rule.cssRules, element, source, results, nextIndex);
328
+ }
329
+ } else if (SupportsRuleCtor && rule instanceof SupportsRuleCtor) {
330
+ // The browser only lists @supports rules whose condition is met.
331
+ walkRules(rule.cssRules, element, source, results, nextIndex);
332
+ } else if (ImportRuleCtor && rule instanceof ImportRuleCtor) {
333
+ // Recurse into @import stylesheets
334
+ try {
335
+ if (rule.styleSheet) {
336
+ const importSource = describeSource(rule.styleSheet);
337
+ walkRules(rule.styleSheet.cssRules, element, importSource, results, nextIndex);
338
+ }
339
+ } catch {
340
+ // Cross-origin stylesheet or failed to load — skip
341
+ }
342
+ } else if ("cssRules" in rule && rule.cssRules) {
343
+ // Handle @layer and other grouping rules
344
+ walkRules(rule.cssRules, element, source, results, nextIndex);
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Collect all CSS rules from all stylesheets that match the given element.
351
+ * The shared `nextIndex` counter assigns each rule a monotonic source-order
352
+ * index used as the cascade tie-breaker.
353
+ */
354
+ function collectMatchingRules(element, nextIndex) {
355
+ const rules = [];
356
+ const unreachableSheets = [];
357
+ for (let i = 0; i < document.styleSheets.length; i++) {
358
+ const sheet = document.styleSheets[i];
359
+ const source = describeSource(sheet);
360
+ try {
361
+ const cssRules = sheet.cssRules;
362
+ walkRules(cssRules, element, source, rules, nextIndex);
363
+ } catch {
364
+ // Cross-origin stylesheet — can't read its rules
365
+ unreachableSheets.push(source);
366
+ }
367
+ }
368
+ return {
369
+ rules,
370
+ unreachableSheets
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Collect inline styles from an element.
376
+ */
377
+ function collectInlineStyles(element, nextIndex) {
378
+ if (!(element instanceof HTMLElement)) return [];
379
+ const style = element.style;
380
+ const rules = [];
381
+ for (let i = 0; i < style.length; i++) {
382
+ const property = style[i];
383
+ const value = style.getPropertyValue(property);
384
+ const important = style.getPropertyPriority(property) === "important";
385
+ rules.push({
386
+ selector: "element.style",
387
+ property,
388
+ value,
389
+ specificity: [1, 0, 0],
390
+ // inline = highest layer (treated as 1,0,0 for sorting)
391
+ important,
392
+ source: "inline",
393
+ sourceIndex: nextIndex.value++
394
+ });
395
+ }
396
+ return rules;
397
+ }
398
+
399
+ // ── Main API ───────────────────────────────────────────────────────────
400
+
401
+ /**
402
+ * Sort function for CSS rules: determines cascade winner. Sorts winners FIRST
403
+ * (descending priority), so callers can take `sorted[0]` as the winning rule.
404
+ *
405
+ * Cascade order (highest priority first):
406
+ * 1. !important beats non-important
407
+ * 2. Inline style beats stylesheet rules (for same importance)
408
+ * 3. Higher specificity beats lower
409
+ * 4. Later source order beats earlier
410
+ */
411
+ function cascadeCompare(a, b) {
412
+ // !important always wins
413
+ if (a.important !== b.important) {
414
+ return a.important ? -1 : 1;
415
+ }
416
+ // Inline styles win over stylesheet rules (unless !important reverses it)
417
+ if (a.source === "inline" !== (b.source === "inline")) {
418
+ return a.source === "inline" ? -1 : 1;
419
+ }
420
+ // Higher specificity wins
421
+ const specCmp = compareSpecificity(a.specificity, b.specificity);
422
+ if (specCmp !== 0) return -specCmp; // negative because we want descending
423
+ // Tie-breaker: later source order wins (higher index sorts first).
424
+ return b.sourceIndex - a.sourceIndex;
425
+ }
426
+
427
+ /**
428
+ * Inspect an element and return all CSS rules grouped by property,
429
+ * showing which rule wins for each property.
430
+ */
431
+ export function inspectCSSRules(element) {
432
+ const nextIndex = {
433
+ value: 0
434
+ };
435
+ const {
436
+ rules: sheetRules,
437
+ unreachableSheets
438
+ } = collectMatchingRules(element, nextIndex);
439
+ const inlineRules = collectInlineStyles(element, nextIndex);
440
+ const allRules = [...sheetRules, ...inlineRules];
441
+
442
+ // Hoist getComputedStyle out of the per-property loop. May be null if the
443
+ // element isn't in the document or in environments without a window.
444
+ let computed = null;
445
+ try {
446
+ if (typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
447
+ computed = window.getComputedStyle(element);
448
+ }
449
+ } catch {
450
+ computed = null;
451
+ }
452
+
453
+ // Group by property
454
+ const byProperty = new Map();
455
+ for (const rule of allRules) {
456
+ const existing = byProperty.get(rule.property);
457
+ if (existing) {
458
+ existing.push(rule);
459
+ } else {
460
+ byProperty.set(rule.property, [rule]);
461
+ }
462
+ }
463
+
464
+ // For each property, sort by cascade priority and mark the winner
465
+ const properties = [];
466
+ for (const [property, rules] of byProperty) {
467
+ // Sort: winner first
468
+ const sorted = [...rules].sort(cascadeCompare);
469
+
470
+ // Prefer the browser's computed value; fall back to the winning rule's value.
471
+ let computedValue = sorted[0]?.value || "";
472
+ const cv = computed?.getPropertyValue(property);
473
+ if (cv) computedValue = cv;
474
+ properties.push({
475
+ property,
476
+ value: computedValue,
477
+ rules: sorted.map((rule, idx) => ({
478
+ selector: rule.selector,
479
+ value: rule.value,
480
+ specificity: rule.specificity,
481
+ important: rule.important,
482
+ source: rule.source,
483
+ winning: idx === 0
484
+ }))
485
+ });
486
+ }
487
+
488
+ // Sort properties alphabetically for stable output
489
+ properties.sort((a, b) => a.property.localeCompare(b.property));
490
+ return {
491
+ element: describeElement(element),
492
+ properties,
493
+ unreachableSheets
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Format CSS inspection result as a human-readable string for AI consumption.
499
+ *
500
+ * Example output:
501
+ *
502
+ * CSS Rules for button.primary#submit
503
+ * ════════════════════════════════════
504
+ *
505
+ * color: #333
506
+ * ✓ .button.primary (0,2,0) — components.css
507
+ * ✗ .button (0,1,0) — base.css
508
+ * ✗ button (0,0,1) — reset.css
509
+ *
510
+ * font-size: 14px
511
+ * ✓ element.style (inline) — inline
512
+ * ✗ .button (0,1,0) — base.css
513
+ */
514
+ export function formatCSSInspection(result) {
515
+ const lines = [];
516
+ const header = `CSS Rules for ${result.element}`;
517
+ lines.push(header);
518
+ lines.push("═".repeat(header.length));
519
+ if (result.unreachableSheets.length > 0) {
520
+ lines.push("");
521
+ lines.push(`⚠ Cross-origin stylesheets (unreadable): ${result.unreachableSheets.join(", ")}`);
522
+ }
523
+ if (result.properties.length === 0) {
524
+ lines.push("");
525
+ lines.push("No matching CSS rules found.");
526
+ return lines.join("\n");
527
+ }
528
+ for (const prop of result.properties) {
529
+ lines.push("");
530
+ lines.push(`${prop.property}: ${prop.value}`);
531
+ for (const rule of prop.rules) {
532
+ const mark = rule.winning ? "✓" : "✗";
533
+ const imp = rule.important ? " !important" : "";
534
+ const spec = rule.source === "inline" ? "(inline)" : formatSpecificity(rule.specificity);
535
+ lines.push(` ${mark} ${rule.selector} ${spec}${imp} — ${rule.source} ${!rule.winning ? `[${rule.value}]` : ""}`.trimEnd());
536
+ }
537
+ }
538
+ return lines.join("\n");
539
+ }
540
+
541
+ // ── Helpers ────────────────────────────────────────────────────────────
542
+
543
+ /**
544
+ * Split a selector on top-level commas. Aware of:
545
+ * - parentheses (`:is(.a, .b)`)
546
+ * - brackets (`[data-value="a,b"]`)
547
+ * - string literals (`"a,b"`, `'a,b'`)
548
+ * - escape sequences (`\,`)
549
+ */
550
+ function splitOnCommas(selector) {
551
+ const parts = [];
552
+ let current = "";
553
+ let parenDepth = 0;
554
+ let bracketDepth = 0;
555
+ let inString = null;
556
+ let escapeNext = false;
557
+ for (let i = 0; i < selector.length; i++) {
558
+ const ch = selector[i];
559
+ if (escapeNext) {
560
+ current += ch;
561
+ escapeNext = false;
562
+ continue;
563
+ }
564
+ if (ch === "\\") {
565
+ current += ch;
566
+ escapeNext = true;
567
+ continue;
568
+ }
569
+ if (inString) {
570
+ current += ch;
571
+ if (ch === inString) inString = null;
572
+ continue;
573
+ }
574
+ if (ch === '"' || ch === "'") {
575
+ inString = ch;
576
+ current += ch;
577
+ continue;
578
+ }
579
+ if (ch === "(") {
580
+ parenDepth++;
581
+ current += ch;
582
+ continue;
583
+ }
584
+ if (ch === ")") {
585
+ if (parenDepth > 0) parenDepth--;
586
+ current += ch;
587
+ continue;
588
+ }
589
+ if (ch === "[") {
590
+ bracketDepth++;
591
+ current += ch;
592
+ continue;
593
+ }
594
+ if (ch === "]") {
595
+ if (bracketDepth > 0) bracketDepth--;
596
+ current += ch;
597
+ continue;
598
+ }
599
+ if (ch === "," && parenDepth === 0 && bracketDepth === 0) {
600
+ parts.push(current);
601
+ current = "";
602
+ continue;
603
+ }
604
+ current += ch;
605
+ }
606
+ parts.push(current);
607
+ return parts;
608
+ }
@@ -0,0 +1 @@
1
+ export {};