@aaqiljamal/visual-editor-runtime 0.2.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/dist/index.js ADDED
@@ -0,0 +1,2049 @@
1
+ "use client";
2
+
3
+ // src/Overlay.tsx
4
+ import { useEffect } from "react";
5
+ import { h, render } from "preact";
6
+ import Moveable from "moveable";
7
+ var ANCHOR_TAG = "visual-editor-anchor";
8
+ var SELF_TEST_KEY = "visualEditorMounted";
9
+ var FRAMEWORK_INTERNALS = /* @__PURE__ */ new Set([
10
+ "SegmentViewNode",
11
+ "ClientPageRoot",
12
+ "ClientSegmentRoot",
13
+ "OutletBoundary",
14
+ "AppRouter",
15
+ "Router",
16
+ "ServerInsertedHTMLProvider",
17
+ "ServerInsertedMetadataProvider",
18
+ "DevRootHTTPAccessFallbackBoundary",
19
+ "HTTPAccessFallbackBoundary",
20
+ "HTTPAccessFallbackErrorBoundary",
21
+ "RenderFromTemplateContext",
22
+ "LayoutRouter",
23
+ "RedirectBoundary",
24
+ "RedirectErrorBoundary",
25
+ "NotFoundBoundary",
26
+ "NotFoundErrorBoundary",
27
+ "DevRootNotFoundBoundary",
28
+ "ReactDevOverlay",
29
+ "HotReload",
30
+ "AppRouterAnnouncer",
31
+ "Provider",
32
+ "Overlay"
33
+ ]);
34
+ function getComponentName(el) {
35
+ const fiberKey = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
36
+ if (!fiberKey) return null;
37
+ let fiber = el[fiberKey];
38
+ let depth = 0;
39
+ while (fiber && depth < 30) {
40
+ const t = fiber.type;
41
+ if (typeof t === "function") {
42
+ const fn = t;
43
+ const name = fn.displayName || fn.name;
44
+ if (name && name !== "_default" && !FRAMEWORK_INTERNALS.has(name)) {
45
+ return name;
46
+ }
47
+ }
48
+ fiber = fiber.return ?? void 0;
49
+ depth++;
50
+ }
51
+ return null;
52
+ }
53
+ function nameFromDataOid(oid) {
54
+ if (!oid) return null;
55
+ const filePath = oid.split(":")[0];
56
+ const base = filePath.split("/").pop();
57
+ if (!base) return null;
58
+ const stem = base.replace(/\.(tsx|ts|jsx|js)$/, "");
59
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
60
+ }
61
+ var DEFAULT_SCALE_STEPS = [
62
+ 0,
63
+ 0.5,
64
+ 1,
65
+ 1.5,
66
+ 2,
67
+ 2.5,
68
+ 3,
69
+ 3.5,
70
+ 4,
71
+ 5,
72
+ 6,
73
+ 7,
74
+ 8,
75
+ 9,
76
+ 10,
77
+ 11,
78
+ 12,
79
+ 14,
80
+ 16,
81
+ 20,
82
+ 24,
83
+ 28,
84
+ 32,
85
+ 36,
86
+ 40,
87
+ 44,
88
+ 48,
89
+ 52,
90
+ 56,
91
+ 60,
92
+ 64,
93
+ 72,
94
+ 80,
95
+ 96
96
+ ];
97
+ var resolvedScaleCache = null;
98
+ function getResolvedScale(spacingPx) {
99
+ if (resolvedScaleCache && resolvedScaleCache.spacingPx === spacingPx) {
100
+ return resolvedScaleCache.map;
101
+ }
102
+ const map = /* @__PURE__ */ new Map();
103
+ const cs = getComputedStyle(document.documentElement);
104
+ for (const step of DEFAULT_SCALE_STEPS) {
105
+ const named = cs.getPropertyValue(`--spacing-${step}`).trim();
106
+ let px;
107
+ if (named) {
108
+ const parsed = parseCssLengthToPx(named);
109
+ px = parsed !== null ? parsed : step * spacingPx;
110
+ } else {
111
+ px = step * spacingPx;
112
+ }
113
+ map.set(step, px);
114
+ }
115
+ resolvedScaleCache = { spacingPx, map };
116
+ return map;
117
+ }
118
+ function snapToTailwind(targetPx, spacingPx, tolerancePx = 1) {
119
+ if (!Number.isFinite(targetPx) || targetPx < 0) {
120
+ return { step: null, suffix: `[0px]`, resolvedPx: 0, snapped: false };
121
+ }
122
+ const scale = getResolvedScale(spacingPx);
123
+ let bestStep = null;
124
+ let bestDiff = Infinity;
125
+ let bestPx = 0;
126
+ for (const [step, px] of scale) {
127
+ const diff = Math.abs(px - targetPx);
128
+ if (diff < bestDiff) {
129
+ bestDiff = diff;
130
+ bestStep = step;
131
+ bestPx = px;
132
+ }
133
+ }
134
+ if (bestStep !== null && bestDiff <= tolerancePx) {
135
+ return {
136
+ step: bestStep,
137
+ suffix: String(bestStep),
138
+ resolvedPx: bestPx,
139
+ snapped: true
140
+ };
141
+ }
142
+ const intPx = Math.round(targetPx);
143
+ return { step: null, suffix: `[${intPx}px]`, resolvedPx: intPx, snapped: false };
144
+ }
145
+ function parseCssLengthToPx(value, rootFontPx = 16) {
146
+ const v = value.trim();
147
+ if (v.endsWith("px")) {
148
+ const n = parseFloat(v.slice(0, -2));
149
+ return Number.isFinite(n) ? n : null;
150
+ }
151
+ if (v.endsWith("rem")) {
152
+ const n = parseFloat(v.slice(0, -3));
153
+ return Number.isFinite(n) ? n * rootFontPx : null;
154
+ }
155
+ if (v.endsWith("em")) {
156
+ const n = parseFloat(v.slice(0, -2));
157
+ return Number.isFinite(n) ? n * rootFontPx : null;
158
+ }
159
+ return null;
160
+ }
161
+ function pxFromClass(token, spacingPx) {
162
+ const open = token.indexOf("[");
163
+ if (open !== -1 && token.endsWith("]")) {
164
+ return parseCssLengthToPx(token.slice(open + 1, -1));
165
+ }
166
+ const lastDash = token.lastIndexOf("-");
167
+ if (lastDash === -1) return null;
168
+ const stepStr = token.slice(lastDash + 1);
169
+ if (!/^-?\d+(?:\.\d+)?$/.test(stepStr)) return null;
170
+ const step = parseFloat(stepStr);
171
+ return Number.isFinite(step) ? step * spacingPx : null;
172
+ }
173
+ function bumpStep(currentToken, direction, spacingPx) {
174
+ const open = currentToken.indexOf("[");
175
+ let lastDash;
176
+ if (open !== -1 && currentToken.endsWith("]")) {
177
+ lastDash = currentToken.lastIndexOf("-", open);
178
+ } else {
179
+ lastDash = currentToken.lastIndexOf("-");
180
+ }
181
+ if (lastDash <= 0) return null;
182
+ const prefix = currentToken.slice(0, lastDash);
183
+ const currentPx = pxFromClass(currentToken, spacingPx);
184
+ if (currentPx === null) return null;
185
+ const scale = Array.from(getResolvedScale(spacingPx).entries());
186
+ let currentIdx = -1;
187
+ for (let i = 0; i < scale.length; i++) {
188
+ if (Math.abs(scale[i][1] - currentPx) < 1e-3) {
189
+ currentIdx = i;
190
+ break;
191
+ }
192
+ }
193
+ if (currentIdx === -1) {
194
+ let nearestIdx = 0;
195
+ let nearestDiff = Infinity;
196
+ for (let i = 0; i < scale.length; i++) {
197
+ const diff = Math.abs(scale[i][1] - currentPx);
198
+ if (diff < nearestDiff) {
199
+ nearestDiff = diff;
200
+ nearestIdx = i;
201
+ }
202
+ }
203
+ if (direction === "up" && scale[nearestIdx][1] < currentPx) {
204
+ currentIdx = nearestIdx;
205
+ } else if (direction === "down" && scale[nearestIdx][1] > currentPx) {
206
+ currentIdx = nearestIdx;
207
+ } else {
208
+ return `${prefix}-${scale[nearestIdx][0]}`;
209
+ }
210
+ }
211
+ const newIdx = direction === "up" ? currentIdx + 1 : currentIdx - 1;
212
+ if (newIdx < 0 || newIdx >= scale.length) return null;
213
+ return `${prefix}-${scale[newIdx][0]}`;
214
+ }
215
+ var PADDING_PREFIXES = ["p", "px", "py", "pt", "pr", "pb", "pl"];
216
+ var MARGIN_PREFIXES = ["m", "mx", "my", "mt", "mr", "mb", "ml"];
217
+ var GAP_PREFIXES = ["gap", "gap-x", "gap-y"];
218
+ var WIDTH_PREFIXES = ["w"];
219
+ var HEIGHT_PREFIXES = ["h"];
220
+ function swapClassToken(value, before, after) {
221
+ const parts = value.split(/(\s+)/);
222
+ let found = false;
223
+ return parts.map((p) => {
224
+ if (!found && p === before) {
225
+ found = true;
226
+ return after;
227
+ }
228
+ return p;
229
+ }).join("");
230
+ }
231
+ function findTokenByPrefix(className, prefixes) {
232
+ const tokens = className.split(/\s+/).filter(Boolean);
233
+ for (const p of prefixes) {
234
+ for (const t of tokens) {
235
+ if (t.startsWith(p + "-") && /^(?:-?\d+(?:\.\d+)?|\[[^\]]+\])$/.test(t.slice(p.length + 1))) {
236
+ return t;
237
+ }
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function getSpacingPx() {
243
+ const rootStyle = getComputedStyle(document.documentElement);
244
+ const rootFontPx = parseFloat(rootStyle.fontSize) || 16;
245
+ const spacing = rootStyle.getPropertyValue("--spacing").trim();
246
+ if (!spacing) return 4;
247
+ if (spacing.endsWith("rem")) {
248
+ const n = parseFloat(spacing);
249
+ return Number.isFinite(n) ? n * rootFontPx : 4;
250
+ }
251
+ if (spacing.endsWith("px")) {
252
+ const n = parseFloat(spacing);
253
+ return Number.isFinite(n) ? n : 4;
254
+ }
255
+ return 4;
256
+ }
257
+ var WIDTH_TOKEN_RE = /^w-(\d+(?:\.\d+)?|\[[^\]]+\])$/;
258
+ var SERVER_URL = "/api/visual-editor";
259
+ var DRAFT_STORAGE_KEY = "visual-editor:draft-v1";
260
+ function saveDraft(d) {
261
+ try {
262
+ localStorage.setItem(
263
+ DRAFT_STORAGE_KEY,
264
+ JSON.stringify({ ...d, savedAt: Date.now() })
265
+ );
266
+ } catch {
267
+ }
268
+ }
269
+ function loadDraft() {
270
+ try {
271
+ const raw = localStorage.getItem(DRAFT_STORAGE_KEY);
272
+ if (!raw) return null;
273
+ const parsed = JSON.parse(raw);
274
+ if (typeof parsed.file === "string" && typeof parsed.line === "number" && typeof parsed.col === "number" && typeof parsed.before === "string" && typeof parsed.after === "string" && typeof parsed.oid === "string") {
275
+ return parsed;
276
+ }
277
+ return null;
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ function clearDraft() {
283
+ try {
284
+ localStorage.removeItem(DRAFT_STORAGE_KEY);
285
+ } catch {
286
+ }
287
+ }
288
+ var sessionTokenPromise = null;
289
+ function ensureSessionToken() {
290
+ if (sessionTokenPromise) return sessionTokenPromise;
291
+ sessionTokenPromise = (async () => {
292
+ try {
293
+ const r = await fetch(`${SERVER_URL}/token`);
294
+ if (!r.ok) return null;
295
+ const body = await r.json();
296
+ return typeof body.token === "string" ? body.token : null;
297
+ } catch {
298
+ return null;
299
+ }
300
+ })();
301
+ return sessionTokenPromise;
302
+ }
303
+ async function authedFetch(url, init = {}) {
304
+ const token = await ensureSessionToken();
305
+ const headers = new Headers(init.headers);
306
+ if (init.body !== void 0 && !headers.has("Content-Type")) {
307
+ headers.set("Content-Type", "application/json");
308
+ }
309
+ if (token) headers.set("Authorization", `Bearer ${token}`);
310
+ return fetch(url, { ...init, headers });
311
+ }
312
+ function Overlay({ serverUrl } = {}) {
313
+ if (serverUrl) SERVER_URL = serverUrl;
314
+ useEffect(() => {
315
+ if (process.env.NODE_ENV !== "development") return;
316
+ void ensureSessionToken();
317
+ const anchor = document.createElement(ANCHOR_TAG);
318
+ Object.assign(anchor.style, {
319
+ position: "fixed",
320
+ inset: "0",
321
+ pointerEvents: "none",
322
+ zIndex: "2147483647"
323
+ });
324
+ document.body.appendChild(anchor);
325
+ const shadow = anchor.attachShadow({ mode: "closed" });
326
+ const style = document.createElement("style");
327
+ style.textContent = `
328
+ :host { all: initial; }
329
+ .badge {
330
+ position: fixed; top: 12px; right: 12px;
331
+ background: rebeccapurple; color: white;
332
+ font: 12px/1.2 system-ui, sans-serif;
333
+ padding: 6px 10px; border-radius: 6px;
334
+ pointer-events: auto;
335
+ }
336
+ .hover-outline {
337
+ position: fixed; display: none;
338
+ pointer-events: none;
339
+ box-sizing: border-box;
340
+ border: 1.5px solid #ec4899;
341
+ border-radius: 2px;
342
+ background: rgba(236, 72, 153, 0.06);
343
+ transition: none;
344
+ }
345
+ .hover-tag {
346
+ position: fixed; display: none;
347
+ pointer-events: none;
348
+ background: #0f172a; color: white;
349
+ font: 11px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
350
+ padding: 4px 7px; border-radius: 3px;
351
+ white-space: nowrap;
352
+ box-shadow: 0 2px 6px rgba(0,0,0,0.2);
353
+ }
354
+ .hover-tag .comp { color: #fbbf24; margin-right: 6px; font-weight: 600; }
355
+ .hover-tag .tag { color: #93c5fd; margin-right: 6px; }
356
+ .hover-tag .src { color: #cbd5e1; }
357
+ .moveable-container { pointer-events: auto; }
358
+ .pending-panel {
359
+ position: fixed; display: none;
360
+ top: 56px; right: 12px;
361
+ flex-direction: column; gap: 8px;
362
+ background: #0f172a; color: white;
363
+ font: 12px/1.3 system-ui, sans-serif;
364
+ padding: 10px 12px; border-radius: 8px;
365
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
366
+ pointer-events: auto;
367
+ min-width: 240px;
368
+ }
369
+ .pending-header {
370
+ font: 11px/1.2 ui-monospace, monospace;
371
+ color: #94a3b8;
372
+ }
373
+ .pending-body {
374
+ display: flex; align-items: center; gap: 8px;
375
+ font: 12px/1.2 ui-monospace, monospace;
376
+ }
377
+ .pending-body .before { color: #f87171; background: rgba(248,113,113,0.1); padding: 2px 6px; border-radius: 3px; }
378
+ .pending-body .arrow { color: #94a3b8; }
379
+ .pending-body .after { color: #4ade80; background: rgba(74,222,128,0.1); padding: 2px 6px; border-radius: 3px; font-weight: 600; }
380
+ .pending-body .resolved { color: #94a3b8; font-size: 11px; margin-left: auto; }
381
+ .pending-instances {
382
+ font: 11px/1.3 system-ui;
383
+ color: #fbbf24;
384
+ background: rgba(251, 191, 36, 0.1);
385
+ padding: 6px 8px;
386
+ border-radius: 4px;
387
+ border-left: 3px solid #fbbf24;
388
+ }
389
+ .instance-outline {
390
+ position: fixed;
391
+ pointer-events: none;
392
+ box-sizing: border-box;
393
+ border: 1.5px dashed #fbbf24;
394
+ border-radius: 2px;
395
+ background: rgba(251, 191, 36, 0.05);
396
+ }
397
+ .indicator-pad {
398
+ position: fixed; display: none;
399
+ pointer-events: none;
400
+ background: rgba(147, 196, 125, 0.45);
401
+ }
402
+ .indicator-margin {
403
+ position: fixed; display: none;
404
+ pointer-events: none;
405
+ background: rgba(246, 178, 107, 0.40);
406
+ }
407
+ .anchor-outline {
408
+ position: fixed; display: none;
409
+ pointer-events: none;
410
+ box-sizing: border-box;
411
+ border: 1.5px solid #14b8a6;
412
+ background: rgba(20, 184, 166, 0.06);
413
+ border-radius: 2px;
414
+ }
415
+ .distance-label {
416
+ position: fixed; display: none;
417
+ pointer-events: none;
418
+ background: #be185d; color: white;
419
+ font: 11px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
420
+ padding: 4px 7px; border-radius: 3px;
421
+ box-shadow: 0 2px 6px rgba(0,0,0,0.25);
422
+ white-space: nowrap;
423
+ }
424
+ .distance-line {
425
+ position: fixed; display: none;
426
+ pointer-events: none;
427
+ background: #ec4899;
428
+ }
429
+ /* Padding handles sit ON the inner edge of the padding band.
430
+ Horizontal bars for top/bottom (you drag them vertically),
431
+ vertical bars for left/right (you drag them horizontally).
432
+ Subtle teal \u2014 the green padding indicator band is the
433
+ "what's being adjusted" cue; the handle itself is just
434
+ the grip. */
435
+ .padding-handle {
436
+ position: fixed; display: none;
437
+ pointer-events: auto;
438
+ background: #0d9488;
439
+ border: 1px solid white;
440
+ border-radius: 2px;
441
+ box-shadow: 0 1px 3px rgba(0,0,0,0.35);
442
+ z-index: 2;
443
+ touch-action: none;
444
+ transition: background 100ms ease;
445
+ }
446
+ .padding-handle:hover { background: #0f766e; }
447
+ .padding-handle-top, .padding-handle-bottom {
448
+ width: 16px; height: 4px;
449
+ margin-left: -8px; margin-top: -2px;
450
+ cursor: ns-resize;
451
+ }
452
+ .padding-handle-left, .padding-handle-right {
453
+ width: 4px; height: 16px;
454
+ margin-left: -2px; margin-top: -8px;
455
+ cursor: ew-resize;
456
+ }
457
+ .pending-actions { display: flex; gap: 6px; }
458
+ .pending-actions button {
459
+ font: 12px/1 system-ui; padding: 6px 10px; border-radius: 4px;
460
+ border: none; cursor: pointer;
461
+ }
462
+ .btn-apply { background: #4ade80; color: #052e16; font-weight: 600; }
463
+ .btn-apply:hover { background: #22c55e; }
464
+ .btn-discard { background: #334155; color: white; }
465
+ .btn-discard:hover { background: #475569; }
466
+ .pending-result {
467
+ font: 11px/1.3 system-ui;
468
+ padding: 6px 8px; border-radius: 4px;
469
+ }
470
+ .pending-result.success { background: rgba(74,222,128,0.15); color: #4ade80; }
471
+ .pending-result.error { background: rgba(248,113,113,0.15); color: #f87171; }
472
+ .btn-undo { background: #1e293b; color: white; border: 1px solid #334155; }
473
+ .btn-undo:hover { background: #334155; border-color: #475569; }
474
+ /* Shortcuts hint badge \u2014 shown when a target is selected so the user
475
+ immediately knows what's editable on this specific element. */
476
+ .shortcuts-hint {
477
+ position: fixed; display: none;
478
+ bottom: 12px; left: 12px;
479
+ background: #0f172a; color: white;
480
+ font: 11px/1.4 system-ui, sans-serif;
481
+ padding: 8px 12px; border-radius: 6px;
482
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
483
+ max-width: 320px;
484
+ pointer-events: auto;
485
+ }
486
+ .shortcuts-hint .hint-title {
487
+ font-weight: 600; color: #fbbf24;
488
+ margin-bottom: 4px;
489
+ font-size: 11px;
490
+ }
491
+ .shortcuts-hint kbd {
492
+ display: inline-block;
493
+ background: #1e293b;
494
+ color: #e2e8f0;
495
+ padding: 1px 5px;
496
+ border-radius: 3px;
497
+ font: 10px/1 ui-monospace, monospace;
498
+ border: 1px solid #334155;
499
+ margin: 0 1px;
500
+ }
501
+ .shortcuts-hint .disabled { color: #64748b; }
502
+ .shortcuts-hint .available { color: #cbd5e1; }
503
+ .badge { cursor: pointer; user-select: none; }
504
+ /* B6: history panel showing recent applies. Slides in from the
505
+ right when the user clicks the "visual-editor on" badge. */
506
+ .history-panel {
507
+ position: fixed; display: none;
508
+ top: 52px; right: 12px;
509
+ width: 320px;
510
+ max-height: calc(100vh - 80px);
511
+ background: #0f172a; color: white;
512
+ font: 12px/1.3 system-ui, sans-serif;
513
+ padding: 10px 12px;
514
+ border-radius: 8px;
515
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
516
+ pointer-events: auto;
517
+ flex-direction: column;
518
+ gap: 6px;
519
+ overflow: hidden;
520
+ }
521
+ .history-title {
522
+ display: flex; align-items: center; justify-content: space-between;
523
+ font-weight: 600; color: #fbbf24;
524
+ font-size: 11px;
525
+ padding-bottom: 4px;
526
+ border-bottom: 1px solid #1e293b;
527
+ }
528
+ .history-list { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; max-height: calc(100vh - 140px); }
529
+ .history-row {
530
+ display: grid;
531
+ grid-template-columns: 1fr auto;
532
+ align-items: center;
533
+ gap: 4px;
534
+ padding: 6px 8px;
535
+ background: #1e293b;
536
+ border-radius: 4px;
537
+ }
538
+ .history-row:hover { background: #243044; }
539
+ .history-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
540
+ .history-source {
541
+ font: 10px/1.2 ui-monospace, monospace;
542
+ color: #94a3b8;
543
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
544
+ }
545
+ .history-tokens {
546
+ font: 11px/1.2 ui-monospace, monospace;
547
+ }
548
+ .history-tokens .before { color: #f87171; background: rgba(248,113,113,0.1); padding: 1px 4px; border-radius: 2px; }
549
+ .history-tokens .arrow { color: #64748b; margin: 0 4px; }
550
+ .history-tokens .after { color: #4ade80; background: rgba(74,222,128,0.1); padding: 1px 4px; border-radius: 2px; font-weight: 600; }
551
+ .history-time {
552
+ font: 10px/1.2 system-ui;
553
+ color: #64748b;
554
+ margin-top: 2px;
555
+ }
556
+ .btn-undo-row {
557
+ background: transparent;
558
+ border: 1px solid #334155;
559
+ color: #cbd5e1;
560
+ font: 11px/1 system-ui;
561
+ padding: 4px 8px;
562
+ border-radius: 3px;
563
+ cursor: pointer;
564
+ }
565
+ .btn-undo-row:hover { background: #334155; }
566
+ .history-empty {
567
+ color: #64748b;
568
+ text-align: center;
569
+ font-size: 11px;
570
+ padding: 16px 8px;
571
+ }
572
+ /* B8: asset picker panel \u2014 opens on 'i' when an img is selected. */
573
+ .asset-picker {
574
+ position: fixed; display: none;
575
+ top: 12px; right: 12px;
576
+ width: 320px;
577
+ max-height: calc(100vh - 80px);
578
+ background: #0f172a; color: white;
579
+ font: 12px/1.3 system-ui, sans-serif;
580
+ padding: 10px 12px;
581
+ border-radius: 8px;
582
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
583
+ pointer-events: auto;
584
+ flex-direction: column;
585
+ gap: 6px;
586
+ overflow: hidden;
587
+ z-index: 3;
588
+ }
589
+ .asset-picker-title {
590
+ display: flex; align-items: center; justify-content: space-between;
591
+ font-weight: 600; color: #fbbf24;
592
+ font-size: 11px;
593
+ padding-bottom: 4px;
594
+ border-bottom: 1px solid #1e293b;
595
+ }
596
+ .asset-picker-list {
597
+ display: flex; flex-direction: column; gap: 2px;
598
+ overflow-y: auto; max-height: calc(100vh - 160px);
599
+ }
600
+ .asset-row {
601
+ display: flex; align-items: center; gap: 8px;
602
+ padding: 6px 8px;
603
+ background: #1e293b;
604
+ border-radius: 4px;
605
+ cursor: pointer;
606
+ font: 11px/1.2 ui-monospace, monospace;
607
+ }
608
+ .asset-row:hover { background: #334155; }
609
+ .asset-row.current { border: 1px solid #4ade80; }
610
+ .asset-row .current-badge { color: #4ade80; font-size: 9px; font-family: system-ui; }
611
+ .asset-empty {
612
+ color: #64748b; text-align: center; padding: 16px; font-size: 11px;
613
+ }
614
+ /* B2b: CSS Module mutation panel \u2014 opens automatically when the
615
+ selected element has a data-css-module-class attribute (stamped
616
+ by the Babel plugin). Distinct from the Tailwind pending panel
617
+ because the input is a CSS property + value, not a class token. */
618
+ .css-panel {
619
+ position: fixed; display: none;
620
+ top: 56px; left: 12px;
621
+ width: 320px;
622
+ background: #0f172a; color: white;
623
+ font: 12px/1.3 system-ui, sans-serif;
624
+ padding: 10px 12px;
625
+ border-radius: 8px;
626
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
627
+ pointer-events: auto;
628
+ flex-direction: column;
629
+ gap: 8px;
630
+ z-index: 3;
631
+ }
632
+ .css-panel-title {
633
+ font-weight: 600; color: #f472b6;
634
+ font-size: 11px;
635
+ padding-bottom: 4px;
636
+ border-bottom: 1px solid #1e293b;
637
+ }
638
+ .css-panel-meta {
639
+ font: 10px/1.3 ui-monospace, monospace;
640
+ color: #94a3b8;
641
+ overflow: hidden;
642
+ text-overflow: ellipsis;
643
+ white-space: nowrap;
644
+ }
645
+ .css-input-row {
646
+ display: flex; gap: 6px;
647
+ }
648
+ .css-input-row .label {
649
+ color: #94a3b8; font-size: 10px;
650
+ display: flex; align-items: center;
651
+ min-width: 56px;
652
+ }
653
+ .css-input-row input {
654
+ flex: 1;
655
+ background: #1e293b;
656
+ color: white;
657
+ border: 1px solid #334155;
658
+ border-radius: 3px;
659
+ padding: 4px 6px;
660
+ font: 11px/1.2 ui-monospace, monospace;
661
+ }
662
+ .css-input-row input:focus { outline: none; border-color: #60a5fa; }
663
+ .css-panel-actions { display: flex; gap: 6px; }
664
+ .css-panel-actions button {
665
+ font: 12px/1 system-ui; padding: 6px 10px; border-radius: 4px;
666
+ border: none; cursor: pointer;
667
+ }
668
+ .btn-css-apply { background: #4ade80; color: #052e16; font-weight: 600; }
669
+ .btn-css-apply:hover { background: #22c55e; }
670
+ .btn-css-close { background: #334155; color: white; }
671
+ .btn-css-close:hover { background: #475569; }
672
+ .css-panel-result {
673
+ font: 11px/1.3 system-ui;
674
+ padding: 6px 8px; border-radius: 4px;
675
+ word-break: break-word;
676
+ }
677
+ .css-panel-result.success { background: rgba(74,222,128,0.15); color: #4ade80; }
678
+ .css-panel-result.error { background: rgba(248,113,113,0.15); color: #f87171; }
679
+ `;
680
+ shadow.appendChild(style);
681
+ const ui = document.createElement("div");
682
+ shadow.appendChild(ui);
683
+ render(h("div", { className: "badge" }, "visual-editor on"), ui);
684
+ const historyPanel = document.createElement("div");
685
+ historyPanel.className = "history-panel";
686
+ shadow.appendChild(historyPanel);
687
+ const relativeTime = (ms) => {
688
+ const diff = Date.now() - ms;
689
+ if (diff < 5e3) return "just now";
690
+ if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
691
+ if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
692
+ if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
693
+ return `${Math.round(diff / 864e5)}d ago`;
694
+ };
695
+ const renderHistory = async () => {
696
+ let applies = [];
697
+ try {
698
+ const res = await authedFetch(`${SERVER_URL}/recent`, { method: "GET" });
699
+ if (res.ok) {
700
+ const body = await res.json();
701
+ applies = body.applies ?? [];
702
+ }
703
+ } catch {
704
+ }
705
+ historyPanel.innerHTML = "";
706
+ const title = document.createElement("div");
707
+ title.className = "history-title";
708
+ title.innerHTML = `<span>Recent edits</span><span>${applies.length}</span>`;
709
+ historyPanel.appendChild(title);
710
+ if (applies.length === 0) {
711
+ const empty = document.createElement("div");
712
+ empty.className = "history-empty";
713
+ empty.textContent = "No edits yet. Drag a handle, press a shortcut, or click an element to start.";
714
+ historyPanel.appendChild(empty);
715
+ return;
716
+ }
717
+ const list = document.createElement("div");
718
+ list.className = "history-list";
719
+ const sorted = [...applies].sort((a, b) => b.appliedAt - a.appliedAt);
720
+ for (const a of sorted) {
721
+ const row = document.createElement("div");
722
+ row.className = "history-row";
723
+ const main = document.createElement("div");
724
+ main.className = "history-row-main";
725
+ const src = document.createElement("div");
726
+ src.className = "history-source";
727
+ src.textContent = `${a.file}:${a.line}:${a.col}`;
728
+ const tokens = document.createElement("div");
729
+ tokens.className = "history-tokens";
730
+ tokens.innerHTML = `<span class="before">${a.before}</span><span class="arrow">\u2192</span><span class="after">${a.after}</span>`;
731
+ const time = document.createElement("div");
732
+ time.className = "history-time";
733
+ time.textContent = relativeTime(a.appliedAt);
734
+ main.appendChild(src);
735
+ main.appendChild(tokens);
736
+ main.appendChild(time);
737
+ const undo = document.createElement("button");
738
+ undo.className = "btn-undo-row";
739
+ undo.textContent = "Undo";
740
+ undo.dataset.file = a.file;
741
+ undo.dataset.line = String(a.line);
742
+ undo.dataset.col = String(a.col);
743
+ row.appendChild(main);
744
+ row.appendChild(undo);
745
+ list.appendChild(row);
746
+ }
747
+ historyPanel.appendChild(list);
748
+ };
749
+ const assetPicker = document.createElement("div");
750
+ assetPicker.className = "asset-picker";
751
+ shadow.appendChild(assetPicker);
752
+ const cssPanel = document.createElement("div");
753
+ cssPanel.className = "css-panel";
754
+ shadow.appendChild(cssPanel);
755
+ let cssPanelTarget = null;
756
+ const hideCssPanel = () => {
757
+ cssPanel.style.display = "none";
758
+ cssPanelTarget = null;
759
+ };
760
+ const showCssPanel = (el) => {
761
+ const isCssModule = el.hasAttribute("data-css-module-class");
762
+ const isStyled = el.hasAttribute("data-styled-name");
763
+ if (!isCssModule && !isStyled) return;
764
+ const mode = isCssModule ? "css-module" : "styled-component";
765
+ const endpoint = mode === "css-module" ? "/apply-css-prop" : "/apply-styled-prop";
766
+ const cssClass = el.getAttribute("data-css-module-class");
767
+ const cssFile = el.getAttribute("data-css-module-file");
768
+ const styledName = el.getAttribute("data-styled-name");
769
+ const styledTag = el.getAttribute("data-styled-tag");
770
+ const oid = el.getAttribute("data-oid");
771
+ if (!oid) return;
772
+ const parts = oid.split(":");
773
+ if (parts.length < 3) return;
774
+ const jsxFile = parts.slice(0, -2).join(":");
775
+ const line = parseInt(parts[parts.length - 2], 10);
776
+ const col = parseInt(parts[parts.length - 1], 10);
777
+ cssPanelTarget = el;
778
+ cssPanel.innerHTML = "";
779
+ const title = document.createElement("div");
780
+ title.className = "css-panel-title";
781
+ title.textContent = mode === "css-module" ? "CSS Module" : "styled-components";
782
+ cssPanel.appendChild(title);
783
+ const meta = document.createElement("div");
784
+ meta.className = "css-panel-meta";
785
+ meta.textContent = mode === "css-module" ? `.${cssClass} in ${cssFile}` : `${styledName} = styled.${styledTag}\`\u2026\``;
786
+ cssPanel.appendChild(meta);
787
+ const mkRow = (label, placeholder, cls) => {
788
+ const row = document.createElement("div");
789
+ row.className = "css-input-row";
790
+ const lab = document.createElement("span");
791
+ lab.className = "label";
792
+ lab.textContent = label;
793
+ const input = document.createElement("input");
794
+ input.type = "text";
795
+ input.className = cls;
796
+ input.placeholder = placeholder;
797
+ input.autocomplete = "off";
798
+ input.spellcheck = false;
799
+ row.appendChild(lab);
800
+ row.appendChild(input);
801
+ cssPanel.appendChild(row);
802
+ return input;
803
+ };
804
+ const propInput = mkRow("property", "padding", "css-prop-input");
805
+ const valInput = mkRow("value", "1.5rem", "css-val-input");
806
+ const actions = document.createElement("div");
807
+ actions.className = "css-panel-actions";
808
+ const applyBtn = document.createElement("button");
809
+ applyBtn.className = "btn-css-apply";
810
+ applyBtn.textContent = "Apply";
811
+ const closeBtn = document.createElement("button");
812
+ closeBtn.className = "btn-css-close";
813
+ closeBtn.textContent = "Close";
814
+ actions.appendChild(applyBtn);
815
+ actions.appendChild(closeBtn);
816
+ cssPanel.appendChild(actions);
817
+ const showResultLine = (text, kind) => {
818
+ const existing = cssPanel.querySelector(".css-panel-result");
819
+ existing?.remove();
820
+ const r = document.createElement("div");
821
+ r.className = `css-panel-result ${kind}`;
822
+ r.textContent = text;
823
+ cssPanel.appendChild(r);
824
+ };
825
+ applyBtn.addEventListener("click", async () => {
826
+ const property = propInput.value.trim();
827
+ const value = valInput.value.trim();
828
+ if (!property || !value) {
829
+ showResultLine("Both property and value are required.", "error");
830
+ return;
831
+ }
832
+ try {
833
+ const res = await authedFetch(`${SERVER_URL}${endpoint}`, {
834
+ method: "POST",
835
+ body: JSON.stringify({
836
+ file: jsxFile,
837
+ line,
838
+ col,
839
+ property,
840
+ value
841
+ })
842
+ });
843
+ const body = await res.json();
844
+ if (res.ok && body.ok) {
845
+ const target = "selector" in body && body.selector ? body.selector : "componentName" in body && body.componentName ? body.componentName : "(target)";
846
+ const prev = body.previousValue ? `${property}: ${body.previousValue} \u2192 ${value}` : `${property}: ${value} (inserted)`;
847
+ showResultLine(`Applied to ${target} \u2014 ${prev}`, "success");
848
+ propInput.value = "";
849
+ valInput.value = "";
850
+ propInput.focus();
851
+ } else if (!body.ok) {
852
+ showResultLine(`Refused (${body.reason}): ${body.details}`, "error");
853
+ }
854
+ } catch (err) {
855
+ showResultLine(`Network error: ${err.message}`, "error");
856
+ }
857
+ });
858
+ closeBtn.addEventListener("click", () => {
859
+ hideCssPanel();
860
+ });
861
+ cssPanel.style.display = "flex";
862
+ window.setTimeout(() => propInput.focus(), 0);
863
+ };
864
+ const renderAssetPicker = async (imgEl) => {
865
+ let assets = [];
866
+ try {
867
+ const res = await authedFetch(`${SERVER_URL}/assets`, { method: "GET" });
868
+ if (res.ok) {
869
+ const body = await res.json();
870
+ assets = body.assets ?? [];
871
+ }
872
+ } catch {
873
+ }
874
+ const currentSrc = imgEl.getAttribute("src") ?? "";
875
+ assetPicker.innerHTML = "";
876
+ const title = document.createElement("div");
877
+ title.className = "asset-picker-title";
878
+ title.innerHTML = `<span>Replace image</span><span>${assets.length} available</span>`;
879
+ assetPicker.appendChild(title);
880
+ if (assets.length === 0) {
881
+ const empty = document.createElement("div");
882
+ empty.className = "asset-empty";
883
+ empty.textContent = "No images found under public/. Put a .png/.jpg/.svg/.webp/.avif/.gif there.";
884
+ assetPicker.appendChild(empty);
885
+ assetPicker.style.display = "flex";
886
+ return;
887
+ }
888
+ const list = document.createElement("div");
889
+ list.className = "asset-picker-list";
890
+ for (const a of assets) {
891
+ const row = document.createElement("div");
892
+ row.className = "asset-row" + (a === currentSrc ? " current" : "");
893
+ row.dataset.asset = a;
894
+ row.innerHTML = `<span>${a}</span>${a === currentSrc ? "<span class='current-badge'>current</span>" : ""}`;
895
+ list.appendChild(row);
896
+ }
897
+ assetPicker.appendChild(list);
898
+ assetPicker.style.display = "flex";
899
+ };
900
+ const hideAssetPicker = () => {
901
+ assetPicker.style.display = "none";
902
+ };
903
+ assetPicker.addEventListener("click", async (e) => {
904
+ const row = e.target?.closest(
905
+ ".asset-row"
906
+ );
907
+ if (!row || !lastSelected) return;
908
+ const picked = row.dataset.asset;
909
+ if (!picked) return;
910
+ const oid = lastSelected.getAttribute("data-oid");
911
+ if (!oid) return;
912
+ const parts = oid.split(":");
913
+ if (parts.length < 3) return;
914
+ const file = parts.slice(0, -2).join(":");
915
+ const line = parseInt(parts[parts.length - 2], 10);
916
+ const col = parseInt(parts[parts.length - 1], 10);
917
+ try {
918
+ const res = await authedFetch(`${SERVER_URL}/apply`, {
919
+ method: "POST",
920
+ body: JSON.stringify({
921
+ file,
922
+ line,
923
+ col,
924
+ attribute: "src",
925
+ before: null,
926
+ after: picked
927
+ })
928
+ });
929
+ const body = await res.json();
930
+ if (res.ok && body.ok) {
931
+ lastSelected.src = picked;
932
+ hideAssetPicker();
933
+ showResult(`Swapped image to ${picked}`, "success");
934
+ } else if (!body.ok) {
935
+ showResult(`Refused (${body.reason}): ${body.details}`, "error");
936
+ }
937
+ } catch (err) {
938
+ showResult(
939
+ `Network error: ${err.message}`,
940
+ "error"
941
+ );
942
+ }
943
+ });
944
+ historyPanel.addEventListener("click", async (e) => {
945
+ const target = e.target;
946
+ if (!target || !target.classList.contains("btn-undo-row")) return;
947
+ const file = target.dataset.file;
948
+ const line = Number(target.dataset.line);
949
+ const col = Number(target.dataset.col);
950
+ try {
951
+ await authedFetch(`${SERVER_URL}/revert`, {
952
+ method: "POST",
953
+ body: JSON.stringify({ file, line, col })
954
+ });
955
+ await renderHistory();
956
+ } catch {
957
+ }
958
+ });
959
+ const badgeEl = shadow.querySelector(".badge");
960
+ if (badgeEl) {
961
+ badgeEl.addEventListener("click", async (e) => {
962
+ e.stopPropagation();
963
+ const isOpen = historyPanel.style.display === "flex";
964
+ if (isOpen) {
965
+ historyPanel.style.display = "none";
966
+ } else {
967
+ await renderHistory();
968
+ historyPanel.style.display = "flex";
969
+ }
970
+ });
971
+ }
972
+ const hoverOutline = document.createElement("div");
973
+ hoverOutline.className = "hover-outline";
974
+ shadow.appendChild(hoverOutline);
975
+ const hoverTag = document.createElement("div");
976
+ hoverTag.className = "hover-tag";
977
+ shadow.appendChild(hoverTag);
978
+ const makeIndicator = (cls) => {
979
+ const d = document.createElement("div");
980
+ d.className = cls;
981
+ shadow.appendChild(d);
982
+ return d;
983
+ };
984
+ const indicators = {
985
+ padTop: makeIndicator("indicator-pad"),
986
+ padRight: makeIndicator("indicator-pad"),
987
+ padBottom: makeIndicator("indicator-pad"),
988
+ padLeft: makeIndicator("indicator-pad"),
989
+ marTop: makeIndicator("indicator-margin"),
990
+ marRight: makeIndicator("indicator-margin"),
991
+ marBottom: makeIndicator("indicator-margin"),
992
+ marLeft: makeIndicator("indicator-margin")
993
+ };
994
+ const setBand = (el, visible, left, top, width, height) => {
995
+ if (!visible || width <= 0 || height <= 0) {
996
+ el.style.display = "none";
997
+ return;
998
+ }
999
+ el.style.display = "block";
1000
+ el.style.left = `${left}px`;
1001
+ el.style.top = `${top}px`;
1002
+ el.style.width = `${width}px`;
1003
+ el.style.height = `${height}px`;
1004
+ };
1005
+ const clearIndicators = () => {
1006
+ for (const el of Object.values(indicators)) el.style.display = "none";
1007
+ };
1008
+ const updateIndicators = (el, rect) => {
1009
+ const cs = getComputedStyle(el);
1010
+ const p = {
1011
+ t: parseFloat(cs.paddingTop) || 0,
1012
+ r: parseFloat(cs.paddingRight) || 0,
1013
+ b: parseFloat(cs.paddingBottom) || 0,
1014
+ l: parseFloat(cs.paddingLeft) || 0
1015
+ };
1016
+ const m = {
1017
+ t: parseFloat(cs.marginTop) || 0,
1018
+ r: parseFloat(cs.marginRight) || 0,
1019
+ b: parseFloat(cs.marginBottom) || 0,
1020
+ l: parseFloat(cs.marginLeft) || 0
1021
+ };
1022
+ setBand(indicators.padTop, p.t > 0, rect.left, rect.top, rect.width, p.t);
1023
+ setBand(
1024
+ indicators.padBottom,
1025
+ p.b > 0,
1026
+ rect.left,
1027
+ rect.bottom - p.b,
1028
+ rect.width,
1029
+ p.b
1030
+ );
1031
+ setBand(
1032
+ indicators.padLeft,
1033
+ p.l > 0,
1034
+ rect.left,
1035
+ rect.top + p.t,
1036
+ p.l,
1037
+ rect.height - p.t - p.b
1038
+ );
1039
+ setBand(
1040
+ indicators.padRight,
1041
+ p.r > 0,
1042
+ rect.right - p.r,
1043
+ rect.top + p.t,
1044
+ p.r,
1045
+ rect.height - p.t - p.b
1046
+ );
1047
+ setBand(
1048
+ indicators.marTop,
1049
+ m.t > 0,
1050
+ rect.left - m.l,
1051
+ rect.top - m.t,
1052
+ rect.width + m.l + m.r,
1053
+ m.t
1054
+ );
1055
+ setBand(
1056
+ indicators.marBottom,
1057
+ m.b > 0,
1058
+ rect.left - m.l,
1059
+ rect.bottom,
1060
+ rect.width + m.l + m.r,
1061
+ m.b
1062
+ );
1063
+ setBand(indicators.marLeft, m.l > 0, rect.left - m.l, rect.top, m.l, rect.height);
1064
+ setBand(indicators.marRight, m.r > 0, rect.right, rect.top, m.r, rect.height);
1065
+ };
1066
+ const anchorOutline = document.createElement("div");
1067
+ anchorOutline.className = "anchor-outline";
1068
+ shadow.appendChild(anchorOutline);
1069
+ const distanceLabel = document.createElement("div");
1070
+ distanceLabel.className = "distance-label";
1071
+ shadow.appendChild(distanceLabel);
1072
+ const distanceLineH = document.createElement("div");
1073
+ distanceLineH.className = "distance-line";
1074
+ shadow.appendChild(distanceLineH);
1075
+ const distanceLineV = document.createElement("div");
1076
+ distanceLineV.className = "distance-line";
1077
+ shadow.appendChild(distanceLineV);
1078
+ const measureSet = [];
1079
+ const moveableContainer = document.createElement("div");
1080
+ moveableContainer.className = "moveable-container";
1081
+ shadow.appendChild(moveableContainer);
1082
+ const shortcutsHint = document.createElement("div");
1083
+ shortcutsHint.className = "shortcuts-hint";
1084
+ shadow.appendChild(shortcutsHint);
1085
+ const updateShortcutsHint = (el) => {
1086
+ const tokens = el.className.split(/\s+/).filter(Boolean);
1087
+ const numericSuffix = /^(-?\d+(?:\.\d+)?|\[[^\]]+\])$/;
1088
+ const hasPrefix = (prefixes) => tokens.some(
1089
+ (t) => prefixes.some(
1090
+ (p) => t.startsWith(p + "-") && numericSuffix.test(t.slice(p.length + 1))
1091
+ )
1092
+ );
1093
+ const hasW = hasPrefix(["w"]);
1094
+ const hasH = hasPrefix(["h"]);
1095
+ const hasPad = hasPrefix(["p", "px", "py", "pt", "pr", "pb", "pl"]);
1096
+ const hasMar = hasPrefix(["m", "mx", "my", "mt", "mr", "mb", "ml"]);
1097
+ const hasGap = hasPrefix(["gap", "gap-x", "gap-y"]);
1098
+ const line = (label, keys, enabled) => `<div class="${enabled ? "available" : "disabled"}">${keys} \u2192 ${label}${enabled ? "" : " (no class on this element)"}</div>`;
1099
+ const isImg = el.tagName.toLowerCase() === "img";
1100
+ shortcutsHint.innerHTML = `<div class="hint-title">This element is editable via:</div>` + line("resize width / height", "drag side handles", hasW || hasH) + line("padding (per side)", "drag teal bars \xB7 <kbd>]</kbd> <kbd>[</kbd>", hasPad) + line("margin", "<kbd>}</kbd> <kbd>{</kbd> (shift+])", hasMar) + line("gap (flex/grid)", "<kbd>alt+]</kbd> <kbd>alt+[</kbd>", hasGap) + line("width / height nudge", "<kbd>alt+\u2192</kbd> <kbd>alt+\u2193</kbd>", hasW || hasH) + line("replace image asset", "<kbd>i</kbd>", isImg) + `<div class="disabled" style="margin-top:4px;">deselect: <kbd>esc</kbd> \xB7 alt-hover for distance \xB7 shift-click to set anchor</div>`;
1101
+ shortcutsHint.style.display = "block";
1102
+ };
1103
+ const hideShortcutsHint = () => {
1104
+ shortcutsHint.style.display = "none";
1105
+ };
1106
+ const paddingHandles = {
1107
+ top: document.createElement("div"),
1108
+ right: document.createElement("div"),
1109
+ bottom: document.createElement("div"),
1110
+ left: document.createElement("div")
1111
+ };
1112
+ for (const side of ["top", "right", "bottom", "left"]) {
1113
+ const h2 = paddingHandles[side];
1114
+ h2.className = `padding-handle padding-handle-${side}`;
1115
+ shadow.appendChild(h2);
1116
+ }
1117
+ const hidePaddingHandles = () => {
1118
+ for (const side of ["top", "right", "bottom", "left"]) {
1119
+ paddingHandles[side].style.display = "none";
1120
+ }
1121
+ };
1122
+ const positionPaddingHandles = (el) => {
1123
+ const rect = el.getBoundingClientRect();
1124
+ const cs = getComputedStyle(el);
1125
+ const pt = parseFloat(cs.paddingTop) || 0;
1126
+ const pr = parseFloat(cs.paddingRight) || 0;
1127
+ const pb = parseFloat(cs.paddingBottom) || 0;
1128
+ const pl = parseFloat(cs.paddingLeft) || 0;
1129
+ const hasAnyPadding = pt > 0 || pr > 0 || pb > 0 || pl > 0;
1130
+ if (!hasAnyPadding) {
1131
+ hidePaddingHandles();
1132
+ return;
1133
+ }
1134
+ const cx = rect.left + rect.width / 2;
1135
+ const cy = rect.top + rect.height / 2;
1136
+ const showAt = (h2, x, y) => {
1137
+ h2.style.display = "block";
1138
+ h2.style.left = `${x}px`;
1139
+ h2.style.top = `${y}px`;
1140
+ };
1141
+ if (pt > 0) showAt(paddingHandles.top, cx, rect.top + pt);
1142
+ else paddingHandles.top.style.display = "none";
1143
+ if (pb > 0) showAt(paddingHandles.bottom, cx, rect.bottom - pb);
1144
+ else paddingHandles.bottom.style.display = "none";
1145
+ if (pl > 0) showAt(paddingHandles.left, rect.left + pl, cy);
1146
+ else paddingHandles.left.style.display = "none";
1147
+ if (pr > 0) showAt(paddingHandles.right, rect.right - pr, cy);
1148
+ else paddingHandles.right.style.display = "none";
1149
+ };
1150
+ const cssSideKey = (side) => ({
1151
+ top: "paddingTop",
1152
+ right: "paddingRight",
1153
+ bottom: "paddingBottom",
1154
+ left: "paddingLeft"
1155
+ })[side];
1156
+ const setupPaddingDrag = (handle, side) => {
1157
+ handle.addEventListener("pointerdown", (ev) => {
1158
+ if (!lastSelected) return;
1159
+ ev.preventDefault();
1160
+ ev.stopPropagation();
1161
+ const target = lastSelected;
1162
+ const cssKey = cssSideKey(side);
1163
+ const startX = ev.clientX;
1164
+ const startY = ev.clientY;
1165
+ const startPad = parseFloat(getComputedStyle(target)[cssKey]) || 0;
1166
+ handle.setPointerCapture(ev.pointerId);
1167
+ const onMove = (mv) => {
1168
+ let delta = 0;
1169
+ if (side === "top") delta = mv.clientY - startY;
1170
+ else if (side === "bottom") delta = -(mv.clientY - startY);
1171
+ else if (side === "left") delta = mv.clientX - startX;
1172
+ else if (side === "right") delta = -(mv.clientX - startX);
1173
+ const next = Math.max(0, startPad + delta);
1174
+ target.style[cssKey] = `${next}px`;
1175
+ positionPaddingHandles(target);
1176
+ };
1177
+ const onUp = (mv) => {
1178
+ handle.releasePointerCapture(mv.pointerId);
1179
+ document.removeEventListener("pointermove", onMove);
1180
+ document.removeEventListener("pointerup", onUp);
1181
+ const finalPx = parseFloat(getComputedStyle(target)[cssKey]) || 0;
1182
+ proposePaddingSideChange(target, side, finalPx);
1183
+ };
1184
+ document.addEventListener("pointermove", onMove);
1185
+ document.addEventListener("pointerup", onUp);
1186
+ });
1187
+ };
1188
+ for (const side of ["top", "right", "bottom", "left"]) {
1189
+ setupPaddingDrag(paddingHandles[side], side);
1190
+ }
1191
+ const SIDE_PREFIX = {
1192
+ top: "pt",
1193
+ right: "pr",
1194
+ bottom: "pb",
1195
+ left: "pl"
1196
+ };
1197
+ const AXIS_PREFIX = {
1198
+ top: "py",
1199
+ bottom: "py",
1200
+ left: "px",
1201
+ right: "px"
1202
+ };
1203
+ const proposePaddingSideChange = (target, side, newPx) => {
1204
+ const sidePrefix = SIDE_PREFIX[side];
1205
+ const axisPrefix = AXIS_PREFIX[side];
1206
+ const tokens = target.className.split(/\s+/).filter(Boolean);
1207
+ const numericSuffix = /^(-?\d+(?:\.\d+)?|\[[^\]]+\])$/;
1208
+ const sideToken = tokens.find(
1209
+ (t) => t.startsWith(sidePrefix + "-") && numericSuffix.test(t.slice(sidePrefix.length + 1))
1210
+ );
1211
+ const spacingPx = getSpacingPx();
1212
+ const snap = snapToTailwind(newPx, spacingPx);
1213
+ if (sideToken) {
1214
+ const newToken = `${sidePrefix}-${snap.suffix}`;
1215
+ if (newToken === sideToken) {
1216
+ clearInlineSizing(target);
1217
+ return;
1218
+ }
1219
+ proposeTokenChange(target, sideToken, newToken, snap.resolvedPx);
1220
+ return;
1221
+ }
1222
+ const axisToken = tokens.find(
1223
+ (t) => t.startsWith(axisPrefix + "-") && numericSuffix.test(t.slice(axisPrefix.length + 1))
1224
+ );
1225
+ const shortToken = tokens.find(
1226
+ (t) => t.startsWith("p-") && numericSuffix.test(t.slice("p-".length))
1227
+ );
1228
+ const base = axisToken ?? shortToken;
1229
+ if (!base) {
1230
+ showResult(
1231
+ `This element has no padding class (no p-*, ${axisPrefix}-*, or ${sidePrefix}-*). Drag handles need an existing padding token to mutate.`,
1232
+ "error"
1233
+ );
1234
+ clearInlineSizing(target);
1235
+ return;
1236
+ }
1237
+ const overrideToken = `${sidePrefix}-${snap.suffix}`;
1238
+ proposeTokenChange(
1239
+ target,
1240
+ base,
1241
+ `${base} ${overrideToken}`,
1242
+ snap.resolvedPx
1243
+ );
1244
+ };
1245
+ const pendingPanel = document.createElement("div");
1246
+ pendingPanel.className = "pending-panel";
1247
+ shadow.appendChild(pendingPanel);
1248
+ let moveable = null;
1249
+ let lastSelected = null;
1250
+ let hoverTarget = null;
1251
+ let rafId = 0;
1252
+ let lastMouseX = 0;
1253
+ let lastMouseY = 0;
1254
+ let lastResizeWidth = null;
1255
+ let pending = null;
1256
+ const instanceOutlines = [];
1257
+ const queryInstances = (oid) => {
1258
+ const escaped = oid.replace(/"/g, '\\"');
1259
+ const nodes = document.querySelectorAll(`[data-oid="${escaped}"]`);
1260
+ const out = [];
1261
+ nodes.forEach((n) => {
1262
+ if (n instanceof HTMLElement) out.push(n);
1263
+ });
1264
+ return out;
1265
+ };
1266
+ const clearInstanceOutlines = () => {
1267
+ for (const node of instanceOutlines) node.remove();
1268
+ instanceOutlines.length = 0;
1269
+ };
1270
+ const drawInstanceOutlines = (instances, selected) => {
1271
+ clearInstanceOutlines();
1272
+ for (const el of instances) {
1273
+ if (el === selected) continue;
1274
+ const rect = el.getBoundingClientRect();
1275
+ if (rect.width === 0 && rect.height === 0) continue;
1276
+ const outline = document.createElement("div");
1277
+ outline.className = "instance-outline";
1278
+ outline.style.left = `${rect.left}px`;
1279
+ outline.style.top = `${rect.top}px`;
1280
+ outline.style.width = `${rect.width}px`;
1281
+ outline.style.height = `${rect.height}px`;
1282
+ shadow.appendChild(outline);
1283
+ instanceOutlines.push(outline);
1284
+ }
1285
+ };
1286
+ const showPending = (change, resolvedPx) => {
1287
+ pending = change;
1288
+ pendingPanel.innerHTML = "";
1289
+ hidePaddingHandles();
1290
+ const oid = change.element.getAttribute("data-oid");
1291
+ if (oid) {
1292
+ saveDraft({
1293
+ file: change.file,
1294
+ line: change.line,
1295
+ col: change.col,
1296
+ before: change.before,
1297
+ after: change.after,
1298
+ oid,
1299
+ resolvedPx
1300
+ });
1301
+ }
1302
+ const instances = oid ? queryInstances(oid) : [change.element];
1303
+ const instanceCount = instances.length;
1304
+ const header = document.createElement("div");
1305
+ header.className = "pending-header";
1306
+ header.textContent = `${change.file}:${change.line}:${change.col}`;
1307
+ const body = document.createElement("div");
1308
+ body.className = "pending-body";
1309
+ body.innerHTML = `<span class="before">${change.before}</span><span class="arrow">\u2192</span><span class="after">${change.after}</span><span class="resolved">${Math.round(resolvedPx)}px</span>`;
1310
+ pendingPanel.appendChild(header);
1311
+ pendingPanel.appendChild(body);
1312
+ if (instanceCount > 1) {
1313
+ const banner = document.createElement("div");
1314
+ banner.className = "pending-instances";
1315
+ banner.textContent = `Edits ${instanceCount} elements rendered from this source location.`;
1316
+ pendingPanel.appendChild(banner);
1317
+ drawInstanceOutlines(instances, change.element);
1318
+ } else {
1319
+ clearInstanceOutlines();
1320
+ }
1321
+ const actions = document.createElement("div");
1322
+ actions.className = "pending-actions";
1323
+ const apply = document.createElement("button");
1324
+ apply.className = "btn-apply";
1325
+ apply.textContent = instanceCount > 1 ? `Apply to ${instanceCount}` : "Apply";
1326
+ const discard = document.createElement("button");
1327
+ discard.className = "btn-discard";
1328
+ discard.textContent = "Discard";
1329
+ actions.appendChild(apply);
1330
+ actions.appendChild(discard);
1331
+ pendingPanel.appendChild(actions);
1332
+ pendingPanel.style.display = "flex";
1333
+ };
1334
+ const clearInlineSizing = (el) => {
1335
+ el.style.width = "";
1336
+ el.style.height = "";
1337
+ el.style.transform = "";
1338
+ el.style.paddingTop = "";
1339
+ el.style.paddingRight = "";
1340
+ el.style.paddingBottom = "";
1341
+ el.style.paddingLeft = "";
1342
+ };
1343
+ const hidePending = () => {
1344
+ pending = null;
1345
+ pendingPanel.style.display = "none";
1346
+ clearInstanceOutlines();
1347
+ clearDraft();
1348
+ };
1349
+ let resultDismissTimer = null;
1350
+ let lastApplied = null;
1351
+ const showResult = (message, kind) => {
1352
+ pendingPanel.innerHTML = "";
1353
+ const r = document.createElement("div");
1354
+ r.className = `pending-result ${kind}`;
1355
+ r.textContent = message;
1356
+ pendingPanel.appendChild(r);
1357
+ pendingPanel.style.display = "flex";
1358
+ if (resultDismissTimer !== null) window.clearTimeout(resultDismissTimer);
1359
+ resultDismissTimer = window.setTimeout(() => {
1360
+ if (!pending) pendingPanel.style.display = "none";
1361
+ }, 3500);
1362
+ };
1363
+ const showSuccessWithUndo = (change) => {
1364
+ lastApplied = change;
1365
+ pendingPanel.innerHTML = "";
1366
+ const r = document.createElement("div");
1367
+ r.className = "pending-result success";
1368
+ r.textContent = `Applied: ${change.before} \u2192 ${change.after}`;
1369
+ pendingPanel.appendChild(r);
1370
+ const actions = document.createElement("div");
1371
+ actions.className = "pending-actions";
1372
+ const undo = document.createElement("button");
1373
+ undo.className = "btn-undo";
1374
+ undo.textContent = "Undo";
1375
+ const dismiss = document.createElement("button");
1376
+ dismiss.className = "btn-discard";
1377
+ dismiss.textContent = "Dismiss";
1378
+ actions.appendChild(undo);
1379
+ actions.appendChild(dismiss);
1380
+ pendingPanel.appendChild(actions);
1381
+ pendingPanel.style.display = "flex";
1382
+ if (resultDismissTimer !== null) window.clearTimeout(resultDismissTimer);
1383
+ resultDismissTimer = window.setTimeout(() => {
1384
+ if (!pending) pendingPanel.style.display = "none";
1385
+ lastApplied = null;
1386
+ }, 6e3);
1387
+ };
1388
+ pendingPanel.addEventListener("click", async (e) => {
1389
+ const target = e.target;
1390
+ if (!target || !pending) return;
1391
+ const change = pending;
1392
+ if (target.classList.contains("btn-apply")) {
1393
+ try {
1394
+ const res = await authedFetch(`${SERVER_URL}/apply`, {
1395
+ method: "POST",
1396
+ body: JSON.stringify({
1397
+ file: change.file,
1398
+ line: change.line,
1399
+ col: change.col,
1400
+ before: change.before,
1401
+ after: change.after
1402
+ })
1403
+ });
1404
+ const body = await res.json();
1405
+ if (res.ok && body.ok) {
1406
+ const oid = change.element.getAttribute("data-oid");
1407
+ const targets = oid ? queryInstances(oid) : [change.element];
1408
+ for (const target2 of targets) {
1409
+ target2.className = swapClassToken(
1410
+ target2.className,
1411
+ change.before,
1412
+ change.after
1413
+ );
1414
+ clearInlineSizing(target2);
1415
+ }
1416
+ pending = null;
1417
+ clearInstanceOutlines();
1418
+ clearDraft();
1419
+ showSuccessWithUndo(change);
1420
+ if (lastSelected) positionPaddingHandles(lastSelected);
1421
+ } else if (!body.ok) {
1422
+ showResult(`Refused (${body.reason}): ${body.details}`, "error");
1423
+ }
1424
+ } catch (err) {
1425
+ showResult(
1426
+ `Network error: ${err.message}. Is visual-editor server running on :7790?`,
1427
+ "error"
1428
+ );
1429
+ }
1430
+ } else if (target.classList.contains("btn-discard")) {
1431
+ if (pending) {
1432
+ clearInlineSizing(pending.element);
1433
+ hidePending();
1434
+ } else {
1435
+ pendingPanel.style.display = "none";
1436
+ lastApplied = null;
1437
+ }
1438
+ if (lastSelected) positionPaddingHandles(lastSelected);
1439
+ } else if (target.classList.contains("btn-undo") && lastApplied) {
1440
+ const change2 = lastApplied;
1441
+ try {
1442
+ const res = await authedFetch(`${SERVER_URL}/revert`, {
1443
+ method: "POST",
1444
+ body: JSON.stringify({
1445
+ file: change2.file,
1446
+ line: change2.line,
1447
+ col: change2.col
1448
+ })
1449
+ });
1450
+ const body = await res.json();
1451
+ if (res.ok && body.ok) {
1452
+ lastApplied = null;
1453
+ showResult(`Reverted: ${change2.after} \u2192 ${change2.before}`, "success");
1454
+ } else if (!body.ok) {
1455
+ showResult(
1456
+ `Could not undo (${body.reason}): ${body.details}`,
1457
+ "error"
1458
+ );
1459
+ }
1460
+ } catch (err) {
1461
+ showResult(
1462
+ `Network error during undo: ${err.message}`,
1463
+ "error"
1464
+ );
1465
+ }
1466
+ }
1467
+ });
1468
+ const isOverlayEl = (el) => {
1469
+ if (!el) return true;
1470
+ if (el.tagName.toLowerCase() === ANCHOR_TAG) return true;
1471
+ if (el.closest(ANCHOR_TAG)) return true;
1472
+ if (el.closest("[data-nextjs-dev-tools-button]")) return true;
1473
+ if (el.closest("nextjs-portal")) return true;
1474
+ return false;
1475
+ };
1476
+ const clearDistanceOverlay = () => {
1477
+ anchorOutline.style.display = "none";
1478
+ distanceLabel.style.display = "none";
1479
+ distanceLineH.style.display = "none";
1480
+ distanceLineV.style.display = "none";
1481
+ };
1482
+ const showDistanceBetween = (anchor2, hovered) => {
1483
+ if (anchor2 === hovered) {
1484
+ clearDistanceOverlay();
1485
+ return;
1486
+ }
1487
+ const a = anchor2.getBoundingClientRect();
1488
+ const b = hovered.getBoundingClientRect();
1489
+ anchorOutline.style.display = "block";
1490
+ anchorOutline.style.left = `${a.left}px`;
1491
+ anchorOutline.style.top = `${a.top}px`;
1492
+ anchorOutline.style.width = `${a.width}px`;
1493
+ anchorOutline.style.height = `${a.height}px`;
1494
+ let dx = 0;
1495
+ if (b.left >= a.right) dx = b.left - a.right;
1496
+ else if (b.right <= a.left) dx = b.right - a.left;
1497
+ let dy = 0;
1498
+ if (b.top >= a.bottom) dy = b.top - a.bottom;
1499
+ else if (b.bottom <= a.top) dy = b.bottom - a.top;
1500
+ const ax = (a.left + a.right) / 2;
1501
+ const ay = (a.top + a.bottom) / 2;
1502
+ const bx = (b.left + b.right) / 2;
1503
+ const by = (b.top + b.bottom) / 2;
1504
+ const mx = (ax + bx) / 2;
1505
+ const my = (ay + by) / 2;
1506
+ distanceLabel.textContent = `\u2194 ${Math.round(Math.abs(dx))}px \xB7 \u2195 ${Math.round(Math.abs(dy))}px`;
1507
+ distanceLabel.style.display = "block";
1508
+ distanceLabel.style.left = `${mx + 8}px`;
1509
+ distanceLabel.style.top = `${my + 8}px`;
1510
+ const drawHLine = () => {
1511
+ const y = my;
1512
+ const x1 = Math.min(a.right, b.right) > Math.max(a.left, b.left) ? (
1513
+ // overlap horizontally — skip line, dx is 0/negative
1514
+ null
1515
+ ) : ax < bx ? a.right : a.left;
1516
+ const x2 = x1 === null ? null : ax < bx ? b.left : b.right;
1517
+ if (x1 === null || x2 === null) {
1518
+ distanceLineH.style.display = "none";
1519
+ return;
1520
+ }
1521
+ distanceLineH.style.display = "block";
1522
+ distanceLineH.style.left = `${Math.min(x1, x2)}px`;
1523
+ distanceLineH.style.top = `${y}px`;
1524
+ distanceLineH.style.width = `${Math.abs(x2 - x1)}px`;
1525
+ distanceLineH.style.height = `1px`;
1526
+ };
1527
+ const drawVLine = () => {
1528
+ const x = mx;
1529
+ const y1 = Math.min(a.bottom, b.bottom) > Math.max(a.top, b.top) ? null : ay < by ? a.bottom : a.top;
1530
+ const y2 = y1 === null ? null : ay < by ? b.top : b.bottom;
1531
+ if (y1 === null || y2 === null) {
1532
+ distanceLineV.style.display = "none";
1533
+ return;
1534
+ }
1535
+ distanceLineV.style.display = "block";
1536
+ distanceLineV.style.left = `${x}px`;
1537
+ distanceLineV.style.top = `${Math.min(y1, y2)}px`;
1538
+ distanceLineV.style.width = `1px`;
1539
+ distanceLineV.style.height = `${Math.abs(y2 - y1)}px`;
1540
+ };
1541
+ drawHLine();
1542
+ drawVLine();
1543
+ };
1544
+ const clearHover = () => {
1545
+ hoverOutline.style.display = "none";
1546
+ hoverTag.style.display = "none";
1547
+ clearIndicators();
1548
+ clearDistanceOverlay();
1549
+ hoverTarget = null;
1550
+ };
1551
+ const updateHover = (el) => {
1552
+ const rect = el.getBoundingClientRect();
1553
+ if (rect.width === 0 && rect.height === 0) {
1554
+ clearHover();
1555
+ return;
1556
+ }
1557
+ hoverOutline.style.display = "block";
1558
+ hoverOutline.style.left = `${rect.left}px`;
1559
+ hoverOutline.style.top = `${rect.top}px`;
1560
+ hoverOutline.style.width = `${rect.width}px`;
1561
+ hoverOutline.style.height = `${rect.height}px`;
1562
+ updateIndicators(el, rect);
1563
+ const oid = el.getAttribute("data-oid");
1564
+ const fiberName = getComponentName(el);
1565
+ const fileName = nameFromDataOid(oid);
1566
+ const componentName = fileName || (fiberName && !FRAMEWORK_INTERNALS.has(fiberName) ? fiberName : null);
1567
+ const tagName = el.tagName.toLowerCase();
1568
+ const oidLabel = oid || "(no data-oid)";
1569
+ hoverTag.innerHTML = "";
1570
+ if (componentName) {
1571
+ const c = document.createElement("span");
1572
+ c.className = "comp";
1573
+ c.textContent = `<${componentName}>`;
1574
+ hoverTag.appendChild(c);
1575
+ }
1576
+ const t = document.createElement("span");
1577
+ t.className = "tag";
1578
+ t.textContent = tagName;
1579
+ hoverTag.appendChild(t);
1580
+ const s = document.createElement("span");
1581
+ s.className = "src";
1582
+ s.textContent = oidLabel;
1583
+ hoverTag.appendChild(s);
1584
+ hoverTag.style.display = "block";
1585
+ const tagH = 22;
1586
+ const above = rect.top - tagH - 4;
1587
+ const below = rect.bottom + 4;
1588
+ hoverTag.style.left = `${Math.max(4, rect.left)}px`;
1589
+ hoverTag.style.top = `${above < 0 ? below : above}px`;
1590
+ };
1591
+ const onMouseMove = (e) => {
1592
+ lastMouseX = e.clientX;
1593
+ lastMouseY = e.clientY;
1594
+ const anchor2 = (moveable ? lastSelected : null) ?? measureSet[0] ?? null;
1595
+ if (e.altKey && anchor2) {
1596
+ if (rafId) return;
1597
+ rafId = requestAnimationFrame(() => {
1598
+ rafId = 0;
1599
+ const target = document.elementFromPoint(lastMouseX, lastMouseY);
1600
+ if (!(target instanceof HTMLElement) || isOverlayEl(target)) {
1601
+ clearDistanceOverlay();
1602
+ return;
1603
+ }
1604
+ showDistanceBetween(anchor2, target);
1605
+ });
1606
+ return;
1607
+ }
1608
+ clearDistanceOverlay();
1609
+ if (moveable) return;
1610
+ if (rafId) return;
1611
+ rafId = requestAnimationFrame(() => {
1612
+ rafId = 0;
1613
+ const target = document.elementFromPoint(lastMouseX, lastMouseY);
1614
+ if (!(target instanceof HTMLElement) || isOverlayEl(target)) {
1615
+ clearHover();
1616
+ return;
1617
+ }
1618
+ if (target === hoverTarget) return;
1619
+ hoverTarget = target;
1620
+ updateHover(target);
1621
+ });
1622
+ };
1623
+ const onScrollOrResize = () => {
1624
+ if (pending) {
1625
+ const oid = pending.element.getAttribute("data-oid");
1626
+ if (oid) {
1627
+ const instances = queryInstances(oid);
1628
+ drawInstanceOutlines(instances, pending.element);
1629
+ }
1630
+ }
1631
+ if (moveable && lastSelected) {
1632
+ positionPaddingHandles(lastSelected);
1633
+ }
1634
+ if (moveable) return;
1635
+ const target = document.elementFromPoint(lastMouseX, lastMouseY);
1636
+ if (target instanceof HTMLElement && !isOverlayEl(target)) {
1637
+ hoverTarget = target;
1638
+ updateHover(target);
1639
+ } else {
1640
+ clearHover();
1641
+ }
1642
+ };
1643
+ const onMouseLeave = () => clearHover();
1644
+ const pushSelectionToServer = (el) => {
1645
+ const oid = el.getAttribute("data-oid");
1646
+ if (!oid) return;
1647
+ const parts = oid.split(":");
1648
+ if (parts.length < 3) return;
1649
+ const file = parts.slice(0, parts.length - 2).join(":");
1650
+ const line = parseInt(parts[parts.length - 2], 10);
1651
+ const col = parseInt(parts[parts.length - 1], 10);
1652
+ if (!Number.isInteger(line) || !Number.isInteger(col)) return;
1653
+ const fiberName = getComponentName(el);
1654
+ const fileName = nameFromDataOid(oid);
1655
+ const componentName = fileName || (fiberName && !FRAMEWORK_INTERNALS.has(fiberName) ? fiberName : null);
1656
+ const instances = queryInstances(oid);
1657
+ void authedFetch(`${SERVER_URL}/selection`, {
1658
+ method: "POST",
1659
+ body: JSON.stringify({
1660
+ file,
1661
+ line,
1662
+ col,
1663
+ oid,
1664
+ className: el.className,
1665
+ tagName: el.tagName.toLowerCase(),
1666
+ componentName,
1667
+ instanceCount: instances.length
1668
+ })
1669
+ }).catch(() => {
1670
+ });
1671
+ };
1672
+ const clearSelectionOnServer = () => {
1673
+ void authedFetch(`${SERVER_URL}/selection`, {
1674
+ method: "DELETE"
1675
+ }).catch(() => {
1676
+ });
1677
+ };
1678
+ const acquire = (el) => {
1679
+ if (el === lastSelected) return;
1680
+ if (isOverlayEl(el)) return;
1681
+ if (pending) {
1682
+ clearInlineSizing(pending.element);
1683
+ hidePending();
1684
+ }
1685
+ lastSelected = el;
1686
+ lastResizeWidth = null;
1687
+ clearHover();
1688
+ pushSelectionToServer(el);
1689
+ if (moveable) moveable.destroy();
1690
+ const parent = el.parentElement;
1691
+ const elementGuidelines = parent ? Array.from(parent.children).filter(
1692
+ (c) => c instanceof HTMLElement && c !== el
1693
+ ) : [];
1694
+ moveable = new Moveable(moveableContainer, {
1695
+ target: el,
1696
+ draggable: true,
1697
+ resizable: true,
1698
+ keepRatio: false,
1699
+ origin: false,
1700
+ snappable: true,
1701
+ snapDirections: { top: true, left: true, bottom: true, right: true, center: true, middle: true },
1702
+ elementSnapDirections: { top: true, left: true, bottom: true, right: true, center: true, middle: true },
1703
+ snapThreshold: 4,
1704
+ elementGuidelines
1705
+ });
1706
+ moveable.on("drag", ({ target, transform }) => {
1707
+ const t = target;
1708
+ t.style.transform = transform;
1709
+ positionPaddingHandles(t);
1710
+ updateIndicators(t, t.getBoundingClientRect());
1711
+ });
1712
+ moveable.on("resize", ({ target, width, height }) => {
1713
+ const t = target;
1714
+ t.style.width = `${width}px`;
1715
+ t.style.height = `${height}px`;
1716
+ lastResizeWidth = width;
1717
+ positionPaddingHandles(t);
1718
+ updateIndicators(t, t.getBoundingClientRect());
1719
+ });
1720
+ positionPaddingHandles(el);
1721
+ updateIndicators(el, el.getBoundingClientRect());
1722
+ updateShortcutsHint(el);
1723
+ if (el.hasAttribute("data-css-module-class") || el.hasAttribute("data-styled-name")) {
1724
+ showCssPanel(el);
1725
+ } else {
1726
+ hideCssPanel();
1727
+ }
1728
+ moveable.on("resizeEnd", ({ target }) => {
1729
+ const el2 = target;
1730
+ if (lastResizeWidth === null) return;
1731
+ const newWidthPx = lastResizeWidth;
1732
+ lastResizeWidth = null;
1733
+ const tokens = el2.className.split(/\s+/).filter(Boolean);
1734
+ const widthToken = tokens.find((t) => WIDTH_TOKEN_RE.test(t));
1735
+ if (!widthToken) {
1736
+ const numericSuffix = /^(-?\d+(?:\.\d+)?|\[[^\]]+\])$/;
1737
+ const hasPad = tokens.some(
1738
+ (t) => ["p", "px", "py", "pt", "pr", "pb", "pl"].some(
1739
+ (p) => t.startsWith(p + "-") && numericSuffix.test(t.slice(p.length + 1))
1740
+ )
1741
+ );
1742
+ const suggestion = hasPad ? "Use the teal padding bars or press [ ] to adjust padding instead." : "No editable padding either \u2014 pick a different element.";
1743
+ showResult(`No w-* class on this element. ${suggestion}`, "error");
1744
+ return;
1745
+ }
1746
+ const spacingPx = getSpacingPx();
1747
+ const snap = snapToTailwind(newWidthPx, spacingPx);
1748
+ const newToken = `w-${snap.suffix}`;
1749
+ if (newToken === widthToken) {
1750
+ clearInlineSizing(el2);
1751
+ return;
1752
+ }
1753
+ proposeTokenChange(el2, widthToken, newToken, snap.resolvedPx);
1754
+ });
1755
+ };
1756
+ const proposeTokenChange = (el, before, after, resolvedPx) => {
1757
+ const oid = el.getAttribute("data-oid");
1758
+ if (!oid) {
1759
+ showResult(
1760
+ "Element has no data-oid. Is the Babel plugin loaded?",
1761
+ "error"
1762
+ );
1763
+ return;
1764
+ }
1765
+ const parts = oid.split(":");
1766
+ if (parts.length < 3) {
1767
+ showResult(`Malformed data-oid: ${oid}`, "error");
1768
+ return;
1769
+ }
1770
+ const file = parts.slice(0, parts.length - 2).join(":");
1771
+ const line = parseInt(parts[parts.length - 2], 10);
1772
+ const col = parseInt(parts[parts.length - 1], 10);
1773
+ if (!Number.isInteger(line) || !Number.isInteger(col)) {
1774
+ showResult(`Malformed data-oid: ${oid}`, "error");
1775
+ return;
1776
+ }
1777
+ showPending(
1778
+ { element: el, file, line, col, before, after },
1779
+ resolvedPx
1780
+ );
1781
+ };
1782
+ const handleNudgeKey = (e) => {
1783
+ if (!lastSelected) return false;
1784
+ let prefixes = null;
1785
+ let direction = null;
1786
+ if (e.altKey && e.key === "ArrowRight") {
1787
+ prefixes = WIDTH_PREFIXES;
1788
+ direction = "up";
1789
+ } else if (e.altKey && e.key === "ArrowLeft") {
1790
+ prefixes = WIDTH_PREFIXES;
1791
+ direction = "down";
1792
+ } else if (e.altKey && e.key === "ArrowDown") {
1793
+ prefixes = HEIGHT_PREFIXES;
1794
+ direction = "up";
1795
+ } else if (e.altKey && e.key === "ArrowUp") {
1796
+ prefixes = HEIGHT_PREFIXES;
1797
+ direction = "down";
1798
+ } else if (e.altKey && (e.key === "]" || e.key === "}")) {
1799
+ prefixes = GAP_PREFIXES;
1800
+ direction = "up";
1801
+ } else if (e.altKey && (e.key === "[" || e.key === "{")) {
1802
+ prefixes = GAP_PREFIXES;
1803
+ direction = "down";
1804
+ } else if (e.key === "}" || e.key === "]" && e.shiftKey) {
1805
+ prefixes = MARGIN_PREFIXES;
1806
+ direction = "up";
1807
+ } else if (e.key === "{" || e.key === "[" && e.shiftKey) {
1808
+ prefixes = MARGIN_PREFIXES;
1809
+ direction = "down";
1810
+ } else if (e.key === "]" && !e.shiftKey) {
1811
+ prefixes = PADDING_PREFIXES;
1812
+ direction = "up";
1813
+ } else if (e.key === "[" && !e.shiftKey) {
1814
+ prefixes = PADDING_PREFIXES;
1815
+ direction = "down";
1816
+ }
1817
+ if (!prefixes || !direction) return false;
1818
+ e.preventDefault();
1819
+ const el = lastSelected;
1820
+ const token = findTokenByPrefix(el.className, prefixes);
1821
+ if (!token) {
1822
+ const family = prefixes === PADDING_PREFIXES ? "padding" : prefixes === MARGIN_PREFIXES ? "margin" : prefixes === GAP_PREFIXES ? "gap" : prefixes === WIDTH_PREFIXES ? "width" : prefixes === HEIGHT_PREFIXES ? "height" : `${prefixes[0]}-*`;
1823
+ showResult(
1824
+ `No ${family} class on this element to nudge. See the hint badge for what IS editable.`,
1825
+ "error"
1826
+ );
1827
+ return true;
1828
+ }
1829
+ const spacingPx = getSpacingPx();
1830
+ const newToken = bumpStep(token, direction, spacingPx);
1831
+ if (!newToken) {
1832
+ showResult(
1833
+ `${token} is already at the ${direction === "up" ? "max" : "min"} step. Try the arbitrary-value drag (release outside the snap window).`,
1834
+ "error"
1835
+ );
1836
+ return true;
1837
+ }
1838
+ const resolvedPx = pxFromClass(newToken, spacingPx) ?? 0;
1839
+ proposeTokenChange(el, token, newToken, resolvedPx);
1840
+ return true;
1841
+ };
1842
+ const onClick = (e) => {
1843
+ const target = document.elementFromPoint(e.clientX, e.clientY);
1844
+ if (!(target instanceof HTMLElement)) return;
1845
+ if (isOverlayEl(target)) return;
1846
+ if (e.shiftKey) {
1847
+ const idx = measureSet.indexOf(target);
1848
+ if (idx === -1) measureSet.push(target);
1849
+ else measureSet.splice(idx, 1);
1850
+ return;
1851
+ }
1852
+ acquire(target);
1853
+ };
1854
+ const onKeyUp = (e) => {
1855
+ if (e.key === "Alt" || e.altKey === false) {
1856
+ clearDistanceOverlay();
1857
+ }
1858
+ };
1859
+ const onKeyDown = (e) => {
1860
+ if ((e.key === "i" || e.key === "I") && !e.altKey && !e.metaKey && !e.ctrlKey && lastSelected && lastSelected.tagName.toLowerCase() === "img") {
1861
+ e.preventDefault();
1862
+ void renderAssetPicker(lastSelected);
1863
+ return;
1864
+ }
1865
+ if (e.key === "Escape") {
1866
+ if (pending) {
1867
+ clearInlineSizing(pending.element);
1868
+ hidePending();
1869
+ }
1870
+ if (moveable) {
1871
+ moveable.destroy();
1872
+ moveable = null;
1873
+ lastSelected = null;
1874
+ hidePaddingHandles();
1875
+ clearIndicators();
1876
+ hideShortcutsHint();
1877
+ hideAssetPicker();
1878
+ hideCssPanel();
1879
+ clearSelectionOnServer();
1880
+ }
1881
+ return;
1882
+ }
1883
+ handleNudgeKey(e);
1884
+ };
1885
+ document.addEventListener("mousemove", onMouseMove, { passive: true });
1886
+ document.addEventListener("scroll", onScrollOrResize, { passive: true });
1887
+ window.addEventListener("resize", onScrollOrResize);
1888
+ document.addEventListener("mouseleave", onMouseLeave);
1889
+ document.addEventListener("click", onClick);
1890
+ document.addEventListener("keydown", onKeyDown);
1891
+ document.addEventListener("keyup", onKeyUp);
1892
+ document.body.dataset[SELF_TEST_KEY] = "true";
1893
+ {
1894
+ const draft = loadDraft();
1895
+ if (draft) {
1896
+ const escaped = draft.oid.replace(/"/g, '\\"');
1897
+ const restored = document.querySelector(
1898
+ `[data-oid="${escaped}"]`
1899
+ );
1900
+ if (restored) {
1901
+ showPending(
1902
+ {
1903
+ element: restored,
1904
+ file: draft.file,
1905
+ line: draft.line,
1906
+ col: draft.col,
1907
+ before: draft.before,
1908
+ after: draft.after
1909
+ },
1910
+ draft.resolvedPx
1911
+ );
1912
+ } else {
1913
+ clearDraft();
1914
+ }
1915
+ }
1916
+ }
1917
+ window.__visualEditorSpike = {
1918
+ moveableHandleCount: () => moveableContainer.querySelectorAll(".moveable-control-box").length,
1919
+ badgeText: () => shadow.querySelector(".badge")?.textContent ?? null,
1920
+ hostBodyColor: () => getComputedStyle(document.body).color,
1921
+ badgeColor: () => {
1922
+ const b = shadow.querySelector(".badge");
1923
+ return b ? getComputedStyle(b).backgroundColor : null;
1924
+ },
1925
+ badgeBoxSizing: () => {
1926
+ const b = shadow.querySelector(".badge");
1927
+ return b ? getComputedStyle(b).boxSizing : null;
1928
+ },
1929
+ hostBodyBoxSizing: () => getComputedStyle(document.body).boxSizing,
1930
+ hoverTagVisible: () => shadow.querySelector(".hover-tag")?.style.display === "block",
1931
+ hoverTagText: () => shadow.querySelector(".hover-tag")?.textContent ?? null,
1932
+ pendingPanelVisible: () => shadow.querySelector(".pending-panel")?.style.display === "flex",
1933
+ pendingPanelText: () => shadow.querySelector(".pending-panel")?.textContent ?? null,
1934
+ // Programmatic Apply trigger — clicks the .btn-apply button inside the
1935
+ // shadow so headless tests can drive the Apply flow without
1936
+ // navigating Moveable's pointer-events surface.
1937
+ clickApply: () => {
1938
+ const btn = shadow.querySelector(".btn-apply");
1939
+ if (!btn) return { error: "no-apply-button" };
1940
+ btn.click();
1941
+ return { clicked: true };
1942
+ },
1943
+ instanceOutlineCount: () => shadow.querySelectorAll(".instance-outline").length,
1944
+ visiblePaddingIndicators: () => Array.from(shadow.querySelectorAll(".indicator-pad")).filter(
1945
+ (e) => e.style.display !== "none"
1946
+ ).length,
1947
+ visibleMarginIndicators: () => Array.from(shadow.querySelectorAll(".indicator-margin")).filter(
1948
+ (e) => e.style.display !== "none"
1949
+ ).length,
1950
+ distanceLabelText: () => {
1951
+ const el = shadow.querySelector(".distance-label");
1952
+ if (!el || el.style.display === "none") return null;
1953
+ return el.textContent;
1954
+ },
1955
+ measureSetSize: () => measureSet.length,
1956
+ visiblePaddingHandles: () => Array.from(shadow.querySelectorAll(".padding-handle")).filter(
1957
+ (e) => e.style.display !== "none"
1958
+ ).length,
1959
+ paddingHandleRect: (side) => {
1960
+ const h2 = shadow.querySelector(
1961
+ `.padding-handle-${side}`
1962
+ );
1963
+ if (!h2 || h2.style.display === "none") return null;
1964
+ return {
1965
+ left: parseFloat(h2.style.left),
1966
+ top: parseFloat(h2.style.top)
1967
+ };
1968
+ },
1969
+ historyPanelVisible: () => shadow.querySelector(".history-panel")?.style.display === "flex",
1970
+ historyRowCount: () => shadow.querySelectorAll(".history-row").length,
1971
+ historyRows: () => Array.from(shadow.querySelectorAll(".history-row")).map((r) => ({
1972
+ source: r.querySelector(".history-source")?.textContent,
1973
+ tokens: r.querySelector(".history-tokens")?.textContent
1974
+ })),
1975
+ // Open the history panel programmatically (Playwright can't reach
1976
+ // into closed shadow to click the badge directly).
1977
+ openHistory: () => {
1978
+ const badge = shadow.querySelector(".badge");
1979
+ badge?.click();
1980
+ },
1981
+ undoRowAt: (index) => {
1982
+ const rows = shadow.querySelectorAll(".btn-undo-row");
1983
+ const btn = rows[index];
1984
+ btn?.click();
1985
+ },
1986
+ assetPickerVisible: () => shadow.querySelector(".asset-picker")?.style.display === "flex",
1987
+ assetPickerOptions: () => Array.from(shadow.querySelectorAll(".asset-row")).map(
1988
+ (r) => r.dataset.asset
1989
+ ),
1990
+ pickAsset: (assetPath) => {
1991
+ const row = shadow.querySelector(
1992
+ `.asset-row[data-asset="${CSS.escape(assetPath)}"]`
1993
+ );
1994
+ if (!row) return { error: "asset-not-found" };
1995
+ row.click();
1996
+ return { picked: assetPath };
1997
+ },
1998
+ cssPanelVisible: () => shadow.querySelector(".css-panel")?.style.display === "flex",
1999
+ cssPanelMeta: () => shadow.querySelector(".css-panel-meta")?.textContent ?? null,
2000
+ cssApply: (property, value) => {
2001
+ const propInput = shadow.querySelector(
2002
+ ".css-prop-input"
2003
+ );
2004
+ const valInput = shadow.querySelector(
2005
+ ".css-val-input"
2006
+ );
2007
+ const applyBtn = shadow.querySelector(
2008
+ ".btn-css-apply"
2009
+ );
2010
+ if (!propInput || !valInput || !applyBtn) {
2011
+ return { error: "no-css-panel" };
2012
+ }
2013
+ propInput.value = property;
2014
+ valInput.value = value;
2015
+ applyBtn.click();
2016
+ return { clicked: true };
2017
+ },
2018
+ cssPanelResult: () => shadow.querySelector(".css-panel-result")?.textContent ?? null,
2019
+ selectedTag: () => lastSelected?.tagName ?? null,
2020
+ selectedAttrs: () => lastSelected ? {
2021
+ dataCssClass: lastSelected.getAttribute("data-css-module-class"),
2022
+ dataCssFile: lastSelected.getAttribute("data-css-module-file"),
2023
+ dataOid: lastSelected.getAttribute("data-oid")
2024
+ } : null
2025
+ };
2026
+ return () => {
2027
+ document.removeEventListener("mousemove", onMouseMove);
2028
+ document.removeEventListener("scroll", onScrollOrResize);
2029
+ window.removeEventListener("resize", onScrollOrResize);
2030
+ document.removeEventListener("mouseleave", onMouseLeave);
2031
+ document.removeEventListener("click", onClick);
2032
+ document.removeEventListener("keydown", onKeyDown);
2033
+ document.removeEventListener("keyup", onKeyUp);
2034
+ cancelAnimationFrame(rafId);
2035
+ if (resultDismissTimer !== null) window.clearTimeout(resultDismissTimer);
2036
+ moveable?.destroy();
2037
+ hidePaddingHandles();
2038
+ hideShortcutsHint();
2039
+ clearInstanceOutlines();
2040
+ anchor.remove();
2041
+ delete document.body.dataset[SELF_TEST_KEY];
2042
+ };
2043
+ }, []);
2044
+ return null;
2045
+ }
2046
+ export {
2047
+ Overlay as VisualEditOverlay
2048
+ };
2049
+ //# sourceMappingURL=index.js.map