@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.
- package/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- 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
|
+
};
|