@treelocator/runtime 0.5.2 → 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 (163) 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/functions/cssRuleInspector.d.ts +83 -0
  41. package/dist/functions/cssRuleInspector.js +608 -0
  42. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  43. package/dist/functions/cssRuleInspector.test.js +439 -0
  44. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  45. package/dist/functions/deduplicateLabels.test.js +178 -0
  46. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  47. package/dist/functions/extractComputedStyles.d.ts +51 -0
  48. package/dist/functions/extractComputedStyles.js +447 -0
  49. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  50. package/dist/functions/extractComputedStyles.test.js +549 -0
  51. package/dist/functions/formatAncestryChain.d.ts +8 -0
  52. package/dist/functions/formatAncestryChain.js +21 -1
  53. package/dist/functions/formatAncestryChain.test.js +18 -0
  54. package/dist/functions/getUsableName.test.d.ts +1 -0
  55. package/dist/functions/getUsableName.test.js +219 -0
  56. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  57. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  58. package/dist/functions/mergeRects.test.js +210 -1
  59. package/dist/functions/namedSnapshots.d.ts +52 -0
  60. package/dist/functions/namedSnapshots.js +161 -0
  61. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  62. package/dist/functions/namedSnapshots.test.js +85 -0
  63. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  64. package/dist/functions/normalizeFilePath.test.js +66 -0
  65. package/dist/functions/parseDataId.test.d.ts +1 -0
  66. package/dist/functions/parseDataId.test.js +101 -0
  67. package/dist/hooks/getStorage.d.ts +3 -0
  68. package/dist/hooks/getStorage.js +17 -0
  69. package/dist/hooks/useEventListeners.d.ts +15 -0
  70. package/dist/hooks/useEventListeners.js +56 -0
  71. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  72. package/dist/hooks/useLocatorStorage.js +41 -0
  73. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  74. package/dist/hooks/useLocatorStorage.test.js +124 -0
  75. package/dist/hooks/useRecordingState.d.ts +43 -0
  76. package/dist/hooks/useRecordingState.js +387 -0
  77. package/dist/hooks/useSettings.d.ts +13 -0
  78. package/dist/hooks/useSettings.js +66 -0
  79. package/dist/index.d.ts +5 -2
  80. package/dist/index.js +4 -2
  81. package/dist/initRuntime.d.ts +3 -1
  82. package/dist/initRuntime.js +4 -1
  83. package/dist/mcpBridge.d.ts +61 -0
  84. package/dist/mcpBridge.js +534 -0
  85. package/dist/mcpBridge.test.d.ts +1 -0
  86. package/dist/mcpBridge.test.js +248 -0
  87. package/dist/output.css +20 -0
  88. package/dist/visualDiff/diff.d.ts +9 -0
  89. package/dist/visualDiff/diff.js +209 -0
  90. package/dist/visualDiff/diff.test.d.ts +1 -0
  91. package/dist/visualDiff/diff.test.js +253 -0
  92. package/dist/visualDiff/settle.d.ts +3 -0
  93. package/dist/visualDiff/settle.js +50 -0
  94. package/dist/visualDiff/settle.test.d.ts +1 -0
  95. package/dist/visualDiff/settle.test.js +65 -0
  96. package/dist/visualDiff/snapshot.d.ts +4 -0
  97. package/dist/visualDiff/snapshot.js +84 -0
  98. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  99. package/dist/visualDiff/snapshot.test.js +245 -0
  100. package/dist/visualDiff/types.d.ts +37 -0
  101. package/dist/visualDiff/types.js +1 -0
  102. package/package.json +2 -2
  103. package/scripts/wrapCSS.js +1 -1
  104. package/scripts/wrapImage.js +1 -1
  105. package/src/_generated_styles.ts +21 -1
  106. package/src/_generated_tree_icon.ts +1 -1
  107. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  108. package/src/adapters/createTreeNode.ts +12 -51
  109. package/src/adapters/detectFramework.test.ts +73 -0
  110. package/src/adapters/detectFramework.ts +28 -0
  111. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  112. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  113. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  114. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  115. package/src/adapters/react/findDebugSource.ts +5 -6
  116. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  117. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  118. package/src/adapters/react/reactAdapter.ts +1 -2
  119. package/src/adapters/resolveAdapter.ts +4 -14
  120. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  121. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  122. package/src/browserApi.test.ts +329 -0
  123. package/src/browserApi.ts +351 -4
  124. package/src/components/RecordingPillButton.tsx +301 -0
  125. package/src/components/RecordingResults.tsx +114 -13
  126. package/src/components/Runtime.tsx +176 -621
  127. package/src/components/SettingsPanel.tsx +339 -0
  128. package/src/consoleCapture.ts +113 -0
  129. package/src/functions/cssRuleInspector.test.ts +517 -0
  130. package/src/functions/cssRuleInspector.ts +708 -0
  131. package/src/functions/deduplicateLabels.test.ts +115 -0
  132. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  133. package/src/functions/extractComputedStyles.test.ts +681 -0
  134. package/src/functions/extractComputedStyles.ts +768 -0
  135. package/src/functions/formatAncestryChain.test.ts +23 -1
  136. package/src/functions/formatAncestryChain.ts +22 -1
  137. package/src/functions/getUsableName.test.ts +242 -0
  138. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  139. package/src/functions/mergeRects.test.ts +111 -1
  140. package/src/functions/namedSnapshots.test.ts +106 -0
  141. package/src/functions/namedSnapshots.ts +232 -0
  142. package/src/functions/normalizeFilePath.test.ts +80 -0
  143. package/src/functions/parseDataId.test.ts +125 -0
  144. package/src/hooks/getStorage.ts +26 -0
  145. package/src/hooks/useEventListeners.ts +97 -0
  146. package/src/hooks/useLocatorStorage.test.ts +127 -0
  147. package/src/hooks/useLocatorStorage.ts +60 -0
  148. package/src/hooks/useRecordingState.ts +516 -0
  149. package/src/hooks/useSettings.ts +83 -0
  150. package/src/index.ts +10 -5
  151. package/src/initRuntime.ts +5 -0
  152. package/src/mcpBridge.test.ts +260 -0
  153. package/src/mcpBridge.ts +677 -0
  154. package/src/visualDiff/diff.test.ts +167 -0
  155. package/src/visualDiff/diff.ts +242 -0
  156. package/src/visualDiff/settle.test.ts +77 -0
  157. package/src/visualDiff/settle.ts +62 -0
  158. package/src/visualDiff/snapshot.test.ts +200 -0
  159. package/src/visualDiff/snapshot.ts +119 -0
  160. package/src/visualDiff/types.ts +40 -0
  161. package/tsconfig.json +3 -1
  162. package/vitest.config.ts +18 -0
  163. package/jest.config.ts +0 -195
