uivisor 0.1.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/babel/index.d.ts +21 -0
- package/dist/babel/index.js +6 -0
- package/dist/chunk-4XKGGP26.js +32 -0
- package/dist/next/index.cjs +81 -0
- package/dist/next/loader.cjs +96 -0
- package/dist/overlay/index.d.ts +3 -0
- package/dist/overlay/index.js +1915 -0
- package/dist/vite/index.d.ts +13 -0
- package/dist/vite/index.js +80 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1915 @@
|
|
|
1
|
+
// src/overlay/breakpoint.ts
|
|
2
|
+
var TAILWIND = {
|
|
3
|
+
name: "tailwind",
|
|
4
|
+
breakpoints: [
|
|
5
|
+
{ name: "sm", minWidth: 640 },
|
|
6
|
+
{ name: "md", minWidth: 768 },
|
|
7
|
+
{ name: "lg", minWidth: 1024 },
|
|
8
|
+
{ name: "xl", minWidth: 1280 },
|
|
9
|
+
{ name: "2xl", minWidth: 1536 }
|
|
10
|
+
]
|
|
11
|
+
};
|
|
12
|
+
function activeBreakpoint(width, system = TAILWIND) {
|
|
13
|
+
let active = { name: "base", minWidth: 0 };
|
|
14
|
+
for (const bp of system.breakpoints) {
|
|
15
|
+
if (width >= bp.minWidth) active = { name: bp.name, minWidth: bp.minWidth };
|
|
16
|
+
}
|
|
17
|
+
return active;
|
|
18
|
+
}
|
|
19
|
+
function currentBreakpoint(system = TAILWIND) {
|
|
20
|
+
const width = typeof window !== "undefined" ? window.innerWidth : 0;
|
|
21
|
+
return activeBreakpoint(width, system);
|
|
22
|
+
}
|
|
23
|
+
var KNOWN_NAMES = {
|
|
24
|
+
640: "sm",
|
|
25
|
+
768: "md",
|
|
26
|
+
1024: "lg",
|
|
27
|
+
1280: "xl",
|
|
28
|
+
1536: "2xl"
|
|
29
|
+
};
|
|
30
|
+
function nameForWidth(px2) {
|
|
31
|
+
return KNOWN_NAMES[px2] ?? `${px2}`;
|
|
32
|
+
}
|
|
33
|
+
function detectBreakpoints() {
|
|
34
|
+
if (typeof document === "undefined") return TAILWIND;
|
|
35
|
+
const widths = /* @__PURE__ */ new Set();
|
|
36
|
+
const visit = (rules) => {
|
|
37
|
+
if (!rules) return;
|
|
38
|
+
for (let i = 0; i < rules.length; i++) {
|
|
39
|
+
const rule = rules[i];
|
|
40
|
+
if (rule.type === 4 && rule.media) {
|
|
41
|
+
const m = /min-width:\s*(\d*\.?\d+)(px|rem|em)/i.exec(rule.media.mediaText);
|
|
42
|
+
if (m) {
|
|
43
|
+
const val = parseFloat(m[1]);
|
|
44
|
+
const px2 = m[2].toLowerCase() === "px" ? val : val * 16;
|
|
45
|
+
if (px2 >= 240 && px2 <= 4096) widths.add(Math.round(px2));
|
|
46
|
+
}
|
|
47
|
+
visit(rule.cssRules);
|
|
48
|
+
} else if (rule.cssRules) {
|
|
49
|
+
visit(rule.cssRules);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
for (let i = 0; i < document.styleSheets.length; i++) {
|
|
54
|
+
try {
|
|
55
|
+
visit(document.styleSheets[i].cssRules);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const sorted = [...widths].sort((a, b) => a - b);
|
|
60
|
+
if (!sorted.length) return TAILWIND;
|
|
61
|
+
return {
|
|
62
|
+
name: "detected",
|
|
63
|
+
breakpoints: sorted.map((w) => ({ name: nameForWidth(w), minWidth: w }))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/overlay/tokens.ts
|
|
68
|
+
var SPACING_PREFIX = {
|
|
69
|
+
padding: "p",
|
|
70
|
+
"padding-top": "pt",
|
|
71
|
+
"padding-right": "pr",
|
|
72
|
+
"padding-bottom": "pb",
|
|
73
|
+
"padding-left": "pl",
|
|
74
|
+
margin: "m",
|
|
75
|
+
"margin-top": "mt",
|
|
76
|
+
"margin-right": "mr",
|
|
77
|
+
"margin-bottom": "mb",
|
|
78
|
+
"margin-left": "ml",
|
|
79
|
+
gap: "gap",
|
|
80
|
+
"row-gap": "gap-y",
|
|
81
|
+
"column-gap": "gap-x"
|
|
82
|
+
};
|
|
83
|
+
var BORDER_WIDTH = {
|
|
84
|
+
0: "border-0",
|
|
85
|
+
1: "border",
|
|
86
|
+
2: "border-2",
|
|
87
|
+
4: "border-4",
|
|
88
|
+
8: "border-8"
|
|
89
|
+
};
|
|
90
|
+
var SPACING_SCALE = /* @__PURE__ */ new Set([
|
|
91
|
+
0,
|
|
92
|
+
0.5,
|
|
93
|
+
1,
|
|
94
|
+
1.5,
|
|
95
|
+
2,
|
|
96
|
+
2.5,
|
|
97
|
+
3,
|
|
98
|
+
3.5,
|
|
99
|
+
4,
|
|
100
|
+
5,
|
|
101
|
+
6,
|
|
102
|
+
7,
|
|
103
|
+
8,
|
|
104
|
+
9,
|
|
105
|
+
10,
|
|
106
|
+
11,
|
|
107
|
+
12,
|
|
108
|
+
14,
|
|
109
|
+
16,
|
|
110
|
+
20,
|
|
111
|
+
24,
|
|
112
|
+
28,
|
|
113
|
+
32,
|
|
114
|
+
36,
|
|
115
|
+
40,
|
|
116
|
+
44,
|
|
117
|
+
48,
|
|
118
|
+
52,
|
|
119
|
+
56,
|
|
120
|
+
60,
|
|
121
|
+
64,
|
|
122
|
+
72,
|
|
123
|
+
80,
|
|
124
|
+
96
|
|
125
|
+
]);
|
|
126
|
+
var FONT_WEIGHT = {
|
|
127
|
+
"100": "font-thin",
|
|
128
|
+
"200": "font-extralight",
|
|
129
|
+
"300": "font-light",
|
|
130
|
+
"400": "font-normal",
|
|
131
|
+
"500": "font-medium",
|
|
132
|
+
"600": "font-semibold",
|
|
133
|
+
"700": "font-bold",
|
|
134
|
+
"800": "font-extrabold",
|
|
135
|
+
"900": "font-black"
|
|
136
|
+
};
|
|
137
|
+
var FONT_SIZE = {
|
|
138
|
+
12: "text-xs",
|
|
139
|
+
14: "text-sm",
|
|
140
|
+
16: "text-base",
|
|
141
|
+
18: "text-lg",
|
|
142
|
+
20: "text-xl",
|
|
143
|
+
24: "text-2xl",
|
|
144
|
+
30: "text-3xl",
|
|
145
|
+
36: "text-4xl",
|
|
146
|
+
48: "text-5xl",
|
|
147
|
+
60: "text-6xl"
|
|
148
|
+
};
|
|
149
|
+
var RADIUS = {
|
|
150
|
+
0: "rounded-none",
|
|
151
|
+
2: "rounded-sm",
|
|
152
|
+
4: "rounded",
|
|
153
|
+
6: "rounded-md",
|
|
154
|
+
8: "rounded-lg",
|
|
155
|
+
12: "rounded-xl",
|
|
156
|
+
16: "rounded-2xl",
|
|
157
|
+
24: "rounded-3xl",
|
|
158
|
+
9999: "rounded-full"
|
|
159
|
+
};
|
|
160
|
+
function px(value) {
|
|
161
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(value.trim());
|
|
162
|
+
return m ? parseFloat(m[1]) : null;
|
|
163
|
+
}
|
|
164
|
+
function rgbToHex(value) {
|
|
165
|
+
const m = /rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i.exec(value);
|
|
166
|
+
if (!m) return null;
|
|
167
|
+
const hex = (n) => Number(n).toString(16).padStart(2, "0");
|
|
168
|
+
return `#${hex(m[1])}${hex(m[2])}${hex(m[3])}`;
|
|
169
|
+
}
|
|
170
|
+
function suggestUtility(property, value) {
|
|
171
|
+
const spPrefix = SPACING_PREFIX[property];
|
|
172
|
+
if (spPrefix) {
|
|
173
|
+
const n = px(value);
|
|
174
|
+
if (n == null) return null;
|
|
175
|
+
const scale = n / 4;
|
|
176
|
+
if (SPACING_SCALE.has(scale)) {
|
|
177
|
+
const tok = Number.isInteger(scale) ? String(scale) : String(scale);
|
|
178
|
+
return `${spPrefix}-${tok}`;
|
|
179
|
+
}
|
|
180
|
+
return `${spPrefix}-[${n}px]`;
|
|
181
|
+
}
|
|
182
|
+
if (property === "font-weight") {
|
|
183
|
+
return FONT_WEIGHT[value.trim()] ?? `font-[${value.trim()}]`;
|
|
184
|
+
}
|
|
185
|
+
if (property === "font-size") {
|
|
186
|
+
const n = px(value);
|
|
187
|
+
if (n == null) return null;
|
|
188
|
+
return FONT_SIZE[n] ?? `text-[${n}px]`;
|
|
189
|
+
}
|
|
190
|
+
if (property.endsWith("radius")) {
|
|
191
|
+
const n = px(value);
|
|
192
|
+
if (n == null) return null;
|
|
193
|
+
if (property === "border-radius") return RADIUS[n] ?? `rounded-[${n}px]`;
|
|
194
|
+
const cp = {
|
|
195
|
+
"border-top-left-radius": "rounded-tl",
|
|
196
|
+
"border-top-right-radius": "rounded-tr",
|
|
197
|
+
"border-bottom-right-radius": "rounded-br",
|
|
198
|
+
"border-bottom-left-radius": "rounded-bl"
|
|
199
|
+
}[property];
|
|
200
|
+
return cp ? `${cp}-[${n}px]` : `rounded-[${n}px]`;
|
|
201
|
+
}
|
|
202
|
+
if (property.startsWith("border") && property.endsWith("width")) {
|
|
203
|
+
const n = px(value);
|
|
204
|
+
if (n == null) return null;
|
|
205
|
+
if (property === "border-width") return BORDER_WIDTH[n] ?? `border-[${n}px]`;
|
|
206
|
+
const sp = {
|
|
207
|
+
"border-top-width": "border-t",
|
|
208
|
+
"border-right-width": "border-r",
|
|
209
|
+
"border-bottom-width": "border-b",
|
|
210
|
+
"border-left-width": "border-l"
|
|
211
|
+
}[property];
|
|
212
|
+
if (!sp) return null;
|
|
213
|
+
if (n === 1) return sp;
|
|
214
|
+
if ([0, 2, 4, 8].includes(n)) return `${sp}-${n}`;
|
|
215
|
+
return `${sp}-[${n}px]`;
|
|
216
|
+
}
|
|
217
|
+
if (property === "line-height") {
|
|
218
|
+
const t = value.trim();
|
|
219
|
+
const unitless = /^(\d*\.?\d+)$/.exec(t);
|
|
220
|
+
if (unitless) {
|
|
221
|
+
const named = {
|
|
222
|
+
"1": "leading-none",
|
|
223
|
+
"1.25": "leading-tight",
|
|
224
|
+
"1.375": "leading-snug",
|
|
225
|
+
"1.5": "leading-normal",
|
|
226
|
+
"1.625": "leading-relaxed",
|
|
227
|
+
"2": "leading-loose"
|
|
228
|
+
};
|
|
229
|
+
return named[String(parseFloat(unitless[1]))] ?? `leading-[${t}]`;
|
|
230
|
+
}
|
|
231
|
+
const n = px(t);
|
|
232
|
+
if (n != null) return `leading-[${n}px]`;
|
|
233
|
+
return `leading-[${t}]`;
|
|
234
|
+
}
|
|
235
|
+
if (property === "letter-spacing") {
|
|
236
|
+
const t = value.trim();
|
|
237
|
+
if (t === "normal" || t === "0" || t === "0px" || t === "0em") return "tracking-normal";
|
|
238
|
+
const em = /^(-?\d*\.?\d+)em$/.exec(t);
|
|
239
|
+
if (em) {
|
|
240
|
+
const named = {
|
|
241
|
+
"-0.05": "tracking-tighter",
|
|
242
|
+
"-0.025": "tracking-tight",
|
|
243
|
+
"0.025": "tracking-wide",
|
|
244
|
+
"0.05": "tracking-wider",
|
|
245
|
+
"0.1": "tracking-widest"
|
|
246
|
+
};
|
|
247
|
+
return named[String(parseFloat(em[1]))] ?? `tracking-[${t}]`;
|
|
248
|
+
}
|
|
249
|
+
return `tracking-[${t}]`;
|
|
250
|
+
}
|
|
251
|
+
if (property === "color" || property === "background-color") {
|
|
252
|
+
const hex = rgbToHex(value) ?? (value.startsWith("#") ? value : null);
|
|
253
|
+
if (!hex) return null;
|
|
254
|
+
return property === "color" ? `text-[${hex}]` : `bg-[${hex}]`;
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/overlay/capture.ts
|
|
260
|
+
function snapshot(el, props) {
|
|
261
|
+
const cs = getComputedStyle(el);
|
|
262
|
+
const out = {};
|
|
263
|
+
for (const p of props) out[p] = cs.getPropertyValue(p).trim();
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
function applyOverride(el, css, value) {
|
|
267
|
+
el.style.setProperty(css, value, "important");
|
|
268
|
+
}
|
|
269
|
+
function removeOverride(el, css) {
|
|
270
|
+
el.style.removeProperty(css);
|
|
271
|
+
}
|
|
272
|
+
function toHexInput(computed) {
|
|
273
|
+
if (/^#[0-9a-f]{6}$/i.test(computed.trim())) return computed.trim();
|
|
274
|
+
return rgbToHex(computed) ?? "#000000";
|
|
275
|
+
}
|
|
276
|
+
function valueInfo(property, computed) {
|
|
277
|
+
return { computed, token: suggestUtility(property, computed) };
|
|
278
|
+
}
|
|
279
|
+
function buildChange(property, originalComputed, appliedValue, bp) {
|
|
280
|
+
return {
|
|
281
|
+
property,
|
|
282
|
+
before: valueInfo(property, originalComputed),
|
|
283
|
+
after: valueInfo(property, appliedValue),
|
|
284
|
+
breakpoint: bp.name,
|
|
285
|
+
breakpointPx: bp.minWidth,
|
|
286
|
+
state: null
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/overlay/fields.ts
|
|
291
|
+
var UNIT_LABELS = {
|
|
292
|
+
"": "\xD7",
|
|
293
|
+
px: "px",
|
|
294
|
+
"%": "%",
|
|
295
|
+
em: "em",
|
|
296
|
+
rem: "rem"
|
|
297
|
+
};
|
|
298
|
+
var sv = (paths) => `<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
|
299
|
+
var ICONS = {
|
|
300
|
+
padding: sv('<rect x="2" y="2" width="12" height="12" rx="1.5"/><rect x="5" y="5" width="6" height="6" rx="1" opacity=".45"/>'),
|
|
301
|
+
margin: sv('<rect x="4.5" y="4.5" width="7" height="7" rx="1"/><rect x="1.5" y="1.5" width="13" height="13" rx="1.5" opacity=".45"/>'),
|
|
302
|
+
radius: sv('<path d="M3 13.5 V7 a4 4 0 0 1 4 -4 h6.5"/>'),
|
|
303
|
+
gap: sv('<rect x="2.5" y="3" width="3.5" height="10" rx="1"/><rect x="10" y="3" width="3.5" height="10" rx="1"/>'),
|
|
304
|
+
border: sv('<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke-dasharray="2.2 2"/>'),
|
|
305
|
+
size: sv('<path d="M3.5 13 L8 3 L12.5 13 M5.4 9 H10.6"/>'),
|
|
306
|
+
line: sv('<path d="M2.5 3 H13.5 M2.5 8 H13.5 M2.5 13 H13.5"/>'),
|
|
307
|
+
tracking: sv('<path d="M5 4 V12 M11 4 V12"/><path d="M2.5 8 H4 M1.8 6.6 L0.8 8 L1.8 9.4"/><path d="M13.5 8 H12 M14.2 6.6 L15.2 8 L14.2 9.4"/>'),
|
|
308
|
+
expand: sv('<rect x="2" y="2" width="4.5" height="4.5" rx="1"/><rect x="9.5" y="2" width="4.5" height="4.5" rx="1"/><rect x="2" y="9.5" width="4.5" height="4.5" rx="1"/><rect x="9.5" y="9.5" width="4.5" height="4.5" rx="1"/>'),
|
|
309
|
+
collapse: sv('<rect x="3" y="3" width="10" height="10" rx="2"/>')
|
|
310
|
+
};
|
|
311
|
+
var SECTIONS = [
|
|
312
|
+
{
|
|
313
|
+
title: "Spacing",
|
|
314
|
+
controls: [
|
|
315
|
+
{
|
|
316
|
+
kind: "box",
|
|
317
|
+
key: "padding",
|
|
318
|
+
label: "Padding",
|
|
319
|
+
icon: ICONS.padding,
|
|
320
|
+
sides: [
|
|
321
|
+
{ css: "padding-top", label: "T" },
|
|
322
|
+
{ css: "padding-right", label: "R" },
|
|
323
|
+
{ css: "padding-bottom", label: "B" },
|
|
324
|
+
{ css: "padding-left", label: "L" }
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
kind: "box",
|
|
329
|
+
key: "margin",
|
|
330
|
+
label: "Margin",
|
|
331
|
+
icon: ICONS.margin,
|
|
332
|
+
sides: [
|
|
333
|
+
{ css: "margin-top", label: "T" },
|
|
334
|
+
{ css: "margin-right", label: "R" },
|
|
335
|
+
{ css: "margin-bottom", label: "B" },
|
|
336
|
+
{ css: "margin-left", label: "L" }
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
// Gap only matters on flex/grid containers.
|
|
340
|
+
{ kind: "len", css: "gap", label: "Gap", icon: ICONS.gap, requires: "flexgrid" }
|
|
341
|
+
]
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
title: "Border",
|
|
345
|
+
controls: [
|
|
346
|
+
{
|
|
347
|
+
kind: "box",
|
|
348
|
+
key: "radius",
|
|
349
|
+
label: "Radius",
|
|
350
|
+
icon: ICONS.radius,
|
|
351
|
+
sides: [
|
|
352
|
+
{ css: "border-top-left-radius", label: "TL" },
|
|
353
|
+
{ css: "border-top-right-radius", label: "TR" },
|
|
354
|
+
{ css: "border-bottom-right-radius", label: "BR" },
|
|
355
|
+
{ css: "border-bottom-left-radius", label: "BL" }
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
kind: "box",
|
|
360
|
+
key: "border",
|
|
361
|
+
label: "Border",
|
|
362
|
+
icon: ICONS.border,
|
|
363
|
+
sides: [
|
|
364
|
+
{ css: "border-top-width", label: "T" },
|
|
365
|
+
{ css: "border-right-width", label: "R" },
|
|
366
|
+
{ css: "border-bottom-width", label: "B" },
|
|
367
|
+
{ css: "border-left-width", label: "L" }
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
// Typography only shows when the element renders its own text.
|
|
374
|
+
title: "Typography",
|
|
375
|
+
controls: [
|
|
376
|
+
{ kind: "len", css: "font-size", label: "Size", icon: ICONS.size, requires: "text" },
|
|
377
|
+
{
|
|
378
|
+
kind: "select",
|
|
379
|
+
css: "font-weight",
|
|
380
|
+
label: "Weight",
|
|
381
|
+
options: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
|
382
|
+
requires: "text"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
kind: "dim",
|
|
386
|
+
css: "line-height",
|
|
387
|
+
label: "Line",
|
|
388
|
+
icon: ICONS.line,
|
|
389
|
+
units: ["px", "%", ""],
|
|
390
|
+
defaultUnit: "px",
|
|
391
|
+
requires: "text"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
kind: "dim",
|
|
395
|
+
css: "letter-spacing",
|
|
396
|
+
label: "Spacing",
|
|
397
|
+
icon: ICONS.tracking,
|
|
398
|
+
units: ["em", "px"],
|
|
399
|
+
defaultUnit: "em",
|
|
400
|
+
requires: "text"
|
|
401
|
+
},
|
|
402
|
+
{ kind: "color", css: "color", label: "Text", requires: "text" }
|
|
403
|
+
]
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
title: "Fill",
|
|
407
|
+
controls: [{ kind: "color", css: "background-color", label: "Background" }]
|
|
408
|
+
}
|
|
409
|
+
];
|
|
410
|
+
var ALL_CSS = (() => {
|
|
411
|
+
const set = /* @__PURE__ */ new Set();
|
|
412
|
+
for (const s of SECTIONS)
|
|
413
|
+
for (const c of s.controls) {
|
|
414
|
+
if (c.kind === "box") c.sides.forEach((side) => set.add(side.css));
|
|
415
|
+
else set.add(c.css);
|
|
416
|
+
}
|
|
417
|
+
return [...set];
|
|
418
|
+
})();
|
|
419
|
+
var SHORTHAND_SETS = [
|
|
420
|
+
{ shorthand: "padding", parts: ["padding-top", "padding-right", "padding-bottom", "padding-left"] },
|
|
421
|
+
{ shorthand: "margin", parts: ["margin-top", "margin-right", "margin-bottom", "margin-left"] },
|
|
422
|
+
{
|
|
423
|
+
shorthand: "border-radius",
|
|
424
|
+
parts: [
|
|
425
|
+
"border-top-left-radius",
|
|
426
|
+
"border-top-right-radius",
|
|
427
|
+
"border-bottom-right-radius",
|
|
428
|
+
"border-bottom-left-radius"
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
shorthand: "border-width",
|
|
433
|
+
parts: ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width"]
|
|
434
|
+
}
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
// src/overlay/mechanism.ts
|
|
438
|
+
var TW_UTILITY = /^-?(?:p|m|px|py|pt|pr|pb|pl|mx|my|mt|mr|mb|ml|gap|w|h|min-w|max-w|min-h|max-h|text|bg|border|rounded|flex|grid|gap-x|gap-y|items|justify|self|place|font|leading|tracking|space|inline|block|inline-block|hidden|table|absolute|relative|fixed|sticky|static|top|bottom|left|right|inset|z|shadow|opacity|transition|duration|ease|scale|rotate|translate|cursor|overflow|object|aspect|order|col|row|basis|grow|shrink|divide|ring|outline)(?:-|$)/;
|
|
439
|
+
var TW_VARIANT = /^(?:sm|md|lg|xl|2xl|hover|focus|focus-visible|focus-within|active|visited|disabled|dark|group|group-hover|peer|first|last|odd|even|motion-safe|motion-reduce|print|rtl|ltr)::?|^(?:sm|md|lg|xl|2xl|hover|focus|active|disabled|dark|group-hover):/;
|
|
440
|
+
function stripVariants(cls) {
|
|
441
|
+
let c = cls;
|
|
442
|
+
while (true) {
|
|
443
|
+
const next = c.replace(/^[a-z0-9-]+:/, "");
|
|
444
|
+
if (next === c) break;
|
|
445
|
+
c = next;
|
|
446
|
+
}
|
|
447
|
+
return c;
|
|
448
|
+
}
|
|
449
|
+
var CSS_MODULE = /(?:^|_)[A-Za-z0-9-]+__[A-Za-z0-9_-]{4,}$|^[A-Za-z][\w-]*_[A-Za-z0-9]{4,}$/;
|
|
450
|
+
var STYLED = /^sc-[A-Za-z0-9]+$|^css-[a-z0-9]+(?:-[A-Za-z0-9]+)?$|-emotion-/;
|
|
451
|
+
function detectMechanism(el, authoredInlineLength) {
|
|
452
|
+
const classes = Array.from(el.classList);
|
|
453
|
+
const evidence = [];
|
|
454
|
+
const twClasses = classes.filter((c) => {
|
|
455
|
+
const bare = stripVariants(c);
|
|
456
|
+
return TW_UTILITY.test(bare) || TW_VARIANT.test(c) && TW_UTILITY.test(bare);
|
|
457
|
+
});
|
|
458
|
+
let hasTwVars = false;
|
|
459
|
+
try {
|
|
460
|
+
const cs = getComputedStyle(el);
|
|
461
|
+
for (let i = 0; i < cs.length; i++) {
|
|
462
|
+
if (cs[i].startsWith("--tw-")) {
|
|
463
|
+
hasTwVars = true;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
if (twClasses.length) {
|
|
470
|
+
evidence.push(`utility classes: ${twClasses.join(" ")}`);
|
|
471
|
+
return mk("tailwind", evidence, twClasses);
|
|
472
|
+
}
|
|
473
|
+
if (hasTwVars) {
|
|
474
|
+
evidence.push("--tw-* custom properties present");
|
|
475
|
+
return mk("tailwind", evidence, classes);
|
|
476
|
+
}
|
|
477
|
+
const moduleClasses = classes.filter((c) => CSS_MODULE.test(c));
|
|
478
|
+
if (moduleClasses.length) {
|
|
479
|
+
evidence.push(`hashed CSS-module classes: ${moduleClasses.join(" ")}`);
|
|
480
|
+
return mk("css-modules", evidence, moduleClasses);
|
|
481
|
+
}
|
|
482
|
+
const styledClasses = classes.filter((c) => STYLED.test(c));
|
|
483
|
+
const hasStyledTag = typeof document !== "undefined" && !!document.querySelector("style[data-styled], style[data-emotion]");
|
|
484
|
+
if (styledClasses.length || hasStyledTag) {
|
|
485
|
+
if (styledClasses.length)
|
|
486
|
+
evidence.push(`styled/emotion classes: ${styledClasses.join(" ")}`);
|
|
487
|
+
else evidence.push("styled-components/emotion <style> tag present");
|
|
488
|
+
return mk("styled-components", evidence, styledClasses);
|
|
489
|
+
}
|
|
490
|
+
if (authoredInlineLength > 0) {
|
|
491
|
+
evidence.push("authored inline style attribute");
|
|
492
|
+
return mk("inline", evidence, []);
|
|
493
|
+
}
|
|
494
|
+
if (classes.length) {
|
|
495
|
+
evidence.push(`plain CSS classes: ${classes.join(" ")}`);
|
|
496
|
+
return mk("plain-css", evidence, classes);
|
|
497
|
+
}
|
|
498
|
+
evidence.push("no classes, no inline style \u2014 styled by tag/ancestor selector");
|
|
499
|
+
return mk("unknown", evidence, []);
|
|
500
|
+
}
|
|
501
|
+
function mk(primaryMechanism, evidence, sourceClassNames) {
|
|
502
|
+
return { primaryMechanism, evidence, sourceClassNames };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/overlay/prompt.ts
|
|
506
|
+
function collapseChanges(changes) {
|
|
507
|
+
const byBp = groupByBreakpoint(changes);
|
|
508
|
+
const out = [];
|
|
509
|
+
for (const bp of Object.keys(byBp)) {
|
|
510
|
+
const group = byBp[bp];
|
|
511
|
+
const used = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const set of SHORTHAND_SETS) {
|
|
513
|
+
const parts = set.parts.map((p) => group.find((c) => c.property === p));
|
|
514
|
+
if (parts.every((p) => !!p)) {
|
|
515
|
+
const after = parts[0].after.computed;
|
|
516
|
+
if (parts.every((p) => p.after.computed === after)) {
|
|
517
|
+
parts.forEach((p) => used.add(p));
|
|
518
|
+
const beforeAllEqual = parts.every((p) => p.before.computed === parts[0].before.computed);
|
|
519
|
+
out.push({
|
|
520
|
+
property: set.shorthand,
|
|
521
|
+
before: { computed: beforeAllEqual ? parts[0].before.computed : "mixed" },
|
|
522
|
+
after: { computed: after, token: suggestUtility(set.shorthand, after) },
|
|
523
|
+
breakpoint: parts[0].breakpoint,
|
|
524
|
+
breakpointPx: parts[0].breakpointPx,
|
|
525
|
+
state: null
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const c of group) if (!used.has(c)) out.push(c);
|
|
531
|
+
}
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
function groupByBreakpoint(changes) {
|
|
535
|
+
const out = {};
|
|
536
|
+
for (const c of changes) (out[c.breakpoint] ??= []).push(c);
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
function locationLabel(r) {
|
|
540
|
+
const s = r.identity.source;
|
|
541
|
+
if (s.confidence !== "none") return `${s.file}:${s.line}:${s.column}`;
|
|
542
|
+
if (r.identity.componentName) return `component <${r.identity.componentName}>`;
|
|
543
|
+
return r.identity.selector;
|
|
544
|
+
}
|
|
545
|
+
function variant(breakpoint, token) {
|
|
546
|
+
return breakpoint === "base" ? token : `${breakpoint}:${token}`;
|
|
547
|
+
}
|
|
548
|
+
function renderPrompt(records) {
|
|
549
|
+
const active = records.filter((r) => r.changes.length > 0);
|
|
550
|
+
const total = active.reduce((n, r) => n + collapseChanges(r.changes).length, 0);
|
|
551
|
+
const lines = [];
|
|
552
|
+
lines.push(
|
|
553
|
+
`# uivisor \u2014 apply these UI tweaks (${total} change${total !== 1 ? "s" : ""} across ${active.length} element${active.length !== 1 ? "s" : ""})`
|
|
554
|
+
);
|
|
555
|
+
lines.push("");
|
|
556
|
+
lines.push(
|
|
557
|
+
"These are visual tweaks I made in the running app. Apply them to the source. Do not change anything else."
|
|
558
|
+
);
|
|
559
|
+
lines.push("");
|
|
560
|
+
let anyClassTarget = false;
|
|
561
|
+
let anyNewClass = false;
|
|
562
|
+
active.forEach((r, i) => {
|
|
563
|
+
const id = r.identity;
|
|
564
|
+
const newClass = r.target?.startsWith("new:") ? r.target.slice(4) : null;
|
|
565
|
+
const allTarget = !newClass && r.target === "all";
|
|
566
|
+
const classTarget = !newClass && !allTarget && r.target && r.target !== "element" ? r.target : null;
|
|
567
|
+
if (classTarget) anyClassTarget = true;
|
|
568
|
+
if (newClass) anyNewClass = true;
|
|
569
|
+
const label = newClass ? `<${id.tagName}> \u2192 new class .${newClass}` : allTarget ? `<${id.tagName}> \xD7${id.instanceCount} (all like this)` : classTarget ? `<${id.tagName}> .${classTarget}` : `<${id.tagName}>` + (id.textSnippet ? ` "${id.textSnippet}"` : "");
|
|
570
|
+
lines.push(`## ${i + 1}. ${label} \u2014 ${locationLabel(r)}`);
|
|
571
|
+
const where = () => {
|
|
572
|
+
const w = [];
|
|
573
|
+
if (id.componentName) w.push(`component <${id.componentName}>`);
|
|
574
|
+
w.push(`occurrence at ${locationLabel(r)}`);
|
|
575
|
+
if (id.textSnippet) w.push(`text "${id.textSnippet}"`);
|
|
576
|
+
return w.join(", ");
|
|
577
|
+
};
|
|
578
|
+
if (allTarget) {
|
|
579
|
+
lines.push(
|
|
580
|
+
`- TARGET: this element is rendered ${id.instanceCount}\xD7 \u2014 it and its ${id.instanceCount - 1} sibling(s) are the same component/markup. Apply the change to ALL of them at once by editing the shared source/component (or adding one shared class) \u2014 do NOT pin it to a single positional nth-child. You decide whether editing the component or adding a shared class is cleaner.`
|
|
581
|
+
);
|
|
582
|
+
lines.push(`- Find them via: ${where()} (the source above is rendered ${id.instanceCount}\xD7)`);
|
|
583
|
+
} else if (newClass) {
|
|
584
|
+
lines.push(
|
|
585
|
+
`- TARGET: create a NEW class \`.${newClass}\` with the styles below and add it to this element. Keep the existing classes as-is \u2014 do NOT pile more utilities onto them. (Tailwind: define it via \`@layer components { .${newClass} { @apply \u2026 } }\` or a CSS rule; reuse \`.${newClass}\` on similar elements.)`
|
|
586
|
+
);
|
|
587
|
+
lines.push(`- Add it on: ${where()}`);
|
|
588
|
+
} else if (classTarget) {
|
|
589
|
+
lines.push(
|
|
590
|
+
`- TARGET: the \`.${classTarget}\` style shared by ALL elements with this class. Edit that shared definition (the Tailwind utility, CSS-module/CSS rule, or styled-component) \u2014 NOT just this one node, and NOT via the positional selector.`
|
|
591
|
+
);
|
|
592
|
+
lines.push(`- Find it via: ${where()}`);
|
|
593
|
+
} else {
|
|
594
|
+
if (id.instanceCount > 1) {
|
|
595
|
+
lines.push(
|
|
596
|
+
`- TARGET: THIS instance only. It's rendered ${id.instanceCount}\xD7 from the same source, so editing the shared source would change all ${id.instanceCount}. Scope the change to just this one (a prop/condition or a distinguishing class).`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const anchors = [];
|
|
600
|
+
if (id.componentName) anchors.push(`component <${id.componentName}>`);
|
|
601
|
+
if (id.dataTestId) anchors.push(`data-testid="${id.dataTestId}"`);
|
|
602
|
+
if (id.id) anchors.push(`#${id.id}`);
|
|
603
|
+
if (id.selector) anchors.push(`selector \`${id.selector}\``);
|
|
604
|
+
if (anchors.length) lines.push(`- Identify by: ${anchors.join(", ")}`);
|
|
605
|
+
}
|
|
606
|
+
const styleLine = `- Styling: ${r.styling.primaryMechanism}` + (r.styling.sourceClassNames.length ? ` (current classes: \`${r.styling.sourceClassNames.join(" ")}\`)` : "");
|
|
607
|
+
lines.push(styleLine);
|
|
608
|
+
const byBp = groupByBreakpoint(collapseChanges(r.changes));
|
|
609
|
+
for (const bp of Object.keys(byBp)) {
|
|
610
|
+
const scope = bp === "base" ? "all sizes (base)" : `${bp} breakpoint (\u2265${byBp[bp][0].breakpointPx}px)`;
|
|
611
|
+
lines.push(`- At ${scope}:`);
|
|
612
|
+
for (const c of byBp[bp]) {
|
|
613
|
+
const suggestion = c.after.token ? ` \u2192 \`${variant(bp, c.after.token)}\`` : "";
|
|
614
|
+
lines.push(
|
|
615
|
+
` - ${c.property}: ${c.before.computed} \u2192 ${c.after.computed}${suggestion}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
lines.push("");
|
|
620
|
+
});
|
|
621
|
+
lines.push("### Rules");
|
|
622
|
+
lines.push(
|
|
623
|
+
"- Edit the EXISTING className/styles of each element. Do NOT add inline styles and do NOT duplicate the component."
|
|
624
|
+
);
|
|
625
|
+
lines.push(
|
|
626
|
+
"- Scope every change to its breakpoint with a responsive variant (e.g. `lg:`) so other screen sizes stay unchanged."
|
|
627
|
+
);
|
|
628
|
+
lines.push(
|
|
629
|
+
"- For non-Tailwind styling, edit the matching CSS-module / styled-component / CSS rule instead of the suggested utility, keeping the same breakpoint scope."
|
|
630
|
+
);
|
|
631
|
+
lines.push("- Confirm each element by its source location, text and data-testid before editing.");
|
|
632
|
+
if (anyClassTarget) {
|
|
633
|
+
lines.push(
|
|
634
|
+
"- For class-targeted edits, change the shared style once so every element using that class updates \u2014 do not hardcode it onto a single positional node."
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (anyNewClass) {
|
|
638
|
+
lines.push(
|
|
639
|
+
"- When creating a new class, leave the current classes untouched and add the new class alongside them; pick the styling mechanism the project already uses."
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return lines.join("\n");
|
|
643
|
+
}
|
|
644
|
+
function renderSpec(records, meta) {
|
|
645
|
+
return {
|
|
646
|
+
specVersion: "1.0",
|
|
647
|
+
capturedAt: meta.now,
|
|
648
|
+
url: meta.url,
|
|
649
|
+
viewport: { width: meta.width, height: meta.height, dpr: meta.dpr },
|
|
650
|
+
records: records.filter((r) => r.changes.length > 0)
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/overlay/source.ts
|
|
655
|
+
function parseSourceAttr(raw) {
|
|
656
|
+
if (!raw) return null;
|
|
657
|
+
const m = /^(.*):(\d+):(\d+)$/.exec(raw);
|
|
658
|
+
if (!m) return null;
|
|
659
|
+
return { file: m[1], line: Number(m[2]), column: Number(m[3]) };
|
|
660
|
+
}
|
|
661
|
+
function readBuildSource(el) {
|
|
662
|
+
const holder = el.closest("[data-uiv-src]");
|
|
663
|
+
if (!holder) return null;
|
|
664
|
+
return parseSourceAttr(holder.getAttribute("data-uiv-src"));
|
|
665
|
+
}
|
|
666
|
+
function getFiber(el) {
|
|
667
|
+
const key = Object.keys(el).find(
|
|
668
|
+
(k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
|
|
669
|
+
);
|
|
670
|
+
return key ? el[key] : null;
|
|
671
|
+
}
|
|
672
|
+
var INTERNAL_COMPONENTS = /* @__PURE__ */ new Set([
|
|
673
|
+
"ScrollAndFocusHandler",
|
|
674
|
+
"RenderFromTemplateContext",
|
|
675
|
+
"ClientPageRoot",
|
|
676
|
+
"ClientSegmentRoot",
|
|
677
|
+
"StaticGenerationSearchParamsBailoutProvider",
|
|
678
|
+
"ViewTransition",
|
|
679
|
+
"ReactDevOverlay",
|
|
680
|
+
"HotReload",
|
|
681
|
+
"AppDevOverlay",
|
|
682
|
+
"DevRootHTTPAccessFallbackBoundary",
|
|
683
|
+
"PathnameContextProviderAdapter"
|
|
684
|
+
]);
|
|
685
|
+
function isInternalName(name) {
|
|
686
|
+
return INTERNAL_COMPONENTS.has(name) || /(?:Boundary|Router)$/.test(name) || name.startsWith("_");
|
|
687
|
+
}
|
|
688
|
+
function fiberName(t) {
|
|
689
|
+
if (typeof t === "function") return t.displayName || t.name || null;
|
|
690
|
+
if (t && typeof t === "object") {
|
|
691
|
+
const inner = t.type || t.render;
|
|
692
|
+
if (typeof inner === "function") return t.displayName || inner.displayName || inner.name || null;
|
|
693
|
+
if (typeof t.displayName === "string") return t.displayName;
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
function readComponentName(el) {
|
|
698
|
+
let fiber = getFiber(el);
|
|
699
|
+
let guard = 0;
|
|
700
|
+
while (fiber && guard++ < 80) {
|
|
701
|
+
const name = fiberName(fiber.type);
|
|
702
|
+
if (name && !isInternalName(name)) return name;
|
|
703
|
+
fiber = fiber.return;
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
function buildSelector(el) {
|
|
708
|
+
const parts = [];
|
|
709
|
+
let node = el;
|
|
710
|
+
let depth = 0;
|
|
711
|
+
while (node && node.nodeType === 1 && depth++ < 6) {
|
|
712
|
+
let part = node.tagName.toLowerCase();
|
|
713
|
+
if (node.id) {
|
|
714
|
+
parts.unshift(`${part}#${cssEscape(node.id)}`);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
const testid = node.getAttribute("data-testid");
|
|
718
|
+
if (testid) {
|
|
719
|
+
parts.unshift(`${part}[data-testid="${testid}"]`);
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
const parent = node.parentElement;
|
|
723
|
+
if (parent) {
|
|
724
|
+
const sameTag = Array.from(parent.children).filter(
|
|
725
|
+
(c) => c.tagName === node.tagName
|
|
726
|
+
);
|
|
727
|
+
if (sameTag.length > 1) {
|
|
728
|
+
part += `:nth-of-type(${sameTag.indexOf(node) + 1})`;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
parts.unshift(part);
|
|
732
|
+
node = node.parentElement;
|
|
733
|
+
}
|
|
734
|
+
return parts.join(" > ");
|
|
735
|
+
}
|
|
736
|
+
function cssEscape(s) {
|
|
737
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
|
|
738
|
+
}
|
|
739
|
+
function componentFromFile(file) {
|
|
740
|
+
const base = file.split(/[\\/]/).pop() || file;
|
|
741
|
+
const name = base.replace(/\.[jt]sx?$/, "");
|
|
742
|
+
if (/^(page|layout|route|template|loading|error|not-found|index)$/.test(name)) return null;
|
|
743
|
+
return name;
|
|
744
|
+
}
|
|
745
|
+
function countInstances(el) {
|
|
746
|
+
try {
|
|
747
|
+
const doc = el.ownerDocument;
|
|
748
|
+
const holder = el.closest("[data-uiv-src]");
|
|
749
|
+
const raw = holder?.getAttribute("data-uiv-src");
|
|
750
|
+
if (raw) {
|
|
751
|
+
let n2 = 0;
|
|
752
|
+
doc.querySelectorAll("[data-uiv-src]").forEach((e) => {
|
|
753
|
+
if (e.getAttribute("data-uiv-src") === raw) n2++;
|
|
754
|
+
});
|
|
755
|
+
if (n2 > 0) return n2;
|
|
756
|
+
}
|
|
757
|
+
const sig = (e) => `${e.tagName}|${Array.from(e.classList).sort().join(" ")}`;
|
|
758
|
+
const mine = sig(el);
|
|
759
|
+
if (!el.classList.length) return 1;
|
|
760
|
+
let n = 0;
|
|
761
|
+
doc.querySelectorAll(el.tagName).forEach((e) => {
|
|
762
|
+
if (sig(e) === mine) n++;
|
|
763
|
+
});
|
|
764
|
+
return Math.max(1, n);
|
|
765
|
+
} catch {
|
|
766
|
+
return 1;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function getIdentity(el) {
|
|
770
|
+
const build = readBuildSource(el);
|
|
771
|
+
const source = build ? { ...build, confidence: "build-attr" } : { confidence: "none" };
|
|
772
|
+
return {
|
|
773
|
+
// With an exact file:line, derive the name from the file (reliable, RSC-safe);
|
|
774
|
+
// only walk the fiber tree when there is no build-time source.
|
|
775
|
+
componentName: build ? componentFromFile(build.file) : readComponentName(el),
|
|
776
|
+
source,
|
|
777
|
+
selector: buildSelector(el),
|
|
778
|
+
dataTestId: el.getAttribute("data-testid"),
|
|
779
|
+
id: el.id || null,
|
|
780
|
+
tagName: el.tagName.toLowerCase(),
|
|
781
|
+
role: el.getAttribute("role"),
|
|
782
|
+
textSnippet: (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 40),
|
|
783
|
+
classList: Array.from(el.classList).filter((c) => c !== "uiv-selected"),
|
|
784
|
+
instanceCount: countInstances(el)
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/overlay/ui.css.ts
|
|
789
|
+
var CSS = (
|
|
790
|
+
/* css */
|
|
791
|
+
`
|
|
792
|
+
:host { all: initial; }
|
|
793
|
+
* { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
|
794
|
+
|
|
795
|
+
.uiv-box {
|
|
796
|
+
position: fixed; pointer-events: none; z-index: 2147483646;
|
|
797
|
+
border: 1px solid #6366f1; border-radius: 2px;
|
|
798
|
+
background: rgba(99,102,241,0.12); display: none;
|
|
799
|
+
}
|
|
800
|
+
.uiv-box.sel { border: 1.5px solid #22d3ee; background: rgba(34,211,238,0.10); }
|
|
801
|
+
.uiv-tag {
|
|
802
|
+
position: fixed; pointer-events: none; z-index: 2147483647;
|
|
803
|
+
background: #6366f1; color: #fff; font-size: 11px; line-height: 1;
|
|
804
|
+
padding: 3px 6px; border-radius: 3px; white-space: nowrap; display: none;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* ---- responsive (virtual screen) ---- */
|
|
808
|
+
.uiv-framewrap {
|
|
809
|
+
position: fixed; inset: 0; z-index: 2147483640; display: none;
|
|
810
|
+
background: #0a0a0bf2; flex-direction: column;
|
|
811
|
+
}
|
|
812
|
+
.uiv-framewrap.show { display: flex; }
|
|
813
|
+
.uiv-framebar {
|
|
814
|
+
display: flex; align-items: center; justify-content: center; gap: 14px;
|
|
815
|
+
height: 38px; color: #e4e4e7; font-size: 12px; flex: 0 0 auto;
|
|
816
|
+
border-bottom: 1px solid #27272a;
|
|
817
|
+
}
|
|
818
|
+
.uiv-framew { font-family: ui-monospace, monospace; color: #c7d2fe; font-weight: 600; }
|
|
819
|
+
.uiv-framex { cursor: pointer; color: #a1a1aa; }
|
|
820
|
+
.uiv-framex:hover { color: #fff; }
|
|
821
|
+
.uiv-framestage {
|
|
822
|
+
flex: 1; display: flex; align-items: stretch; justify-content: center;
|
|
823
|
+
padding: 16px; overflow: auto;
|
|
824
|
+
}
|
|
825
|
+
.uiv-framehost { position: relative; width: 768px; flex: 0 0 auto; }
|
|
826
|
+
.uiv-frame {
|
|
827
|
+
width: 100%; height: 100%; border: 0; background: #fff;
|
|
828
|
+
border-radius: 8px; box-shadow: 0 8px 40px rgba(0,0,0,0.5);
|
|
829
|
+
}
|
|
830
|
+
.uiv-framehandle {
|
|
831
|
+
position: absolute; top: 0; right: -7px; width: 14px; height: 100%;
|
|
832
|
+
cursor: ew-resize; display: flex; align-items: center; justify-content: center;
|
|
833
|
+
}
|
|
834
|
+
.uiv-framehandle::after {
|
|
835
|
+
content: ''; width: 4px; height: 44px; border-radius: 4px; background: #52525b;
|
|
836
|
+
}
|
|
837
|
+
.uiv-framehandle:hover::after { background: #818cf8; }
|
|
838
|
+
|
|
839
|
+
.uiv-fab {
|
|
840
|
+
position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
|
|
841
|
+
width: 44px; height: 44px; border-radius: 50%; cursor: pointer;
|
|
842
|
+
background: #18181b; color: #a5b4fc; border: 1px solid #3f3f46;
|
|
843
|
+
display: flex; align-items: center; justify-content: center;
|
|
844
|
+
font-size: 18px; box-shadow: 0 6px 24px rgba(0,0,0,0.35); user-select: none;
|
|
845
|
+
}
|
|
846
|
+
.uiv-fab.on { background: #4f46e5; color: #fff; border-color: #6366f1; }
|
|
847
|
+
|
|
848
|
+
.uiv-panel {
|
|
849
|
+
position: fixed; right: 16px; bottom: 72px; z-index: 2147483647;
|
|
850
|
+
width: 328px; max-height: 80vh; overflow: auto;
|
|
851
|
+
background: #18181b; color: #e4e4e7; border: 1px solid #3f3f46;
|
|
852
|
+
border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,0.45);
|
|
853
|
+
font-size: 12px; display: none;
|
|
854
|
+
}
|
|
855
|
+
.uiv-panel.show { display: block; }
|
|
856
|
+
|
|
857
|
+
.uiv-head {
|
|
858
|
+
display: flex; align-items: center; gap: 8px; padding: 10px 12px;
|
|
859
|
+
border-bottom: 1px solid #27272a; position: sticky; top: 0; background: #18181b;
|
|
860
|
+
}
|
|
861
|
+
.uiv-head b { font-size: 13px; color: #fff; letter-spacing: .3px; }
|
|
862
|
+
.uiv-bp { margin-left: auto; font-size: 10px; padding: 2px 7px; border-radius: 999px;
|
|
863
|
+
background: #312e81; color: #c7d2fe; font-weight: 600; }
|
|
864
|
+
.uiv-x { cursor: pointer; color: #71717a; padding: 2px 4px; }
|
|
865
|
+
.uiv-x:hover { color: #fff; }
|
|
866
|
+
|
|
867
|
+
.uiv-sec { padding: 10px 12px; border-bottom: 1px solid #27272a; }
|
|
868
|
+
.uiv-empty { color: #71717a; padding: 18px 12px; text-align: center; }
|
|
869
|
+
|
|
870
|
+
.uiv-meta { line-height: 1.5; }
|
|
871
|
+
.uiv-meta .uiv-el { color: #67e8f9; font-weight: 600; }
|
|
872
|
+
.uiv-meta .uiv-src { color: #a1a1aa; word-break: break-all; }
|
|
873
|
+
.uiv-meta .uiv-mech { display: inline-block; margin-top: 4px; font-size: 10px;
|
|
874
|
+
padding: 1px 6px; border-radius: 4px; background: #27272a; color: #d4d4d8; }
|
|
875
|
+
|
|
876
|
+
/* ---- breakpoint scope + class target chips ---- */
|
|
877
|
+
.uiv-chips { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
878
|
+
.uiv-chip, .uiv-clschip {
|
|
879
|
+
cursor: pointer; border: 1px solid #3f3f46; background: #27272a; color: #a1a1aa;
|
|
880
|
+
border-radius: 6px; padding: 3px 8px; font-size: 11px; font-weight: 600;
|
|
881
|
+
font-family: ui-monospace, monospace;
|
|
882
|
+
}
|
|
883
|
+
.uiv-chip:hover, .uiv-clschip:hover { color: #fff; background: #3f3f46; }
|
|
884
|
+
.uiv-chip.win { border-color: #52525b; color: #d4d4d8; }
|
|
885
|
+
.uiv-chip.on, .uiv-clschip.on { background: #4f46e5; border-color: #6366f1; color: #fff; }
|
|
886
|
+
.uiv-bphint { margin-top: 7px; font-size: 10px; color: #71717a; line-height: 1.4; }
|
|
887
|
+
.uiv-bphint b { color: #c7d2fe; }
|
|
888
|
+
.uiv-newclass {
|
|
889
|
+
border: 1px dashed #52525b; background: transparent; color: #a1a1aa;
|
|
890
|
+
border-radius: 6px; padding: 3px 8px; font-size: 11px; width: 104px; outline: none;
|
|
891
|
+
font-family: ui-monospace, monospace;
|
|
892
|
+
}
|
|
893
|
+
.uiv-newclass::placeholder { color: #71717a; }
|
|
894
|
+
.uiv-newclass:focus { border-style: solid; border-color: #6366f1; color: #fff; }
|
|
895
|
+
.uiv-newclass.on { border-style: solid; border-color: #22d3ee; color: #fff; }
|
|
896
|
+
|
|
897
|
+
/* ---- Figma-like sectioned controls ---- */
|
|
898
|
+
.uiv-sectitle { margin: 0 0 8px; font-size: 10px; text-transform: uppercase;
|
|
899
|
+
letter-spacing: .5px; color: #8b8b94; font-weight: 600; }
|
|
900
|
+
.uiv-sec + .uiv-sec .uiv-sectitle { margin-top: 0; }
|
|
901
|
+
|
|
902
|
+
.uiv-ctl { display: grid; grid-template-columns: 70px 1fr 26px; gap: 8px;
|
|
903
|
+
align-items: center; margin-bottom: 7px; }
|
|
904
|
+
.uiv-ctl:last-child { margin-bottom: 0; }
|
|
905
|
+
.uiv-ctl > .clabel { font-size: 11px; color: #a1a1aa; }
|
|
906
|
+
.uiv-ctl > .cfield { min-width: 0; }
|
|
907
|
+
|
|
908
|
+
/* numeric field with a scrub handle on the left */
|
|
909
|
+
.uiv-num { display: flex; align-items: stretch; background: #27272a;
|
|
910
|
+
border: 1px solid #3f3f46; border-radius: 7px; overflow: hidden; }
|
|
911
|
+
.uiv-num.changed { border-color: #22d3ee; }
|
|
912
|
+
.uiv-num:focus-within { border-color: #6366f1; }
|
|
913
|
+
.uiv-scrub { display: flex; align-items: center; justify-content: center;
|
|
914
|
+
width: 24px; color: #8b8b94; cursor: ew-resize; user-select: none;
|
|
915
|
+
flex: 0 0 auto; touch-action: none; }
|
|
916
|
+
.uiv-scrub:hover { color: #c7d2fe; background: #323238; }
|
|
917
|
+
.uiv-scrub.txt { font-size: 10px; font-weight: 600; }
|
|
918
|
+
.uiv-num input { flex: 1; min-width: 0; background: transparent; border: none;
|
|
919
|
+
color: #fff; padding: 5px 7px 5px 2px; font-size: 12px; outline: none;
|
|
920
|
+
-moz-appearance: textfield; }
|
|
921
|
+
.uiv-num input::-webkit-outer-spin-button,
|
|
922
|
+
.uiv-num input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
923
|
+
.uiv-num input::placeholder { color: #6b6b73; }
|
|
924
|
+
|
|
925
|
+
/* unit selector inside a dim field */
|
|
926
|
+
.uiv-unit { flex: 0 0 auto; width: auto; max-width: 52px; background: #323238; border: none;
|
|
927
|
+
border-left: 1px solid #3f3f46; color: #a1a1aa; font-size: 11px;
|
|
928
|
+
padding: 0 3px; outline: none; cursor: pointer; }
|
|
929
|
+
.uiv-unit:hover { color: #fff; }
|
|
930
|
+
|
|
931
|
+
.uiv-expand { display: flex; align-items: center; justify-content: center;
|
|
932
|
+
width: 26px; height: 28px; border-radius: 7px; cursor: pointer;
|
|
933
|
+
background: #27272a; border: 1px solid #3f3f46; color: #8b8b94; }
|
|
934
|
+
.uiv-expand:hover { color: #fff; background: #3f3f46; }
|
|
935
|
+
.uiv-expand.on { color: #c7d2fe; border-color: #4f46e5; background: #312e81; }
|
|
936
|
+
|
|
937
|
+
.uiv-sides { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(4, 1fr);
|
|
938
|
+
gap: 6px; margin: -1px 0 8px; }
|
|
939
|
+
|
|
940
|
+
/* Weight dropdown only \u2014 must NOT match the unit <select> inside dim fields. */
|
|
941
|
+
.uiv-ctl select.uiv-sel {
|
|
942
|
+
width: 100%; background: #27272a; border: 1px solid #3f3f46; color: #fff;
|
|
943
|
+
border-radius: 7px; padding: 6px 7px; font-size: 12px; outline: none;
|
|
944
|
+
}
|
|
945
|
+
.uiv-ctl select.uiv-sel:focus { border-color: #6366f1; }
|
|
946
|
+
.uiv-ctl input[type=color] { width: 100%; height: 28px; padding: 2px; cursor: pointer;
|
|
947
|
+
background: #27272a; border: 1px solid #3f3f46; border-radius: 7px; }
|
|
948
|
+
.uiv-ctl input.uiv-text { width: 100%; background: #27272a; border: 1px solid #3f3f46;
|
|
949
|
+
color: #fff; border-radius: 7px; padding: 6px 7px; font-size: 12px; outline: none; }
|
|
950
|
+
.uiv-ctl input.uiv-text:focus { border-color: #6366f1; }
|
|
951
|
+
.uiv-ctl input.uiv-text.changed { border-color: #22d3ee; }
|
|
952
|
+
|
|
953
|
+
.uiv-journal { display: flex; flex-direction: column; gap: 8px; }
|
|
954
|
+
.uiv-jitem { background: #1f1f23; border: 1px solid #27272a; border-radius: 8px; padding: 8px; }
|
|
955
|
+
.uiv-jitem .jhead { display: flex; gap: 6px; align-items: baseline; }
|
|
956
|
+
.uiv-jitem .jel { color: #67e8f9; font-weight: 600; }
|
|
957
|
+
.uiv-jitem .jloc { color: #71717a; font-size: 10px; margin-left: auto; word-break: break-all; }
|
|
958
|
+
.uiv-jchg { color: #d4d4d8; margin-top: 3px; font-family: ui-monospace, monospace; font-size: 11px; }
|
|
959
|
+
.uiv-jchg .bp { color: #818cf8; }
|
|
960
|
+
.uiv-jchg .tok { color: #4ade80; }
|
|
961
|
+
|
|
962
|
+
.uiv-foot { display: flex; gap: 6px; padding: 10px 12px; position: sticky; bottom: 0;
|
|
963
|
+
background: #18181b; border-top: 1px solid #27272a; flex-wrap: wrap; }
|
|
964
|
+
.uiv-btn { flex: 1; cursor: pointer; border: 1px solid #3f3f46; background: #27272a;
|
|
965
|
+
color: #e4e4e7; border-radius: 7px; padding: 7px 8px; font-size: 11px; font-weight: 600;
|
|
966
|
+
white-space: nowrap; }
|
|
967
|
+
.uiv-btn:hover { background: #3f3f46; }
|
|
968
|
+
.uiv-btn.primary { background: #4f46e5; border-color: #6366f1; color: #fff; flex-basis: 100%; }
|
|
969
|
+
.uiv-btn.primary:hover { background: #4338ca; }
|
|
970
|
+
.uiv-btn.ghost { flex: 0 0 auto; }
|
|
971
|
+
.uiv-toast { position: fixed; right: 16px; bottom: 128px; z-index: 2147483647;
|
|
972
|
+
background: #22c55e; color: #052e16; padding: 8px 12px; border-radius: 8px;
|
|
973
|
+
font-size: 12px; font-weight: 600; display: none; }
|
|
974
|
+
.uiv-toast.show { display: block; }
|
|
975
|
+
.uiv-hint { font-size: 10px; color: #71717a; padding: 0 12px 10px; }
|
|
976
|
+
`
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// src/overlay/index.ts
|
|
980
|
+
var counter = 0;
|
|
981
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
982
|
+
var Uivisor = class {
|
|
983
|
+
constructor() {
|
|
984
|
+
this.enabled = false;
|
|
985
|
+
this.selected = null;
|
|
986
|
+
this.states = /* @__PURE__ */ new Map();
|
|
987
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
988
|
+
/** Cached project breakpoint system (detected from CSS), refreshed until found. */
|
|
989
|
+
this._bp = null;
|
|
990
|
+
// responsive (virtual screen) mode
|
|
991
|
+
this.responsive = false;
|
|
992
|
+
this.frameWidth = 768;
|
|
993
|
+
// ---- pointer handling ----
|
|
994
|
+
this.onMove = (e) => {
|
|
995
|
+
if (!this.enabled || this.isOurs(e)) {
|
|
996
|
+
this.hoverBox.style.display = "none";
|
|
997
|
+
this.tag.style.display = "none";
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const el = e.target;
|
|
1001
|
+
const d = this.doc();
|
|
1002
|
+
if (!el || el === d.documentElement || el === d.body) return;
|
|
1003
|
+
const r = el.getBoundingClientRect();
|
|
1004
|
+
const o = this.frameOffset();
|
|
1005
|
+
this.place(this.hoverBox, r);
|
|
1006
|
+
this.hoverBox.style.display = "block";
|
|
1007
|
+
this.tag.textContent = `${el.tagName.toLowerCase()} \xB7 ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
|
|
1008
|
+
this.tag.style.left = `${r.left + o.x}px`;
|
|
1009
|
+
this.tag.style.top = `${Math.max(0, r.top + o.y - 20)}px`;
|
|
1010
|
+
this.tag.style.display = "block";
|
|
1011
|
+
};
|
|
1012
|
+
this.onClick = (e) => {
|
|
1013
|
+
if (!this.enabled || this.isOurs(e)) return;
|
|
1014
|
+
e.preventDefault();
|
|
1015
|
+
e.stopPropagation();
|
|
1016
|
+
e.stopImmediatePropagation();
|
|
1017
|
+
this.select(e.target);
|
|
1018
|
+
};
|
|
1019
|
+
this.onKey = (e) => {
|
|
1020
|
+
if (e.altKey && (e.key === "u" || e.key === "U")) {
|
|
1021
|
+
e.preventDefault();
|
|
1022
|
+
this.toggle();
|
|
1023
|
+
} else if (e.key === "Escape" && this.enabled) {
|
|
1024
|
+
if (this.selected) this.select(null);
|
|
1025
|
+
else this.toggle(false);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
this.lastWinBp = "";
|
|
1029
|
+
this.onResize = () => {
|
|
1030
|
+
this.updateBp();
|
|
1031
|
+
this.reposition();
|
|
1032
|
+
const winBp = currentBreakpoint(this.bpSystem()).name;
|
|
1033
|
+
if (winBp !== this.lastWinBp) {
|
|
1034
|
+
this.lastWinBp = winBp;
|
|
1035
|
+
if (this.enabled && this.selected) this.renderBody();
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
this.reposition = () => {
|
|
1039
|
+
if (this.selected) {
|
|
1040
|
+
this.place(this.selBox, this.selected.getBoundingClientRect());
|
|
1041
|
+
this.selBox.style.display = "block";
|
|
1042
|
+
} else {
|
|
1043
|
+
this.selBox.style.display = "none";
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
/** Project breakpoints — re-detect until the stylesheets yield a real set. */
|
|
1048
|
+
bpSystem() {
|
|
1049
|
+
if (!this._bp || this._bp.name !== "detected") this._bp = detectBreakpoints();
|
|
1050
|
+
return this._bp;
|
|
1051
|
+
}
|
|
1052
|
+
mount() {
|
|
1053
|
+
this.host = document.createElement("div");
|
|
1054
|
+
this.host.id = "uivisor-root";
|
|
1055
|
+
this.host.setAttribute("data-uiv-ignore", "");
|
|
1056
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
1057
|
+
document.documentElement.appendChild(this.host);
|
|
1058
|
+
this.root.innerHTML = `
|
|
1059
|
+
<style>${CSS}</style>
|
|
1060
|
+
<div class="uiv-framewrap">
|
|
1061
|
+
<div class="uiv-framebar"><span class="uiv-framew">768px</span><span class="uiv-framex" title="Exit responsive">\u2715 exit</span></div>
|
|
1062
|
+
<div class="uiv-framestage">
|
|
1063
|
+
<div class="uiv-framehost">
|
|
1064
|
+
<iframe class="uiv-frame" data-uiv-frame="1"></iframe>
|
|
1065
|
+
<div class="uiv-framehandle" title="Drag to resize"></div>
|
|
1066
|
+
</div>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
<div class="uiv-box hover"></div>
|
|
1070
|
+
<div class="uiv-box sel"></div>
|
|
1071
|
+
<div class="uiv-tag"></div>
|
|
1072
|
+
<div class="uiv-fab" title="Toggle uivisor (Alt+U)">\u25CE</div>
|
|
1073
|
+
<div class="uiv-toast"></div>
|
|
1074
|
+
<div class="uiv-panel">
|
|
1075
|
+
<div class="uiv-head">
|
|
1076
|
+
<b>uivisor</b>
|
|
1077
|
+
<span class="uiv-bp">base</span>
|
|
1078
|
+
<span class="uiv-x" title="Close">\u2715</span>
|
|
1079
|
+
</div>
|
|
1080
|
+
<div class="uiv-body"></div>
|
|
1081
|
+
<div class="uiv-foot">
|
|
1082
|
+
<button class="uiv-btn primary copy-prompt">Copy prompt for agent</button>
|
|
1083
|
+
<button class="uiv-btn copy-json">Copy JSON</button>
|
|
1084
|
+
<button class="uiv-btn ghost reset" title="Revert tweaks on selected element">Reset</button>
|
|
1085
|
+
<button class="uiv-btn ghost clear" title="Clear all">Clear</button>
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
`;
|
|
1089
|
+
this.hoverBox = this.q(".uiv-box.hover");
|
|
1090
|
+
this.selBox = this.q(".uiv-box.sel");
|
|
1091
|
+
this.tag = this.q(".uiv-tag");
|
|
1092
|
+
this.fab = this.q(".uiv-fab");
|
|
1093
|
+
this.panel = this.q(".uiv-panel");
|
|
1094
|
+
this.bpBadge = this.q(".uiv-bp");
|
|
1095
|
+
this.toast = this.q(".uiv-toast");
|
|
1096
|
+
this.frameWrap = this.q(".uiv-framewrap");
|
|
1097
|
+
this.frame = this.q(".uiv-frame");
|
|
1098
|
+
this.fab.addEventListener("click", () => this.toggle());
|
|
1099
|
+
this.q(".uiv-x").addEventListener("click", () => this.toggle(false));
|
|
1100
|
+
this.q(".uiv-framex").addEventListener("click", () => this.toggleResponsive(false));
|
|
1101
|
+
this.q(".copy-prompt").addEventListener("click", () => this.copyPrompt());
|
|
1102
|
+
this.q(".copy-json").addEventListener("click", () => this.copyJSON());
|
|
1103
|
+
this.q(".reset").addEventListener("click", () => this.resetSelected());
|
|
1104
|
+
this.q(".clear").addEventListener("click", () => this.clearAll());
|
|
1105
|
+
this.bindFrameHandle();
|
|
1106
|
+
this.attachPicker(document);
|
|
1107
|
+
document.addEventListener("keydown", this.onKey, true);
|
|
1108
|
+
window.addEventListener("scroll", this.reposition, true);
|
|
1109
|
+
window.addEventListener("resize", this.onResize, true);
|
|
1110
|
+
this.renderBody();
|
|
1111
|
+
this.updateBp();
|
|
1112
|
+
}
|
|
1113
|
+
q(sel) {
|
|
1114
|
+
return this.root.querySelector(sel);
|
|
1115
|
+
}
|
|
1116
|
+
isOurs(e) {
|
|
1117
|
+
return e.composedPath().includes(this.host);
|
|
1118
|
+
}
|
|
1119
|
+
// ---- enable / disable ----
|
|
1120
|
+
toggle(force) {
|
|
1121
|
+
this.enabled = force ?? !this.enabled;
|
|
1122
|
+
this.fab.classList.toggle("on", this.enabled);
|
|
1123
|
+
this.panel.classList.toggle("show", this.enabled);
|
|
1124
|
+
if (!this.enabled) {
|
|
1125
|
+
this.hoverBox.style.display = "none";
|
|
1126
|
+
this.tag.style.display = "none";
|
|
1127
|
+
if (this.responsive) this.toggleResponsive(false);
|
|
1128
|
+
} else {
|
|
1129
|
+
this.scheduleBpRefresh();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/** Stylesheets (esp. JIT/CDN Tailwind) load async — re-detect breakpoints a few
|
|
1133
|
+
* times after enabling and re-render only if the set actually changed. */
|
|
1134
|
+
scheduleBpRefresh() {
|
|
1135
|
+
const key = (s) => s.name + ":" + s.breakpoints.map((b) => b.minWidth).join(",");
|
|
1136
|
+
const refresh = () => {
|
|
1137
|
+
if (!this.enabled) return;
|
|
1138
|
+
const prev = this._bp;
|
|
1139
|
+
this._bp = null;
|
|
1140
|
+
const next = this.bpSystem();
|
|
1141
|
+
if (!prev || key(prev) !== key(next)) this.renderBody();
|
|
1142
|
+
};
|
|
1143
|
+
for (const d of [250, 900, 2200]) window.setTimeout(refresh, d);
|
|
1144
|
+
}
|
|
1145
|
+
// ---- responsive (virtual screen) mode ----
|
|
1146
|
+
/** The document the inspector currently targets: the iframe in responsive mode. */
|
|
1147
|
+
doc() {
|
|
1148
|
+
if (this.responsive && this.frame.contentDocument) return this.frame.contentDocument;
|
|
1149
|
+
return document;
|
|
1150
|
+
}
|
|
1151
|
+
/** Offset of the iframe within the top viewport (for positioning highlight boxes). */
|
|
1152
|
+
frameOffset() {
|
|
1153
|
+
if (this.responsive) {
|
|
1154
|
+
const r = this.frame.getBoundingClientRect();
|
|
1155
|
+
return { x: r.left, y: r.top };
|
|
1156
|
+
}
|
|
1157
|
+
return { x: 0, y: 0 };
|
|
1158
|
+
}
|
|
1159
|
+
attachPicker(d) {
|
|
1160
|
+
d.addEventListener("mousemove", this.onMove, true);
|
|
1161
|
+
d.addEventListener("click", this.onClick, true);
|
|
1162
|
+
d.addEventListener("keydown", this.onKey, true);
|
|
1163
|
+
}
|
|
1164
|
+
detachPicker(d) {
|
|
1165
|
+
d.removeEventListener("mousemove", this.onMove, true);
|
|
1166
|
+
d.removeEventListener("click", this.onClick, true);
|
|
1167
|
+
d.removeEventListener("keydown", this.onKey, true);
|
|
1168
|
+
}
|
|
1169
|
+
toggleResponsive(force) {
|
|
1170
|
+
const next = force ?? !this.responsive;
|
|
1171
|
+
if (next === this.responsive) return;
|
|
1172
|
+
const prevId = this.selected ? this.st()?.record.identity ?? null : null;
|
|
1173
|
+
this.detachPicker(this.doc());
|
|
1174
|
+
this.selected = null;
|
|
1175
|
+
if (next) {
|
|
1176
|
+
this.responsive = true;
|
|
1177
|
+
this.frameWrap.classList.add("show");
|
|
1178
|
+
this.setFrameWidth(this.frameWidth);
|
|
1179
|
+
this.frame.onload = () => {
|
|
1180
|
+
const fd = this.frame.contentDocument;
|
|
1181
|
+
if (fd) {
|
|
1182
|
+
this.attachPicker(fd);
|
|
1183
|
+
this.reselect(prevId, fd);
|
|
1184
|
+
}
|
|
1185
|
+
this.reposition();
|
|
1186
|
+
};
|
|
1187
|
+
this.frame.src = location.href;
|
|
1188
|
+
} else {
|
|
1189
|
+
this.responsive = false;
|
|
1190
|
+
this.frameWrap.classList.remove("show");
|
|
1191
|
+
this.frame.onload = null;
|
|
1192
|
+
this.frame.src = "about:blank";
|
|
1193
|
+
this.attachPicker(document);
|
|
1194
|
+
this.reselect(prevId, document);
|
|
1195
|
+
}
|
|
1196
|
+
this.reposition();
|
|
1197
|
+
this.updateBp();
|
|
1198
|
+
this.renderBody();
|
|
1199
|
+
}
|
|
1200
|
+
/** Find and re-select the element matching a prior identity in the given doc. */
|
|
1201
|
+
reselect(id, doc) {
|
|
1202
|
+
if (!id) return;
|
|
1203
|
+
let el = null;
|
|
1204
|
+
if (id.dataTestId) el = doc.querySelector(`[data-testid="${cssAttrEscape(id.dataTestId)}"]`);
|
|
1205
|
+
if (!el && id.source.confidence !== "none") {
|
|
1206
|
+
const raw = `${id.source.file}:${id.source.line}:${id.source.column}`;
|
|
1207
|
+
el = [...doc.querySelectorAll("[data-uiv-src]")].find(
|
|
1208
|
+
(e) => e.getAttribute("data-uiv-src") === raw && (!id.textSnippet || (e.textContent || "").includes(id.textSnippet))
|
|
1209
|
+
) || [...doc.querySelectorAll("[data-uiv-src]")].find((e) => e.getAttribute("data-uiv-src") === raw) || null;
|
|
1210
|
+
}
|
|
1211
|
+
if (!el && id.selector) {
|
|
1212
|
+
try {
|
|
1213
|
+
el = doc.querySelector(id.selector);
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (el) this.select(el);
|
|
1218
|
+
}
|
|
1219
|
+
setFrameWidth(w) {
|
|
1220
|
+
this.frameWidth = Math.max(280, Math.min(2400, Math.round(w)));
|
|
1221
|
+
const host = this.q(".uiv-framehost");
|
|
1222
|
+
host.style.width = `${this.frameWidth}px`;
|
|
1223
|
+
const bp = activeBreakpoint(this.frameWidth, this.bpSystem()).name;
|
|
1224
|
+
this.q(".uiv-framew").textContent = `${this.frameWidth}px \xB7 ${bp}`;
|
|
1225
|
+
this.updateBp();
|
|
1226
|
+
this.reposition();
|
|
1227
|
+
}
|
|
1228
|
+
bindFrameHandle() {
|
|
1229
|
+
const handle = this.q(".uiv-framehandle");
|
|
1230
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
1231
|
+
e.preventDefault();
|
|
1232
|
+
const startX = e.clientX;
|
|
1233
|
+
const startW = this.frameWidth;
|
|
1234
|
+
try {
|
|
1235
|
+
handle.setPointerCapture(e.pointerId);
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
const move = (ev) => this.setFrameWidth(startW + (ev.clientX - startX) * 2);
|
|
1239
|
+
const up = () => {
|
|
1240
|
+
handle.removeEventListener("pointermove", move);
|
|
1241
|
+
handle.removeEventListener("pointerup", up);
|
|
1242
|
+
this.retagSelected();
|
|
1243
|
+
this.renderBody();
|
|
1244
|
+
};
|
|
1245
|
+
handle.addEventListener("pointermove", move);
|
|
1246
|
+
handle.addEventListener("pointerup", up);
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
place(box, r) {
|
|
1250
|
+
const o = this.frameOffset();
|
|
1251
|
+
box.style.left = `${r.left + o.x}px`;
|
|
1252
|
+
box.style.top = `${r.top + o.y}px`;
|
|
1253
|
+
box.style.width = `${r.width}px`;
|
|
1254
|
+
box.style.height = `${r.height}px`;
|
|
1255
|
+
}
|
|
1256
|
+
// ---- selection ----
|
|
1257
|
+
select(el) {
|
|
1258
|
+
this.selected = el;
|
|
1259
|
+
if (el && !this.states.has(el)) {
|
|
1260
|
+
const authoredInlineLen = (el.getAttribute("style") || "").length;
|
|
1261
|
+
const identity = getIdentity(el);
|
|
1262
|
+
const record = {
|
|
1263
|
+
id: `e${++counter}`,
|
|
1264
|
+
identity,
|
|
1265
|
+
styling: detectMechanism(el, authoredInlineLen),
|
|
1266
|
+
changes: [],
|
|
1267
|
+
// Repeated instances default to "all" — editing the shared source/component
|
|
1268
|
+
// updates every sibling, which is what people usually want.
|
|
1269
|
+
target: identity.instanceCount > 1 ? "all" : "element"
|
|
1270
|
+
};
|
|
1271
|
+
this.states.set(el, {
|
|
1272
|
+
record,
|
|
1273
|
+
original: snapshot(el, ALL_CSS),
|
|
1274
|
+
applied: /* @__PURE__ */ new Set(),
|
|
1275
|
+
dimUnit: {}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
this.reposition();
|
|
1279
|
+
this.renderBody();
|
|
1280
|
+
}
|
|
1281
|
+
// ---- value helpers ----
|
|
1282
|
+
st() {
|
|
1283
|
+
return this.selected ? this.states.get(this.selected) ?? null : null;
|
|
1284
|
+
}
|
|
1285
|
+
liveVal(css) {
|
|
1286
|
+
const el = this.selected;
|
|
1287
|
+
const st = this.st();
|
|
1288
|
+
if (!el || !st) return "";
|
|
1289
|
+
return el.style.getPropertyValue(css) || st.original[css] || "";
|
|
1290
|
+
}
|
|
1291
|
+
liveNum(css) {
|
|
1292
|
+
const v = this.liveVal(css).trim();
|
|
1293
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(v);
|
|
1294
|
+
if (m) return parseFloat(m[1]);
|
|
1295
|
+
if (v === "") return null;
|
|
1296
|
+
const n = parseFloat(v);
|
|
1297
|
+
return Number.isFinite(n) ? n : null;
|
|
1298
|
+
}
|
|
1299
|
+
numInfo(cssList) {
|
|
1300
|
+
const nums = cssList.map((c) => this.liveNum(c));
|
|
1301
|
+
if (nums.length === 1) return { value: nums[0] == null ? "" : String(round2(nums[0])), mixed: false };
|
|
1302
|
+
const first = nums[0];
|
|
1303
|
+
const allEqual = nums.every((n) => n != null && n === first);
|
|
1304
|
+
return allEqual ? { value: String(round2(first)), mixed: false } : { value: "", mixed: true };
|
|
1305
|
+
}
|
|
1306
|
+
isChanged(cssList) {
|
|
1307
|
+
const st = this.st();
|
|
1308
|
+
if (!st) return false;
|
|
1309
|
+
return cssList.some((c) => st.record.changes.some((ch) => ch.property === c));
|
|
1310
|
+
}
|
|
1311
|
+
selectCurrent(css) {
|
|
1312
|
+
let v = this.liveVal(css).trim();
|
|
1313
|
+
if (v === "normal") v = "400";
|
|
1314
|
+
if (v === "bold") v = "700";
|
|
1315
|
+
return v;
|
|
1316
|
+
}
|
|
1317
|
+
// ---- apply / record / revert ----
|
|
1318
|
+
/** All elements that share the selected element's JSX origin (or tag+class). */
|
|
1319
|
+
siblingsOf(el) {
|
|
1320
|
+
try {
|
|
1321
|
+
const doc = el.ownerDocument;
|
|
1322
|
+
const raw = el.closest("[data-uiv-src]")?.getAttribute("data-uiv-src");
|
|
1323
|
+
if (raw) {
|
|
1324
|
+
const m = [...doc.querySelectorAll("[data-uiv-src]")].filter(
|
|
1325
|
+
(e) => e.getAttribute("data-uiv-src") === raw
|
|
1326
|
+
);
|
|
1327
|
+
if (m.length) return m;
|
|
1328
|
+
}
|
|
1329
|
+
if (el.classList.length) {
|
|
1330
|
+
const sig = (e) => `${e.tagName}|${[...e.classList].sort().join(" ")}`;
|
|
1331
|
+
const mine = sig(el);
|
|
1332
|
+
const m = [...doc.querySelectorAll(el.tagName)].filter((e) => sig(e) === mine);
|
|
1333
|
+
if (m.length) return m;
|
|
1334
|
+
}
|
|
1335
|
+
} catch {
|
|
1336
|
+
}
|
|
1337
|
+
return [el];
|
|
1338
|
+
}
|
|
1339
|
+
/** Elements the current edit applies to: all siblings when target is "all". */
|
|
1340
|
+
targetEls() {
|
|
1341
|
+
const el = this.selected;
|
|
1342
|
+
if (!el) return [];
|
|
1343
|
+
return this.st()?.record.target === "all" ? this.siblingsOf(el) : [el];
|
|
1344
|
+
}
|
|
1345
|
+
liveSet(cssList, value) {
|
|
1346
|
+
if (!this.selected) return;
|
|
1347
|
+
for (const el of this.targetEls()) {
|
|
1348
|
+
for (const css of cssList) applyOverride(el, css, value);
|
|
1349
|
+
}
|
|
1350
|
+
this.reposition();
|
|
1351
|
+
}
|
|
1352
|
+
/** Re-apply recorded overrides after the target (all ↔ one) changes. */
|
|
1353
|
+
reapplyForTarget() {
|
|
1354
|
+
const el = this.selected;
|
|
1355
|
+
const st = this.st();
|
|
1356
|
+
if (!el || !st) return;
|
|
1357
|
+
const sibs = this.siblingsOf(el);
|
|
1358
|
+
const targets = this.targetEls();
|
|
1359
|
+
const props = new Set(st.record.changes.map((c) => c.property));
|
|
1360
|
+
for (const css of props) {
|
|
1361
|
+
for (const e of sibs) removeOverride(e, css);
|
|
1362
|
+
const c = st.record.changes.find((ch) => ch.property === css);
|
|
1363
|
+
if (c) for (const e of targets) applyOverride(e, css, c.after.computed);
|
|
1364
|
+
}
|
|
1365
|
+
this.reposition();
|
|
1366
|
+
}
|
|
1367
|
+
/** The breakpoint recorded edits are scoped to: manual override, else window. */
|
|
1368
|
+
/** The breakpoint edits are scoped to: the virtual screen's width when in
|
|
1369
|
+
* responsive mode, otherwise the real window. */
|
|
1370
|
+
activeScope() {
|
|
1371
|
+
const sys = this.bpSystem();
|
|
1372
|
+
if (this.responsive) return activeBreakpoint(this.frameWidth, sys);
|
|
1373
|
+
return currentBreakpoint(sys);
|
|
1374
|
+
}
|
|
1375
|
+
/** Re-tag the selected element's already-recorded changes to the current scope. */
|
|
1376
|
+
retagSelected() {
|
|
1377
|
+
const st = this.st();
|
|
1378
|
+
if (!st) return;
|
|
1379
|
+
const scope = this.activeScope();
|
|
1380
|
+
for (const c of st.record.changes) {
|
|
1381
|
+
c.breakpoint = scope.name;
|
|
1382
|
+
c.breakpointPx = scope.minWidth;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
recordProps(cssList) {
|
|
1386
|
+
const el = this.selected;
|
|
1387
|
+
const st = this.st();
|
|
1388
|
+
if (!el || !st) return;
|
|
1389
|
+
const scope = this.activeScope();
|
|
1390
|
+
for (const css of cssList) {
|
|
1391
|
+
const applied = el.style.getPropertyValue(css);
|
|
1392
|
+
if (!applied) continue;
|
|
1393
|
+
st.applied.add(css);
|
|
1394
|
+
this.setChange(st.record, buildChange(css, st.original[css], applied, scope));
|
|
1395
|
+
}
|
|
1396
|
+
this.renderBody();
|
|
1397
|
+
}
|
|
1398
|
+
revertProps(cssList) {
|
|
1399
|
+
const el = this.selected;
|
|
1400
|
+
const st = this.st();
|
|
1401
|
+
if (!el || !st) return;
|
|
1402
|
+
const sibs = this.siblingsOf(el);
|
|
1403
|
+
for (const css of cssList) {
|
|
1404
|
+
for (const e of sibs) removeOverride(e, css);
|
|
1405
|
+
st.applied.delete(css);
|
|
1406
|
+
st.record.changes = st.record.changes.filter((c) => c.property !== css);
|
|
1407
|
+
}
|
|
1408
|
+
this.reposition();
|
|
1409
|
+
this.renderBody();
|
|
1410
|
+
}
|
|
1411
|
+
commitNumeric(cssList, raw) {
|
|
1412
|
+
if (raw.trim() === "") {
|
|
1413
|
+
this.revertProps(cssList);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const n = parseFloat(raw);
|
|
1417
|
+
if (!Number.isFinite(n)) return;
|
|
1418
|
+
this.liveSet(cssList, `${n}px`);
|
|
1419
|
+
this.recordProps(cssList);
|
|
1420
|
+
}
|
|
1421
|
+
commitValue(cssList, value, allowEmpty = false) {
|
|
1422
|
+
if (allowEmpty && value.trim() === "") {
|
|
1423
|
+
this.revertProps(cssList);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
this.liveSet(cssList, value);
|
|
1427
|
+
this.recordProps(cssList);
|
|
1428
|
+
}
|
|
1429
|
+
onDimInput(css, box) {
|
|
1430
|
+
const st = this.st();
|
|
1431
|
+
if (!st) return;
|
|
1432
|
+
const input = box.querySelector("input");
|
|
1433
|
+
const unit = box.querySelector(".uiv-unit").value;
|
|
1434
|
+
st.dimUnit[css] = unit;
|
|
1435
|
+
const num = input.value.trim();
|
|
1436
|
+
if (num === "") {
|
|
1437
|
+
this.revertProps([css]);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
this.liveSet([css], unit === "" ? num : `${num}${unit}`);
|
|
1441
|
+
this.recordProps([css]);
|
|
1442
|
+
}
|
|
1443
|
+
/** Switching the unit only changes how the value is shown — it converts, it
|
|
1444
|
+
* does not re-apply or clear the number. */
|
|
1445
|
+
onUnitChange(css, box) {
|
|
1446
|
+
const st = this.st();
|
|
1447
|
+
if (!st) return;
|
|
1448
|
+
st.dimUnit[css] = box.querySelector(".uiv-unit").value;
|
|
1449
|
+
this.renderBody();
|
|
1450
|
+
}
|
|
1451
|
+
setChange(record, entry) {
|
|
1452
|
+
record.changes = record.changes.filter(
|
|
1453
|
+
(c) => !(c.property === entry.property && c.breakpoint === entry.breakpoint)
|
|
1454
|
+
);
|
|
1455
|
+
if (entry.before.computed !== entry.after.computed) record.changes.push(entry);
|
|
1456
|
+
}
|
|
1457
|
+
// ---- rendering ----
|
|
1458
|
+
renderBody() {
|
|
1459
|
+
const body = this.q(".uiv-body");
|
|
1460
|
+
if (!this.selected) {
|
|
1461
|
+
body.innerHTML = `
|
|
1462
|
+
${this.breakpointBarHtml()}
|
|
1463
|
+
<div class="uiv-empty">Click any element ${this.responsive ? "in the frame" : "on the page"} to select it.</div>
|
|
1464
|
+
<div class="uiv-hint">Alt+U toggles \xB7 Esc deselects. Tweaks stay in the browser \u2014 nothing is written to your code.</div>
|
|
1465
|
+
${this.journalHtml()}
|
|
1466
|
+
`;
|
|
1467
|
+
this.bindControls();
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const st = this.states.get(this.selected);
|
|
1471
|
+
const id = st.record.identity;
|
|
1472
|
+
const src = id.source.confidence !== "none" ? `${id.source.file}:${id.source.line}:${id.source.column}` : id.componentName ? `<${id.componentName}> (no source map)` : id.selector;
|
|
1473
|
+
body.innerHTML = `
|
|
1474
|
+
<div class="uiv-sec uiv-meta">
|
|
1475
|
+
<div><span class="uiv-el"><${id.tagName}></span> ${id.textSnippet ? `"${escapeHtml(id.textSnippet)}"` : ""}</div>
|
|
1476
|
+
<div class="uiv-src">${escapeHtml(src)}</div>
|
|
1477
|
+
<span class="uiv-mech">${st.record.styling.primaryMechanism}</span>
|
|
1478
|
+
</div>
|
|
1479
|
+
${this.breakpointBarHtml()}
|
|
1480
|
+
${this.targetHtml(st)}
|
|
1481
|
+
${this.controlsHtml(this.context(this.selected))}
|
|
1482
|
+
${this.journalHtml()}
|
|
1483
|
+
`;
|
|
1484
|
+
this.bindControls();
|
|
1485
|
+
}
|
|
1486
|
+
/** Breakpoint scope switcher: shows the PROJECT's breakpoints + the live window one. */
|
|
1487
|
+
breakpointBarHtml() {
|
|
1488
|
+
const sys = this.bpSystem();
|
|
1489
|
+
const bps = sys.breakpoints;
|
|
1490
|
+
const names = ["base", ...bps.map((b) => b.name)];
|
|
1491
|
+
const frameBp = this.responsive ? activeBreakpoint(this.frameWidth, sys).name : null;
|
|
1492
|
+
const liveChip = `<button class="uiv-chip${!this.responsive ? " on" : ""}" data-bp="live" title="Your real browser window">Live</button>`;
|
|
1493
|
+
const chips = names.map((n) => {
|
|
1494
|
+
const active = this.responsive && n === frameBp;
|
|
1495
|
+
const px2 = n === "base" ? 0 : bps.find((b) => b.name === n).minWidth;
|
|
1496
|
+
return `<button class="uiv-chip${active ? " on" : ""}" data-bp="${n}" title="Preview at \u2265${px2}px">${n}</button>`;
|
|
1497
|
+
}).join("");
|
|
1498
|
+
const detected = sys.name === "detected" ? "" : " (defaults)";
|
|
1499
|
+
const hint = this.responsive ? `Virtual screen at <b>${this.frameWidth}px</b> (${frameBp}). Edits scoped to <b>${frameBp}:</b>. Drag the frame edge to fine-tune.` : `Click a size to shrink the screen to it & design for that breakpoint. Live = your real window.`;
|
|
1500
|
+
return `<div class="uiv-sec"><div class="uiv-sectitle">Screen / breakpoint${detected}</div><div class="uiv-chips">${liveChip}${chips}</div><div class="uiv-bphint">${hint}</div></div>`;
|
|
1501
|
+
}
|
|
1502
|
+
/** "Apply changes to": this element, an existing shared class, or a NEW class. */
|
|
1503
|
+
targetHtml(st) {
|
|
1504
|
+
const target = st.record.target;
|
|
1505
|
+
const classes = st.record.identity.classList;
|
|
1506
|
+
const isNew = target.startsWith("new:");
|
|
1507
|
+
const newName = isNew ? target.slice(4) : "";
|
|
1508
|
+
const n = st.record.identity.instanceCount;
|
|
1509
|
+
const chip = (val, label, on) => `<button class="uiv-clschip${on ? " on" : ""}" data-target="${escapeAttr(val)}">${escapeHtml(label)}</button>`;
|
|
1510
|
+
const allChip = n > 1 ? chip("all", `All ${n} like this`, target === "all") : "";
|
|
1511
|
+
const elChip = chip("element", n > 1 ? "Only this one" : "This element", target === "element");
|
|
1512
|
+
const classChips = classes.map((c) => chip(c, `.${c}`, target === c)).join("");
|
|
1513
|
+
const newInput = `<input class="uiv-newclass${isNew ? " on" : ""}" placeholder="+ new class" value="${escapeAttr(newName)}" title="Create a new class instead of touching the existing ones">`;
|
|
1514
|
+
return `<div class="uiv-sec"><div class="uiv-sectitle">Apply changes to</div><div class="uiv-chips">${allChip}${elChip}${classChips}${newInput}</div></div>`;
|
|
1515
|
+
}
|
|
1516
|
+
/** Decide which controls are relevant to the selected element. */
|
|
1517
|
+
context(el) {
|
|
1518
|
+
const hasText = Array.from(el.childNodes).some(
|
|
1519
|
+
(n) => n.nodeType === 3 && (n.textContent || "").trim().length > 0
|
|
1520
|
+
);
|
|
1521
|
+
let flexGrid = false;
|
|
1522
|
+
try {
|
|
1523
|
+
flexGrid = /flex|grid/.test(getComputedStyle(el).display);
|
|
1524
|
+
} catch {
|
|
1525
|
+
}
|
|
1526
|
+
return { hasText, flexGrid };
|
|
1527
|
+
}
|
|
1528
|
+
relevant(c, ctx) {
|
|
1529
|
+
const req = c.requires;
|
|
1530
|
+
if (req === "text") return ctx.hasText;
|
|
1531
|
+
if (req === "flexgrid") return ctx.flexGrid;
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
controlsHtml(ctx) {
|
|
1535
|
+
return SECTIONS.map((sec) => {
|
|
1536
|
+
const controls = sec.controls.filter((c) => this.relevant(c, ctx));
|
|
1537
|
+
if (!controls.length) return "";
|
|
1538
|
+
const rows = controls.map((c) => this.controlRow(c)).join("");
|
|
1539
|
+
return `<div class="uiv-sec"><div class="uiv-sectitle">${sec.title}</div>${rows}</div>`;
|
|
1540
|
+
}).join("");
|
|
1541
|
+
}
|
|
1542
|
+
numField(cssAttr, value, handle, changed, isSide, placeholder) {
|
|
1543
|
+
return `<div class="uiv-num${changed ? " changed" : ""}" data-css="${cssAttr}"><span class="uiv-scrub${isSide ? " txt" : ""}" title="Drag to change">${handle}</span><input type="number" value="${escapeAttr(value)}" placeholder="${escapeAttr(placeholder)}"></div>`;
|
|
1544
|
+
}
|
|
1545
|
+
fontSizePx() {
|
|
1546
|
+
const el = this.selected;
|
|
1547
|
+
if (!el) return 16;
|
|
1548
|
+
try {
|
|
1549
|
+
return parseFloat(getComputedStyle(el).fontSize) || 16;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return 16;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/** Measure the used px height of `line-height: normal` for the element's font. */
|
|
1555
|
+
measureNormalLineHeight() {
|
|
1556
|
+
const el = this.selected;
|
|
1557
|
+
if (!el) return null;
|
|
1558
|
+
try {
|
|
1559
|
+
const cs = getComputedStyle(el);
|
|
1560
|
+
const probe = document.createElement("div");
|
|
1561
|
+
probe.textContent = "Mg";
|
|
1562
|
+
probe.style.cssText = "position:absolute;left:-99999px;top:0;visibility:hidden;white-space:nowrap;margin:0;padding:0;border:0;line-height:normal";
|
|
1563
|
+
probe.style.fontFamily = cs.fontFamily;
|
|
1564
|
+
probe.style.fontSize = cs.fontSize;
|
|
1565
|
+
probe.style.fontWeight = cs.fontWeight;
|
|
1566
|
+
probe.style.fontStyle = cs.fontStyle;
|
|
1567
|
+
document.body.appendChild(probe);
|
|
1568
|
+
const h = probe.getBoundingClientRect().height;
|
|
1569
|
+
probe.remove();
|
|
1570
|
+
return h || null;
|
|
1571
|
+
} catch {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/** Resolve a dim property's current value to px. */
|
|
1576
|
+
currentPx(css) {
|
|
1577
|
+
const v = this.liveVal(css).trim();
|
|
1578
|
+
if (v === "" || v === "normal" || v === "auto") {
|
|
1579
|
+
if (css === "letter-spacing") return 0;
|
|
1580
|
+
if (css === "line-height") return this.measureNormalLineHeight();
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
const fs = this.fontSizePx();
|
|
1584
|
+
let m;
|
|
1585
|
+
if (m = /^(-?\d*\.?\d+)px$/.exec(v)) return parseFloat(m[1]);
|
|
1586
|
+
if (m = /^(-?\d*\.?\d+)em$/.exec(v)) return parseFloat(m[1]) * fs;
|
|
1587
|
+
if (m = /^(-?\d*\.?\d+)rem$/.exec(v)) return parseFloat(m[1]) * 16;
|
|
1588
|
+
if (m = /^(-?\d*\.?\d+)%$/.exec(v)) return parseFloat(m[1]) / 100 * fs;
|
|
1589
|
+
if ((m = /^(-?\d*\.?\d+)$/.exec(v)) && css === "line-height") return parseFloat(m[1]) * fs;
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
pxToUnit(px2, unit) {
|
|
1593
|
+
const fs = this.fontSizePx();
|
|
1594
|
+
if (unit === "em" || unit === "") return px2 / fs;
|
|
1595
|
+
if (unit === "%") return px2 / fs * 100;
|
|
1596
|
+
return px2;
|
|
1597
|
+
}
|
|
1598
|
+
dimDisplay(c) {
|
|
1599
|
+
const st = this.st();
|
|
1600
|
+
const computed = this.liveVal(c.css);
|
|
1601
|
+
const unit = st.dimUnit[c.css] ?? c.defaultUnit;
|
|
1602
|
+
const px2 = this.currentPx(c.css);
|
|
1603
|
+
if (px2 == null) return { num: "", unit, placeholder: computed || "normal" };
|
|
1604
|
+
return { num: String(round2(this.pxToUnit(px2, unit))), unit, placeholder: computed || "\u2014" };
|
|
1605
|
+
}
|
|
1606
|
+
dimField(c) {
|
|
1607
|
+
const d = this.dimDisplay(c);
|
|
1608
|
+
const changed = this.isChanged([c.css]);
|
|
1609
|
+
const units = c.units.map((u) => `<option value="${u}"${u === d.unit ? " selected" : ""}>${UNIT_LABELS[u] ?? u}</option>`).join("");
|
|
1610
|
+
return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><div class="uiv-num uiv-dim${changed ? " changed" : ""}" data-css="${c.css}"><span class="uiv-scrub" title="Drag to change">${c.icon}</span><input type="number" step="any" value="${escapeAttr(d.num)}" placeholder="${escapeAttr(d.placeholder)}"><select class="uiv-unit" title="Unit">${units}</select></div></div><span></span></div>`;
|
|
1611
|
+
}
|
|
1612
|
+
controlRow(c) {
|
|
1613
|
+
if (c.kind === "box") {
|
|
1614
|
+
const cssList = c.sides.map((s) => s.css);
|
|
1615
|
+
const info = this.numInfo(cssList);
|
|
1616
|
+
const changed = this.isChanged(cssList);
|
|
1617
|
+
const open = this.expanded.has(c.key);
|
|
1618
|
+
let html = `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield">${this.numField(cssList.join(","), info.mixed ? "" : info.value, c.icon, changed, false, info.mixed ? "Mixed" : "\u2014")}</div><button class="uiv-expand${open ? " on" : ""}" data-key="${c.key}" title="Edit each side individually">${open ? ICONS.collapse : ICONS.expand}</button></div>`;
|
|
1619
|
+
if (open) {
|
|
1620
|
+
html += `<div class="uiv-sides">` + c.sides.map((s) => {
|
|
1621
|
+
const v = this.liveNum(s.css);
|
|
1622
|
+
return this.numField(
|
|
1623
|
+
s.css,
|
|
1624
|
+
v == null ? "" : String(round2(v)),
|
|
1625
|
+
s.label,
|
|
1626
|
+
this.isChanged([s.css]),
|
|
1627
|
+
true,
|
|
1628
|
+
"\u2014"
|
|
1629
|
+
);
|
|
1630
|
+
}).join("") + `</div>`;
|
|
1631
|
+
}
|
|
1632
|
+
return html;
|
|
1633
|
+
}
|
|
1634
|
+
if (c.kind === "len") {
|
|
1635
|
+
const v = this.liveNum(c.css);
|
|
1636
|
+
return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield">${this.numField(c.css, v == null ? "" : String(round2(v)), c.icon, this.isChanged([c.css]), false, "\u2014")}</div><span></span></div>`;
|
|
1637
|
+
}
|
|
1638
|
+
if (c.kind === "dim") {
|
|
1639
|
+
return this.dimField(c);
|
|
1640
|
+
}
|
|
1641
|
+
if (c.kind === "select") {
|
|
1642
|
+
const cur = this.selectCurrent(c.css);
|
|
1643
|
+
const opts = c.options.map((o) => `<option value="${o}"${o === cur ? " selected" : ""}>${o}</option>`).join("");
|
|
1644
|
+
return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><select class="uiv-sel" data-css="${c.css}">${opts}</select></div><span></span></div>`;
|
|
1645
|
+
}
|
|
1646
|
+
const val = toHexInput(this.liveVal(c.css));
|
|
1647
|
+
return `<div class="uiv-ctl"><span class="clabel">${c.label}</span><div class="cfield"><input type="color" class="uiv-color" data-css="${c.css}" value="${val}"></div><span></span></div>`;
|
|
1648
|
+
}
|
|
1649
|
+
bindControls() {
|
|
1650
|
+
const root = this.root;
|
|
1651
|
+
root.querySelectorAll(".uiv-num:not(.uiv-dim)").forEach((node) => {
|
|
1652
|
+
const box = node;
|
|
1653
|
+
const cssList = (box.getAttribute("data-css") || "").split(",").filter(Boolean);
|
|
1654
|
+
const input = box.querySelector("input");
|
|
1655
|
+
const handle = box.querySelector(".uiv-scrub");
|
|
1656
|
+
input.addEventListener("change", () => this.commitNumeric(cssList, input.value));
|
|
1657
|
+
input.addEventListener("keydown", (e) => {
|
|
1658
|
+
if (e.key === "Enter") input.blur();
|
|
1659
|
+
});
|
|
1660
|
+
if (handle) this.bindScrub(handle, input, cssList);
|
|
1661
|
+
});
|
|
1662
|
+
root.querySelectorAll(".uiv-dim").forEach((node) => {
|
|
1663
|
+
const box = node;
|
|
1664
|
+
const css = box.getAttribute("data-css");
|
|
1665
|
+
const input = box.querySelector("input");
|
|
1666
|
+
const unitSel = box.querySelector(".uiv-unit");
|
|
1667
|
+
const handle = box.querySelector(".uiv-scrub");
|
|
1668
|
+
input.addEventListener("change", () => this.onDimInput(css, box));
|
|
1669
|
+
input.addEventListener("keydown", (e) => {
|
|
1670
|
+
if (e.key === "Enter") input.blur();
|
|
1671
|
+
});
|
|
1672
|
+
unitSel.addEventListener("change", () => this.onUnitChange(css, box));
|
|
1673
|
+
if (handle) this.bindDimScrub(handle, input, unitSel, css, box);
|
|
1674
|
+
});
|
|
1675
|
+
root.querySelectorAll(".uiv-color").forEach((node) => {
|
|
1676
|
+
const input = node;
|
|
1677
|
+
const css = input.getAttribute("data-css");
|
|
1678
|
+
input.addEventListener("change", () => this.commitValue([css], input.value));
|
|
1679
|
+
});
|
|
1680
|
+
root.querySelectorAll(".uiv-sel").forEach((node) => {
|
|
1681
|
+
const sel = node;
|
|
1682
|
+
const css = sel.getAttribute("data-css");
|
|
1683
|
+
sel.addEventListener("change", () => this.commitValue([css], sel.value));
|
|
1684
|
+
});
|
|
1685
|
+
root.querySelectorAll(".uiv-expand").forEach((node) => {
|
|
1686
|
+
const btn = node;
|
|
1687
|
+
const key = btn.getAttribute("data-key");
|
|
1688
|
+
btn.addEventListener("click", () => {
|
|
1689
|
+
if (this.expanded.has(key)) this.expanded.delete(key);
|
|
1690
|
+
else this.expanded.add(key);
|
|
1691
|
+
this.renderBody();
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1694
|
+
root.querySelectorAll(".uiv-chip").forEach((node) => {
|
|
1695
|
+
const btn = node;
|
|
1696
|
+
const bp = btn.getAttribute("data-bp");
|
|
1697
|
+
btn.addEventListener("click", () => {
|
|
1698
|
+
if (bp === "live") {
|
|
1699
|
+
if (this.responsive) this.toggleResponsive(false);
|
|
1700
|
+
else this.renderBody();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const w = bp === "base" ? 390 : this.bpSystem().breakpoints.find((b) => b.name === bp)?.minWidth ?? 768;
|
|
1704
|
+
this.frameWidth = w;
|
|
1705
|
+
if (!this.responsive) {
|
|
1706
|
+
this.toggleResponsive(true);
|
|
1707
|
+
} else {
|
|
1708
|
+
this.setFrameWidth(w);
|
|
1709
|
+
this.retagSelected();
|
|
1710
|
+
this.renderBody();
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1714
|
+
root.querySelectorAll(".uiv-clschip").forEach((node) => {
|
|
1715
|
+
const btn = node;
|
|
1716
|
+
const target = btn.getAttribute("data-target");
|
|
1717
|
+
btn.addEventListener("click", () => {
|
|
1718
|
+
const st = this.st();
|
|
1719
|
+
if (st) st.record.target = target;
|
|
1720
|
+
this.reapplyForTarget();
|
|
1721
|
+
this.renderBody();
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
root.querySelectorAll(".uiv-newclass").forEach((node) => {
|
|
1725
|
+
const input = node;
|
|
1726
|
+
input.addEventListener("change", () => {
|
|
1727
|
+
const st = this.st();
|
|
1728
|
+
if (!st) return;
|
|
1729
|
+
const name = input.value.trim().replace(/^\./, "").replace(/\s+/g, "-");
|
|
1730
|
+
st.record.target = name ? `new:${name}` : "element";
|
|
1731
|
+
this.renderBody();
|
|
1732
|
+
});
|
|
1733
|
+
input.addEventListener("keydown", (e) => {
|
|
1734
|
+
if (e.key === "Enter") input.blur();
|
|
1735
|
+
});
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
bindScrub(handle, input, cssList) {
|
|
1739
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
1740
|
+
e.preventDefault();
|
|
1741
|
+
const startX = e.clientX;
|
|
1742
|
+
const start2 = parseFloat(input.value) || 0;
|
|
1743
|
+
try {
|
|
1744
|
+
handle.setPointerCapture(e.pointerId);
|
|
1745
|
+
} catch {
|
|
1746
|
+
}
|
|
1747
|
+
const move = (ev) => {
|
|
1748
|
+
const dx = ev.clientX - startX;
|
|
1749
|
+
let nv = start2 + Math.round(dx);
|
|
1750
|
+
if (ev.shiftKey) nv = Math.round(nv / 10) * 10;
|
|
1751
|
+
input.value = String(nv);
|
|
1752
|
+
this.liveSet(cssList, `${nv}px`);
|
|
1753
|
+
};
|
|
1754
|
+
const up = () => {
|
|
1755
|
+
try {
|
|
1756
|
+
handle.releasePointerCapture(e.pointerId);
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
handle.removeEventListener("pointermove", move);
|
|
1760
|
+
handle.removeEventListener("pointerup", up);
|
|
1761
|
+
this.recordProps(cssList);
|
|
1762
|
+
};
|
|
1763
|
+
handle.addEventListener("pointermove", move);
|
|
1764
|
+
handle.addEventListener("pointerup", up);
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
bindDimScrub(handle, input, unitSel, css, box) {
|
|
1768
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
1769
|
+
e.preventDefault();
|
|
1770
|
+
const startX = e.clientX;
|
|
1771
|
+
const start2 = parseFloat(input.value) || 0;
|
|
1772
|
+
try {
|
|
1773
|
+
handle.setPointerCapture(e.pointerId);
|
|
1774
|
+
} catch {
|
|
1775
|
+
}
|
|
1776
|
+
const stepFor = (u) => u === "" ? 0.1 : u === "em" ? 0.01 : 1;
|
|
1777
|
+
const move = (ev) => {
|
|
1778
|
+
const u = unitSel.value;
|
|
1779
|
+
const step = stepFor(u) * (ev.shiftKey ? 10 : 1);
|
|
1780
|
+
const dec = step < 0.1 ? 2 : step < 1 ? 1 : 0;
|
|
1781
|
+
const nv = +(start2 + (ev.clientX - startX) * step).toFixed(dec);
|
|
1782
|
+
input.value = String(nv);
|
|
1783
|
+
this.liveSet([css], u === "" ? String(nv) : `${nv}${u}`);
|
|
1784
|
+
};
|
|
1785
|
+
const up = () => {
|
|
1786
|
+
try {
|
|
1787
|
+
handle.releasePointerCapture(e.pointerId);
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
handle.removeEventListener("pointermove", move);
|
|
1791
|
+
handle.removeEventListener("pointerup", up);
|
|
1792
|
+
this.onDimInput(css, box);
|
|
1793
|
+
};
|
|
1794
|
+
handle.addEventListener("pointermove", move);
|
|
1795
|
+
handle.addEventListener("pointerup", up);
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
journalHtml() {
|
|
1799
|
+
const records = [...this.states.values()].map((s) => s.record).filter((r) => r.changes.length > 0);
|
|
1800
|
+
if (!records.length) return "";
|
|
1801
|
+
const count = records.reduce((n, r) => n + collapseChanges(r.changes).length, 0);
|
|
1802
|
+
const items = records.map((r) => this.journalItem(r)).join("");
|
|
1803
|
+
return `<div class="uiv-sec"><div class="uiv-sectitle">Recorded (${count})</div><div class="uiv-journal">${items}</div></div>`;
|
|
1804
|
+
}
|
|
1805
|
+
journalItem(r) {
|
|
1806
|
+
const id = r.identity;
|
|
1807
|
+
const loc = id.source.confidence !== "none" ? `${id.source.file}:${id.source.line}` : id.componentName || "";
|
|
1808
|
+
const chgs = collapseChanges(r.changes).map((c) => {
|
|
1809
|
+
const bp = c.breakpoint === "base" ? "" : `<span class="bp">${c.breakpoint}:</span> `;
|
|
1810
|
+
const tok = c.after.token ? ` <span class="tok">${escapeHtml(c.after.token)}</span>` : "";
|
|
1811
|
+
return `<div class="uiv-jchg">${bp}${c.property}: ${c.before.computed} \u2192 ${c.after.computed}${tok}</div>`;
|
|
1812
|
+
}).join("");
|
|
1813
|
+
return `<div class="uiv-jitem"><div class="jhead"><span class="jel"><${id.tagName}></span><span class="jloc">${escapeHtml(loc)}</span></div>${chgs}</div>`;
|
|
1814
|
+
}
|
|
1815
|
+
// ---- actions ----
|
|
1816
|
+
records() {
|
|
1817
|
+
return [...this.states.values()].map((s) => s.record).filter((r) => r.changes.length > 0);
|
|
1818
|
+
}
|
|
1819
|
+
async copyPrompt() {
|
|
1820
|
+
const recs = this.records();
|
|
1821
|
+
if (!recs.length) return this.showToast("No tweaks recorded yet");
|
|
1822
|
+
await this.copy(renderPrompt(recs));
|
|
1823
|
+
this.showToast("Prompt copied \u2713");
|
|
1824
|
+
}
|
|
1825
|
+
async copyJSON() {
|
|
1826
|
+
const recs = this.records();
|
|
1827
|
+
if (!recs.length) return this.showToast("No tweaks recorded yet");
|
|
1828
|
+
const spec = renderSpec(recs, {
|
|
1829
|
+
url: location.href,
|
|
1830
|
+
width: window.innerWidth,
|
|
1831
|
+
height: window.innerHeight,
|
|
1832
|
+
dpr: window.devicePixelRatio,
|
|
1833
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
1834
|
+
});
|
|
1835
|
+
await this.copy(JSON.stringify(spec, null, 2));
|
|
1836
|
+
this.showToast("JSON copied \u2713");
|
|
1837
|
+
}
|
|
1838
|
+
resetSelected() {
|
|
1839
|
+
const el = this.selected;
|
|
1840
|
+
if (!el) return;
|
|
1841
|
+
const st = this.states.get(el);
|
|
1842
|
+
if (!st) return;
|
|
1843
|
+
const sibs = this.siblingsOf(el);
|
|
1844
|
+
for (const css of st.applied) for (const e of sibs) removeOverride(e, css);
|
|
1845
|
+
st.applied.clear();
|
|
1846
|
+
st.record.changes = [];
|
|
1847
|
+
this.reposition();
|
|
1848
|
+
this.renderBody();
|
|
1849
|
+
}
|
|
1850
|
+
clearAll() {
|
|
1851
|
+
for (const [el, st] of this.states) {
|
|
1852
|
+
const sibs = this.siblingsOf(el);
|
|
1853
|
+
for (const css of st.applied) for (const e of sibs) removeOverride(e, css);
|
|
1854
|
+
}
|
|
1855
|
+
this.states.clear();
|
|
1856
|
+
this.selected = null;
|
|
1857
|
+
this.reposition();
|
|
1858
|
+
this.renderBody();
|
|
1859
|
+
}
|
|
1860
|
+
async copy(text) {
|
|
1861
|
+
try {
|
|
1862
|
+
await navigator.clipboard.writeText(text);
|
|
1863
|
+
} catch {
|
|
1864
|
+
const ta = document.createElement("textarea");
|
|
1865
|
+
ta.value = text;
|
|
1866
|
+
this.root.appendChild(ta);
|
|
1867
|
+
ta.select();
|
|
1868
|
+
document.execCommand("copy");
|
|
1869
|
+
ta.remove();
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
updateBp() {
|
|
1873
|
+
if (this.responsive) {
|
|
1874
|
+
const bp = activeBreakpoint(this.frameWidth, this.bpSystem()).name;
|
|
1875
|
+
this.bpBadge.textContent = `${bp} \xB7 ${this.frameWidth}px`;
|
|
1876
|
+
} else {
|
|
1877
|
+
const bp = currentBreakpoint(this.bpSystem());
|
|
1878
|
+
this.bpBadge.textContent = `${bp.name} \xB7 ${window.innerWidth}px`;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
showToast(msg) {
|
|
1882
|
+
this.toast.textContent = msg;
|
|
1883
|
+
this.toast.classList.add("show");
|
|
1884
|
+
window.setTimeout(() => this.toast.classList.remove("show"), 1600);
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
function escapeHtml(s) {
|
|
1888
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
1889
|
+
}
|
|
1890
|
+
function escapeAttr(s) {
|
|
1891
|
+
return s.replace(/"/g, """);
|
|
1892
|
+
}
|
|
1893
|
+
function cssAttrEscape(s) {
|
|
1894
|
+
return s.replace(/["\\]/g, "\\$&");
|
|
1895
|
+
}
|
|
1896
|
+
var instance = null;
|
|
1897
|
+
function start() {
|
|
1898
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1899
|
+
try {
|
|
1900
|
+
if (window.frameElement?.getAttribute("data-uiv-frame")) return;
|
|
1901
|
+
} catch {
|
|
1902
|
+
}
|
|
1903
|
+
if (instance || document.getElementById("uivisor-root")) return;
|
|
1904
|
+
instance = new Uivisor();
|
|
1905
|
+
const boot = () => instance.mount();
|
|
1906
|
+
if (document.readyState === "loading") {
|
|
1907
|
+
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
|
1908
|
+
} else {
|
|
1909
|
+
boot();
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
start();
|
|
1913
|
+
export {
|
|
1914
|
+
start
|
|
1915
|
+
};
|