@@ -0,0 +1,768 @@
1
+ /**
2
+ * Extracts and formats computed styles from a DOM element.
3
+ * Output is optimized for AI consumption — minimal tokens, maximum signal.
4
+ */
5
+
6
+ // --- Types ---
7
+
8
+ export interface StyleSnapshot {
9
+ properties: Record<string, string>;
10
+ boundingRect: {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ top: number;
16
+ right: number;
17
+ bottom: number;
18
+ left: number;
19
+ };
20
+ }
21
+
22
+ export interface ComputedStylesResult {
23
+ formatted: string;
24
+ snapshot: StyleSnapshot;
25
+ }
26
+
27
+ // --- Property Groups (longhands read from getComputedStyle) ---
28
+
29
+ const LAYOUT_PROPERTIES = [
30
+ "display",
31
+ "position",
32
+ "top",
33
+ "right",
34
+ "bottom",
35
+ "left",
36
+ "width",
37
+ "height",
38
+ "min-width",
39
+ "max-width",
40
+ "min-height",
41
+ "max-height",
42
+ "box-sizing",
43
+ "overflow-x",
44
+ "overflow-y",
45
+ "flex-direction",
46
+ "flex-wrap",
47
+ "flex-grow",
48
+ "flex-shrink",
49
+ "flex-basis",
50
+ "align-items",
51
+ "align-self",
52
+ "align-content",
53
+ "justify-content",
54
+ "justify-self",
55
+ "grid-template-columns",
56
+ "grid-template-rows",
57
+ "grid-area",
58
+ "gap",
59
+ "column-gap",
60
+ "row-gap",
61
+ "margin-top",
62
+ "margin-right",
63
+ "margin-bottom",
64
+ "margin-left",
65
+ "padding-top",
66
+ "padding-right",
67
+ "padding-bottom",
68
+ "padding-left",
69
+ "z-index",
70
+ "float",
71
+ "clear",
72
+ ];
73
+
74
+ const VISUAL_PROPERTIES = [
75
+ "background-color",
76
+ "background-image",
77
+ "background-size",
78
+ "background-position",
79
+ "color",
80
+ "border-top-width",
81
+ "border-right-width",
82
+ "border-bottom-width",
83
+ "border-left-width",
84
+ "border-top-style",
85
+ "border-right-style",
86
+ "border-bottom-style",
87
+ "border-left-style",
88
+ "border-top-color",
89
+ "border-right-color",
90
+ "border-bottom-color",
91
+ "border-left-color",
92
+ "border-top-left-radius",
93
+ "border-top-right-radius",
94
+ "border-bottom-right-radius",
95
+ "border-bottom-left-radius",
96
+ "outline-width",
97
+ "outline-style",
98
+ "outline-color",
99
+ "box-shadow",
100
+ "opacity",
101
+ "visibility",
102
+ "transform",
103
+ "transition",
104
+ "animation",
105
+ ];
106
+
107
+ const TYPOGRAPHY_PROPERTIES = [
108
+ "font-family",
109
+ "font-size",
110
+ "font-weight",
111
+ "font-style",
112
+ "line-height",
113
+ "letter-spacing",
114
+ "text-align",
115
+ "text-decoration",
116
+ "text-transform",
117
+ "white-space",
118
+ "word-break",
119
+ ];
120
+
121
+ const INTERACTION_PROPERTIES = [
122
+ "cursor",
123
+ "pointer-events",
124
+ "user-select",
125
+ ];
126
+
127
+ const SVG_PROPERTIES = [
128
+ "fill",
129
+ "stroke",
130
+ "stroke-width",
131
+ "stroke-dasharray",
132
+ "stroke-dashoffset",
133
+ "stroke-linecap",
134
+ "stroke-linejoin",
135
+ "fill-opacity",
136
+ "stroke-opacity",
137
+ ];
138
+
139
+ const ALL_PROPERTIES = [
140
+ ...LAYOUT_PROPERTIES,
141
+ ...VISUAL_PROPERTIES,
142
+ ...TYPOGRAPHY_PROPERTIES,
143
+ ...INTERACTION_PROPERTIES,
144
+ ];
145
+
146
+ const ALL_PROPERTIES_WITH_SVG = [...ALL_PROPERTIES, ...SVG_PROPERTIES];
147
+
148
+ // Values that are always excluded regardless of defaults comparison
149
+ const ALWAYS_EXCLUDE_VALUES = new Set(["initial", "inherit", "unset"]);
150
+
151
+ // --- Property Groups Definition ---
152
+
153
+ interface PropertyGroup {
154
+ name: string;
155
+ props: string[];
156
+ }
157
+
158
+ function getPropertyGroups(isSvg: boolean): PropertyGroup[] {
159
+ const groups: PropertyGroup[] = [
160
+ { name: "Layout", props: LAYOUT_PROPERTIES },
161
+ { name: "Visual", props: VISUAL_PROPERTIES },
162
+ { name: "Typography", props: TYPOGRAPHY_PROPERTIES },
163
+ { name: "Interaction", props: INTERACTION_PROPERTIES },
164
+ ];
165
+ if (isSvg) {
166
+ groups.push({ name: "SVG", props: SVG_PROPERTIES });
167
+ }
168
+ return groups;
169
+ }
170
+
171
+ // --- Default Detection via Temp Element ---
172
+
173
+ /**
174
+ * Cache of default computed styles per tag name (+ SVG namespace marker).
175
+ * Browser defaults for a given tag are constant within a page load, so we
176
+ * avoid creating a temporary element and triggering layout on every click.
177
+ */
178
+ const defaultStylesCache = new Map<string, Map<string, string>>();
179
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
180
+
181
+ function createDefaultStyleProbe(
182
+ tagName: string,
183
+ isSvgElement: boolean
184
+ ): { element: Element; cleanup: () => void } {
185
+ const createProbeElement = () =>
186
+ isSvgElement
187
+ ? document.createElementNS(SVG_NAMESPACE, tagName)
188
+ : document.createElement(tagName);
189
+
190
+ const mountTarget = document.body ?? document.documentElement;
191
+ if (!mountTarget) {
192
+ return { element: createProbeElement(), cleanup: () => {} };
193
+ }
194
+
195
+ const host = document.createElement("div");
196
+ if (typeof host.attachShadow === "function") {
197
+ // Reset inherited values on the host and use Shadow DOM so author CSS
198
+ // from the page does not suppress informative inherited typography.
199
+ host.style.cssText =
200
+ "all:initial!important;position:fixed!important;display:block!important;visibility:hidden!important;pointer-events:none!important;left:-9999px!important;top:-9999px!important;contain:style layout paint!important;";
201
+ const root = host.attachShadow({ mode: "open" });
202
+ const probe = createProbeElement();
203
+ root.appendChild(probe);
204
+ mountTarget.appendChild(host);
205
+ return { element: probe, cleanup: () => host.remove() };
206
+ }
207
+
208
+ const probe = createProbeElement();
209
+ if (probe instanceof HTMLElement || probe instanceof SVGElement) {
210
+ probe.style.cssText =
211
+ "position:fixed!important;visibility:hidden!important;pointer-events:none!important;left:-9999px!important;top:-9999px!important;width:auto!important;height:auto!important;";
212
+ }
213
+ mountTarget.appendChild(probe);
214
+ return { element: probe, cleanup: () => probe.remove() };
215
+ }
216
+
217
+ function getDefaultStyles(
218
+ element: Element,
219
+ properties: string[]
220
+ ): Map<string, string> {
221
+ const tagName = element.tagName.toLowerCase();
222
+ const isSvgElement =
223
+ element.namespaceURI === "http://www.w3.org/2000/svg";
224
+ const cacheKey = `${isSvgElement ? "svg:" : ""}${tagName}`;
225
+
226
+ const cached = defaultStylesCache.get(cacheKey);
227
+ if (cached) return cached;
228
+
229
+ const defaults = new Map<string, string>();
230
+ let cleanup = () => {};
231
+ try {
232
+ const probe = createDefaultStyleProbe(tagName, isSvgElement);
233
+ cleanup = probe.cleanup;
234
+ const computed = window.getComputedStyle(probe.element);
235
+ for (const prop of properties) {
236
+ defaults.set(prop, computed.getPropertyValue(prop));
237
+ }
238
+ defaultStylesCache.set(cacheKey, defaults);
239
+ } catch {
240
+ // Fallback: empty defaults means nothing gets filtered. Do not cache
241
+ // a partial result so a future call can retry.
242
+ } finally {
243
+ cleanup();
244
+ }
245
+ return defaults;
246
+ }
247
+
248
+ // --- Shorthand Collapse Helpers ---
249
+
250
+ function collapseFourValues(
251
+ top: string,
252
+ right: string,
253
+ bottom: string,
254
+ left: string
255
+ ): string {
256
+ if (top === right && right === bottom && bottom === left) return top;
257
+ if (top === bottom && right === left) return `${top} ${right}`;
258
+ if (right === left) return `${top} ${right} ${bottom}`;
259
+ return `${top} ${right} ${bottom} ${left}`;
260
+ }
261
+
262
+ interface GroupedEntry {
263
+ name: string;
264
+ value: string;
265
+ }
266
+
267
+ function processGroupEntries(
268
+ groupProps: string[],
269
+ values: Record<string, string>,
270
+ isNonDefault: (prop: string, value: string) => boolean
271
+ ): GroupedEntry[] {
272
+ const entries: GroupedEntry[] = [];
273
+ const consumed = new Set<string>();
274
+
275
+ // --- Margin shorthand ---
276
+ tryFourValueShorthand(
277
+ "margin",
278
+ ["margin-top", "margin-right", "margin-bottom", "margin-left"],
279
+ groupProps,
280
+ values,
281
+ isNonDefault,
282
+ entries,
283
+ consumed
284
+ );
285
+
286
+ // --- Padding shorthand ---
287
+ tryFourValueShorthand(
288
+ "padding",
289
+ ["padding-top", "padding-right", "padding-bottom", "padding-left"],
290
+ groupProps,
291
+ values,
292
+ isNonDefault,
293
+ entries,
294
+ consumed
295
+ );
296
+
297
+ // --- Overflow shorthand ---
298
+ tryTwoValueShorthand(
299
+ "overflow",
300
+ ["overflow-x", "overflow-y"],
301
+ groupProps,
302
+ values,
303
+ isNonDefault,
304
+ entries,
305
+ consumed
306
+ );
307
+
308
+ // --- Flex shorthand ---
309
+ tryFlexShorthand(groupProps, values, isNonDefault, entries, consumed);
310
+
311
+ // --- Border shorthand ---
312
+ tryBorderShorthand(groupProps, values, isNonDefault, entries, consumed);
313
+
314
+ // --- Border-radius shorthand ---
315
+ tryFourValueShorthand(
316
+ "border-radius",
317
+ [
318
+ "border-top-left-radius",
319
+ "border-top-right-radius",
320
+ "border-bottom-right-radius",
321
+ "border-bottom-left-radius",
322
+ ],
323
+ groupProps,
324
+ values,
325
+ isNonDefault,
326
+ entries,
327
+ consumed
328
+ );
329
+
330
+ // --- Outline shorthand ---
331
+ tryOutlineShorthand(groupProps, values, isNonDefault, entries, consumed);
332
+
333
+ // --- Remaining individual properties ---
334
+ for (const prop of groupProps) {
335
+ if (consumed.has(prop)) continue;
336
+ const value = values[prop];
337
+ if (value && isNonDefault(prop, value)) {
338
+ entries.push({ name: prop, value });
339
+ }
340
+ }
341
+
342
+ return entries;
343
+ }
344
+
345
+ function tryFourValueShorthand(
346
+ shorthand: string,
347
+ longhands: string[],
348
+ groupProps: string[],
349
+ values: Record<string, string>,
350
+ isNonDefault: (prop: string, value: string) => boolean,
351
+ entries: GroupedEntry[],
352
+ consumed: Set<string>
353
+ ): void {
354
+ if (!longhands.every((l) => groupProps.includes(l))) return;
355
+
356
+ const vals = longhands.map((l) => values[l] || "");
357
+ const nonDefaultIndices = longhands.filter((l, i) =>
358
+ isNonDefault(l, vals[i]!)
359
+ );
360
+
361
+ if (nonDefaultIndices.length === 0) {
362
+ longhands.forEach((l) => consumed.add(l));
363
+ return;
364
+ }
365
+
366
+ if (nonDefaultIndices.length >= 2) {
367
+ const collapsed = collapseFourValues(
368
+ vals[0]!,
369
+ vals[1]!,
370
+ vals[2]!,
371
+ vals[3]!
372
+ );
373
+ entries.push({ name: shorthand, value: collapsed });
374
+ longhands.forEach((l) => consumed.add(l));
375
+ }
376
+ // If only 1 non-default, let it be output as individual longhand
377
+ }
378
+
379
+ function tryTwoValueShorthand(
380
+ shorthand: string,
381
+ longhands: [string, string],
382
+ groupProps: string[],
383
+ values: Record<string, string>,
384
+ isNonDefault: (prop: string, value: string) => boolean,
385
+ entries: GroupedEntry[],
386
+ consumed: Set<string>
387
+ ): void {
388
+ if (!longhands.every((l) => groupProps.includes(l))) return;
389
+
390
+ const a = values[longhands[0]] || "";
391
+ const b = values[longhands[1]] || "";
392
+ const aNonDefault = isNonDefault(longhands[0], a);
393
+ const bNonDefault = isNonDefault(longhands[1], b);
394
+
395
+ if (!aNonDefault && !bNonDefault) {
396
+ longhands.forEach((l) => consumed.add(l));
397
+ return;
398
+ }
399
+
400
+ if (aNonDefault && bNonDefault) {
401
+ if (a === b) {
402
+ entries.push({ name: shorthand, value: a });
403
+ } else {
404
+ entries.push({ name: shorthand, value: `${a} ${b}` });
405
+ }
406
+ longhands.forEach((l) => consumed.add(l));
407
+ }
408
+ // If only one is non-default, let it output individually
409
+ }
410
+
411
+ function tryFlexShorthand(
412
+ groupProps: string[],
413
+ values: Record<string, string>,
414
+ isNonDefault: (prop: string, value: string) => boolean,
415
+ entries: GroupedEntry[],
416
+ consumed: Set<string>
417
+ ): void {
418
+ const longhands = ["flex-grow", "flex-shrink", "flex-basis"];
419
+ if (!longhands.every((l) => groupProps.includes(l))) return;
420
+
421
+ const nonDefaultCount = longhands.filter((l) =>
422
+ isNonDefault(l, values[l] || "")
423
+ ).length;
424
+
425
+ if (nonDefaultCount === 0) {
426
+ longhands.forEach((l) => consumed.add(l));
427
+ return;
428
+ }
429
+
430
+ if (nonDefaultCount >= 2) {
431
+ entries.push({
432
+ name: "flex",
433
+ value: `${values["flex-grow"]} ${values["flex-shrink"]} ${values["flex-basis"]}`,
434
+ });
435
+ longhands.forEach((l) => consumed.add(l));
436
+ }
437
+ }
438
+
439
+ function tryBorderShorthand(
440
+ groupProps: string[],
441
+ values: Record<string, string>,
442
+ isNonDefault: (prop: string, value: string) => boolean,
443
+ entries: GroupedEntry[],
444
+ consumed: Set<string>
445
+ ): void {
446
+ const sides = ["top", "right", "bottom", "left"];
447
+ const widths = sides.map((s) => `border-${s}-width`);
448
+ const styles = sides.map((s) => `border-${s}-style`);
449
+ const colors = sides.map((s) => `border-${s}-color`);
450
+ const allProps = [...widths, ...styles, ...colors];
451
+
452
+ if (!allProps.every((p) => groupProps.includes(p))) return;
453
+
454
+ const anyNonDefault = allProps.some((p) =>
455
+ isNonDefault(p, values[p] || "")
456
+ );
457
+ if (!anyNonDefault) {
458
+ allProps.forEach((p) => consumed.add(p));
459
+ return;
460
+ }
461
+
462
+ const wVals = widths.map((p) => values[p] || "");
463
+ const sVals = styles.map((p) => values[p] || "");
464
+ const cVals = colors.map((p) => values[p] || "");
465
+
466
+ const allSameW = wVals.every((v) => v === wVals[0]);
467
+ const allSameS = sVals.every((v) => v === sVals[0]);
468
+ const allSameC = cVals.every((v) => v === cVals[0]);
469
+
470
+ if (allSameW && allSameS && allSameC) {
471
+ // All sides identical — use single shorthand
472
+ if (sVals[0] !== "none") {
473
+ entries.push({
474
+ name: "border",
475
+ value: `${wVals[0]} ${sVals[0]} ${cVals[0]}`,
476
+ });
477
+ }
478
+ allProps.forEach((p) => consumed.add(p));
479
+ return;
480
+ }
481
+
482
+ // Per-side shorthands for non-uniform borders
483
+ for (let i = 0; i < sides.length; i++) {
484
+ const side = sides[i]!;
485
+ const sideProps = [widths[i]!, styles[i]!, colors[i]!];
486
+ const anySideNonDefault = sideProps.some((p) =>
487
+ isNonDefault(p, values[p] || "")
488
+ );
489
+
490
+ if (anySideNonDefault && sVals[i] !== "none") {
491
+ entries.push({
492
+ name: `border-${side}`,
493
+ value: `${wVals[i]} ${sVals[i]} ${cVals[i]}`,
494
+ });
495
+ }
496
+ sideProps.forEach((p) => consumed.add(p));
497
+ }
498
+ }
499
+
500
+ function tryOutlineShorthand(
501
+ groupProps: string[],
502
+ values: Record<string, string>,
503
+ isNonDefault: (prop: string, value: string) => boolean,
504
+ entries: GroupedEntry[],
505
+ consumed: Set<string>
506
+ ): void {
507
+ const longhands = ["outline-width", "outline-style", "outline-color"];
508
+ if (!longhands.every((l) => groupProps.includes(l))) return;
509
+
510
+ const anyNonDefault = longhands.some((l) =>
511
+ isNonDefault(l, values[l] || "")
512
+ );
513
+ if (!anyNonDefault) {
514
+ longhands.forEach((l) => consumed.add(l));
515
+ return;
516
+ }
517
+
518
+ const outlineStyle = values["outline-style"] || "none";
519
+ if (outlineStyle !== "none") {
520
+ entries.push({
521
+ name: "outline",
522
+ value: `${values["outline-width"]} ${values["outline-style"]} ${values["outline-color"]}`,
523
+ });
524
+ }
525
+ longhands.forEach((l) => consumed.add(l));
526
+ }
527
+
528
+ // --- Diff Mode ---
529
+
530
+ // WeakRef is only guaranteed in modern runtimes (Chrome 84+, Safari 14.1+,
531
+ // Firefox 79+, Node 14.6+). Guard access so the runtime doesn't throw a
532
+ // ReferenceError in older environments; we fall back to a strong reference.
533
+ const HAS_WEAK_REF = typeof (globalThis as any).WeakRef !== "undefined";
534
+
535
+ type ElementHolder = {
536
+ deref: () => Element | undefined;
537
+ };
538
+
539
+ function makeElementHolder(element: Element): ElementHolder {
540
+ if (HAS_WEAK_REF) {
541
+ const ref = new (globalThis as any).WeakRef(element) as {
542
+ deref: () => Element | undefined;
543
+ };
544
+ return { deref: () => ref.deref() };
545
+ }
546
+ return { deref: () => element };
547
+ }
548
+
549
+ let lastSnapshot: {
550
+ elementRef: ElementHolder;
551
+ snapshot: StyleSnapshot;
552
+ } | null = null;
553
+
554
+ function formatDiff(
555
+ prev: StyleSnapshot,
556
+ curr: StyleSnapshot,
557
+ elementLabel: string | undefined
558
+ ): string {
559
+ const lines: string[] = [];
560
+
561
+ const header = elementLabel
562
+ ? `[ComputedStyles \u0394] ${elementLabel}`
563
+ : "[ComputedStyles \u0394]";
564
+ lines.push(header);
565
+ lines.push("\u2500".repeat(Math.max(header.length, 40)));
566
+
567
+ const allProps = new Set([
568
+ ...Object.keys(prev.properties),
569
+ ...Object.keys(curr.properties),
570
+ ]);
571
+
572
+ const changes: string[] = [];
573
+
574
+ for (const prop of allProps) {
575
+ const prevVal = prev.properties[prop] || "";
576
+ const currVal = curr.properties[prop] || "";
577
+ if (prevVal === currVal) continue;
578
+
579
+ if (!prevVal || ALWAYS_EXCLUDE_VALUES.has(prevVal)) {
580
+ changes.push(`+ ${prop}: ${currVal}`);
581
+ } else if (!currVal || ALWAYS_EXCLUDE_VALUES.has(currVal)) {
582
+ changes.push(`- ${prop}: ${prevVal}`);
583
+ } else {
584
+ changes.push(`~ ${prop}: ${prevVal} \u2192 ${currVal}`);
585
+ }
586
+ }
587
+
588
+ const rectKeys = ["x", "y", "width", "height"] as const;
589
+ const rectChanges: string[] = [];
590
+ for (const key of rectKeys) {
591
+ const p = prev.boundingRect[key];
592
+ const c = curr.boundingRect[key];
593
+ if (p !== c) {
594
+ rectChanges.push(`~ ${key}: ${p} \u2192 ${c}`);
595
+ }
596
+ }
597
+
598
+ if (changes.length === 0 && rectChanges.length === 0) {
599
+ lines.push("");
600
+ lines.push("No changes detected");
601
+ } else {
602
+ if (changes.length > 0) {
603
+ lines.push("");
604
+ for (const change of changes) {
605
+ lines.push(change);
606
+ }
607
+ }
608
+ if (rectChanges.length > 0) {
609
+ lines.push("");
610
+ lines.push("BoundingRect");
611
+ for (const change of rectChanges) {
612
+ lines.push(change);
613
+ }
614
+ }
615
+ }
616
+
617
+ return lines.join("\n");
618
+ }
619
+
620
+ // --- Main Extraction Function ---
621
+
622
+ export interface ExtractOptions {
623
+ /**
624
+ * Skip diff-mode detection and always return the full formatted styles.
625
+ * Used by internal call sites that re-extract for the same element within
626
+ * the diff window (e.g. source-map enrichment re-runs in Runtime.tsx) where
627
+ * returning a "No changes detected" delta would be incorrect.
628
+ */
629
+ forceFull?: boolean;
630
+ /**
631
+ * Include curated properties even when they match browser defaults for the
632
+ * element's tag. Useful when you want a fuller dump closer to DevTools.
633
+ */
634
+ includeDefaults?: boolean;
635
+ }
636
+
637
+ export function readSnapshot(element: Element): StyleSnapshot {
638
+ const isSvg = element instanceof SVGElement;
639
+ const properties = isSvg ? ALL_PROPERTIES_WITH_SVG : ALL_PROPERTIES;
640
+
641
+ const computed = window.getComputedStyle(element);
642
+ const values: Record<string, string> = {};
643
+ for (const prop of properties) {
644
+ values[prop] = computed.getPropertyValue(prop);
645
+ }
646
+
647
+ const rect = element.getBoundingClientRect();
648
+ const boundingRect = {
649
+ x: Math.round(rect.x),
650
+ y: Math.round(rect.y),
651
+ width: Math.round(rect.width),
652
+ height: Math.round(rect.height),
653
+ top: Math.round(rect.top),
654
+ right: Math.round(rect.right),
655
+ bottom: Math.round(rect.bottom),
656
+ left: Math.round(rect.left),
657
+ };
658
+
659
+ return { properties: values, boundingRect };
660
+ }
661
+
662
+ export function extractComputedStyles(
663
+ element: Element,
664
+ elementLabel?: string,
665
+ options: ExtractOptions = {}
666
+ ): ComputedStylesResult {
667
+ const isSvg = element instanceof SVGElement;
668
+ const properties = isSvg ? ALL_PROPERTIES_WITH_SVG : ALL_PROPERTIES;
669
+
670
+ const snapshot = readSnapshot(element);
671
+ const values = snapshot.properties;
672
+ const boundingRect = snapshot.boundingRect;
673
+
674
+ let diffMode = false;
675
+ let previousSnapshot: StyleSnapshot | null = null;
676
+ if (lastSnapshot && !options.forceFull) {
677
+ const prevElement = lastSnapshot.elementRef.deref();
678
+ if (prevElement === element) {
679
+ diffMode = true;
680
+ previousSnapshot = lastSnapshot.snapshot;
681
+ }
682
+ }
683
+
684
+ lastSnapshot = {
685
+ elementRef: makeElementHolder(element),
686
+ snapshot,
687
+ };
688
+
689
+ // Compute default filtering up front so both diff-mode and normal-mode
690
+ // return paths can hand callers a filtered snapshot.
691
+ const defaults = options.includeDefaults
692
+ ? null
693
+ : getDefaultStyles(element, properties);
694
+
695
+ const isNonDefault = (prop: string, value: string): boolean => {
696
+ if (!value || value === "") return false;
697
+ if (ALWAYS_EXCLUDE_VALUES.has(value)) return false;
698
+ if (options.includeDefaults) return true;
699
+ return defaults?.get(prop) !== value;
700
+ };
701
+
702
+ const returnedSnapshot: StyleSnapshot = options.includeDefaults
703
+ ? snapshot
704
+ : {
705
+ properties: Object.fromEntries(
706
+ Object.entries(values).filter(([prop, value]) =>
707
+ isNonDefault(prop, value)
708
+ )
709
+ ),
710
+ boundingRect,
711
+ };
712
+
713
+ if (diffMode && previousSnapshot) {
714
+ return {
715
+ formatted: formatDiff(previousSnapshot, snapshot, elementLabel),
716
+ snapshot: returnedSnapshot,
717
+ };
718
+ }
719
+
720
+ const lines: string[] = [];
721
+
722
+ // Header
723
+ const header = elementLabel
724
+ ? `[ComputedStyles] ${elementLabel}`
725
+ : "[ComputedStyles]";
726
+ lines.push(header);
727
+ lines.push("\u2500".repeat(Math.max(header.length, 40)));
728
+
729
+ // Process each property group
730
+ const groups = getPropertyGroups(isSvg);
731
+ for (const group of groups) {
732
+ const entries = processGroupEntries(
733
+ group.props,
734
+ values,
735
+ isNonDefault
736
+ );
737
+ if (entries.length > 0) {
738
+ lines.push("");
739
+ lines.push(group.name);
740
+ for (const entry of entries) {
741
+ lines.push(` ${entry.name}: ${entry.value}`);
742
+ }
743
+ }
744
+ }
745
+
746
+ // Bounding rect always last
747
+ lines.push("");
748
+ lines.push("BoundingRect");
749
+ lines.push(
750
+ ` x: ${boundingRect.x} y: ${boundingRect.y} width: ${boundingRect.width} height: ${boundingRect.height}`
751
+ );
752
+
753
+ return { formatted: lines.join("\n"), snapshot: returnedSnapshot };
754
+ }
755
+
756
+ export { formatDiff as formatSnapshotDiff };
757
+
758
+ // Exported for testing
759
+ export {
760
+ collapseFourValues as _collapseFourValues,
761
+ processGroupEntries as _processGroupEntries,
762
+ formatDiff as _formatDiff,
763
+ LAYOUT_PROPERTIES,
764
+ VISUAL_PROPERTIES,
765
+ TYPOGRAPHY_PROPERTIES,
766
+ INTERACTION_PROPERTIES,
767
+ SVG_PROPERTIES,
768
+ };