vite-plugin-visual-selector 0.1.0 → 0.1.1

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 CHANGED
@@ -1,14 +1,17 @@
1
1
  // src/plugin.ts
2
2
  import { parse } from "@babel/parser";
3
- import traverse from "@babel/traverse";
3
+ import _traverse from "@babel/traverse";
4
4
  import * as t from "@babel/types";
5
5
  import MagicString from "magic-string";
6
6
  import path from "path";
7
7
 
8
8
  // src/shared/constants.ts
9
9
  var DEFAULT_ATTRIBUTE_NAME = "data-source-location";
10
+ var CONTROL_MESSAGE_TYPE = "visual-selector:set-active";
11
+ var AGENT_ATTR = "data-vite-plugin-element";
10
12
 
11
13
  // src/plugin.ts
14
+ var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
12
15
  function toRelativeUnixPath(root, filePath) {
13
16
  return path.relative(root, filePath).split(path.sep).join("/");
14
17
  }
@@ -28,6 +31,7 @@ function visualSelectorPlugin(options = {}) {
28
31
  return {
29
32
  name: "vite-plugin-visual-selector",
30
33
  enforce: "pre",
34
+ // 在其他插件(如 @vitejs/plugin-react)之前运行,确保 JSX 还未被转译
31
35
  transform(code, id) {
32
36
  if (!include.some((pattern) => pattern.test(id))) {
33
37
  return null;
@@ -35,13 +39,19 @@ function visualSelectorPlugin(options = {}) {
35
39
  if (exclude.some((pattern) => pattern.test(id))) {
36
40
  return null;
37
41
  }
38
- const ast = parse(code, {
39
- sourceType: "module",
40
- plugins: ["jsx", "typescript"]
41
- });
42
+ let ast;
43
+ try {
44
+ ast = parse(code, {
45
+ sourceType: "module",
46
+ plugins: ["jsx", "typescript"]
47
+ });
48
+ } catch {
49
+ return null;
50
+ }
42
51
  const magicString = new MagicString(code);
43
52
  let mutated = false;
44
53
  traverse(ast, {
54
+ // 遍历 AST 中所有 JSX 开标签节点
45
55
  JSXOpeningElement(jsxPath) {
46
56
  const node = jsxPath.node;
47
57
  if (!isIntrinsicElement(node)) {
@@ -59,16 +69,900 @@ function visualSelectorPlugin(options = {}) {
59
69
  }
60
70
  });
61
71
  if (!mutated) {
62
- return code;
72
+ return null;
63
73
  }
64
74
  return {
65
75
  code: magicString.toString(),
66
76
  map: magicString.generateMap({ hires: true })
77
+ // 高精度 sourcemap,保证调试准确
67
78
  };
68
79
  }
69
80
  };
70
81
  }
82
+
83
+ // src/runtime/utils.ts
84
+ function debounce(fn, ms) {
85
+ let timer = null;
86
+ return ((...args) => {
87
+ if (timer) clearTimeout(timer);
88
+ timer = setTimeout(() => fn(...args), ms);
89
+ });
90
+ }
91
+ function getSourceId(element) {
92
+ return element.dataset?.sourceLocation ?? element.dataset?.visualSelectorId ?? null;
93
+ }
94
+ function findAllElementsById(id) {
95
+ let elements = Array.from(document.querySelectorAll(`[data-source-location="${id}"]`));
96
+ if (elements.length === 0) {
97
+ elements = Array.from(document.querySelectorAll(`[data-visual-selector-id="${id}"]`));
98
+ }
99
+ return elements;
100
+ }
101
+ function hasSourceLocation(el) {
102
+ return el.hasAttribute("data-source-location") || el.hasAttribute("data-visual-selector-id");
103
+ }
104
+ function getElementPosition(element) {
105
+ const rect = element.getBoundingClientRect();
106
+ return {
107
+ top: rect.top,
108
+ left: rect.left,
109
+ right: rect.right,
110
+ bottom: rect.bottom,
111
+ width: rect.width,
112
+ height: rect.height,
113
+ centerX: rect.left + rect.width / 2,
114
+ centerY: rect.top + rect.height / 2
115
+ };
116
+ }
117
+ var TEXT_TAGS = ["P", "H1", "H2", "H3", "H4", "H5", "H6", "SPAN", "A", "LABEL"];
118
+ var INLINE_EDIT_TAGS = [
119
+ "div",
120
+ "p",
121
+ "h1",
122
+ "h2",
123
+ "h3",
124
+ "h4",
125
+ "h5",
126
+ "h6",
127
+ "span",
128
+ "li",
129
+ "td",
130
+ "a",
131
+ "button",
132
+ "label"
133
+ ];
134
+
135
+ // src/runtime/inline-edit.ts
136
+ function canInlineEdit(element) {
137
+ return INLINE_EDIT_TAGS.includes(element.tagName.toLowerCase()) && !!element.textContent?.trim() && !element.querySelector("img, video, canvas, svg") && element.children.length === 0;
138
+ }
139
+ function startInlineEditing(state, element) {
140
+ if (!canInlineEdit(element)) return;
141
+ state.editingElement = element;
142
+ element.dataset.originalTextContent = element.textContent || "";
143
+ element.contentEditable = "true";
144
+ element.setAttribute(AGENT_ATTR, "");
145
+ element.style.cursor = "text";
146
+ const range = document.createRange();
147
+ range.selectNodeContents(element);
148
+ const sel = window.getSelection();
149
+ sel?.removeAllRanges();
150
+ sel?.addRange(range);
151
+ element.focus();
152
+ state.selectionOverlays.forEach((o) => o.style.display = "none");
153
+ const debouncedReport = debounce(() => {
154
+ try {
155
+ window.parent.postMessage(
156
+ {
157
+ type: "inline-edit",
158
+ elementInfo: {
159
+ tagName: element.tagName,
160
+ visualSelectorId: state.selectedId,
161
+ dataSourceLocation: element.dataset?.sourceLocation
162
+ },
163
+ originalContent: element.dataset.originalTextContent || "",
164
+ newContent: element.textContent || ""
165
+ },
166
+ state.targetOrigin
167
+ );
168
+ } catch {
169
+ }
170
+ }, 500);
171
+ element.addEventListener("input", debouncedReport);
172
+ element.__inlineEditHandler = debouncedReport;
173
+ try {
174
+ window.parent.postMessage(
175
+ {
176
+ type: "content-editing-started",
177
+ visualSelectorId: state.selectedId
178
+ },
179
+ state.targetOrigin
180
+ );
181
+ } catch {
182
+ }
183
+ }
184
+ function stopInlineEditing(state, updatePositions) {
185
+ if (!state.editingElement) return;
186
+ state.editingElement.contentEditable = "false";
187
+ state.editingElement.removeAttribute(AGENT_ATTR);
188
+ state.editingElement.style.cursor = "";
189
+ const handler = state.editingElement.__inlineEditHandler;
190
+ if (handler) {
191
+ state.editingElement.removeEventListener("input", handler);
192
+ delete state.editingElement.__inlineEditHandler;
193
+ }
194
+ state.selectionOverlays.forEach((o) => o.style.display = "");
195
+ updatePositions();
196
+ try {
197
+ window.parent.postMessage(
198
+ {
199
+ type: "content-editing-ended",
200
+ visualSelectorId: state.selectedId
201
+ },
202
+ state.targetOrigin
203
+ );
204
+ } catch {
205
+ }
206
+ state.editingElement = null;
207
+ }
208
+ function handleInlineEdit(state, data, updatePositions) {
209
+ if (data.inlineEditingMode) {
210
+ const el = document.querySelector(
211
+ `[data-source-location="${data.dataSourceLocation}"]`
212
+ );
213
+ if (el) {
214
+ startInlineEditing(state, el);
215
+ }
216
+ } else {
217
+ stopInlineEditing(state, updatePositions);
218
+ }
219
+ }
220
+
221
+ // src/runtime/layer-navigation.ts
222
+ function buildLayerTree(element) {
223
+ const tree = [];
224
+ const ancestors = [];
225
+ let parent = element.parentElement;
226
+ while (parent && parent !== document.body) {
227
+ if (hasSourceLocation(parent)) {
228
+ ancestors.push(parent);
229
+ }
230
+ parent = parent.parentElement;
231
+ }
232
+ ancestors.reverse();
233
+ ancestors.forEach((el) => {
234
+ tree.push({ element: el, tagName: el.tagName.toLowerCase(), depth: 0 });
235
+ });
236
+ tree.push({ element, tagName: element.tagName.toLowerCase(), depth: 0 });
237
+ for (let i = 0; i < element.children.length; i++) {
238
+ const child = element.children[i];
239
+ if (hasSourceLocation(child)) {
240
+ tree.push({ element: child, tagName: child.tagName.toLowerCase(), depth: 0 });
241
+ }
242
+ }
243
+ return tree;
244
+ }
245
+ function removeLayerDropdown(state) {
246
+ if (state.layerDropdown) {
247
+ const parentOverlay = state.layerDropdown.parentElement;
248
+ if (parentOverlay) {
249
+ const arrowEl = parentOverlay.querySelector("[data-tag-arrow]");
250
+ if (arrowEl) arrowEl.textContent = "\u2304";
251
+ }
252
+ state.layerDropdown.remove();
253
+ state.layerDropdown = null;
254
+ document.removeEventListener("keydown", handleLayerKeyboard);
255
+ }
256
+ }
257
+ function toggleLayerDropdown(state, element, anchor, onSelectElement) {
258
+ if (state.layerDropdown) {
259
+ removeLayerDropdown(state);
260
+ return;
261
+ }
262
+ const layers = buildLayerTree(element);
263
+ renderLayerDropdown(state, anchor, layers, element, onSelectElement);
264
+ }
265
+ function renderLayerDropdown(state, anchor, layers, currentElement, onSelectElement) {
266
+ const dropdown = document.createElement("div");
267
+ dropdown.setAttribute("data-layer-dropdown", "true");
268
+ dropdown.setAttribute(AGENT_ATTR, "");
269
+ Object.assign(dropdown.style, {
270
+ position: "absolute",
271
+ backgroundColor: "#ffffff",
272
+ border: "1px solid #e2e8f0",
273
+ borderRadius: "6px",
274
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
275
+ fontSize: "12px",
276
+ zIndex: "99999",
277
+ pointerEvents: "auto",
278
+ padding: "4px 0",
279
+ minWidth: "120px",
280
+ maxHeight: "320px",
281
+ overflowY: "auto"
282
+ });
283
+ let focusedIndex = -1;
284
+ const items = [];
285
+ layers.forEach((layer, idx) => {
286
+ const item = document.createElement("div");
287
+ item.setAttribute(AGENT_ATTR, "");
288
+ item.textContent = layer.tagName;
289
+ item.style.padding = "6px 16px";
290
+ item.style.cursor = "pointer";
291
+ item.style.whiteSpace = "nowrap";
292
+ if (layer.element === currentElement) {
293
+ item.style.color = "#000000";
294
+ item.style.backgroundColor = "rgba(255, 204, 0, 0.2)";
295
+ item.style.fontWeight = "600";
296
+ focusedIndex = idx;
297
+ } else {
298
+ item.style.color = "#64748b";
299
+ }
300
+ item.addEventListener("click", (e) => {
301
+ e.stopPropagation();
302
+ removeLayerDropdown(state);
303
+ onSelectElement(layer.element);
304
+ });
305
+ item.addEventListener("mouseenter", () => {
306
+ if (layer.element !== currentElement) {
307
+ item.style.backgroundColor = "#f1f5f9";
308
+ item.style.color = "#334155";
309
+ }
310
+ });
311
+ item.addEventListener("mouseleave", () => {
312
+ if (layer.element !== currentElement) {
313
+ item.style.backgroundColor = "transparent";
314
+ item.style.color = "#64748b";
315
+ }
316
+ });
317
+ items.push(item);
318
+ dropdown.appendChild(item);
319
+ });
320
+ document.body.appendChild(dropdown);
321
+ const anchorRect = anchor.getBoundingClientRect();
322
+ dropdown.style.top = `${anchorRect.bottom + window.scrollY + 2}px`;
323
+ dropdown.style.left = `${anchorRect.left + window.scrollX}px`;
324
+ requestAnimationFrame(() => {
325
+ const ddRect = dropdown.getBoundingClientRect();
326
+ if (ddRect.right > window.innerWidth - 4) {
327
+ dropdown.style.left = `${window.innerWidth - ddRect.width - 4 + window.scrollX}px`;
328
+ }
329
+ if (ddRect.left < 4) {
330
+ dropdown.style.left = `${4 + window.scrollX}px`;
331
+ }
332
+ if (ddRect.bottom > window.innerHeight) {
333
+ dropdown.style.top = `${anchorRect.top + window.scrollY - ddRect.height - 2}px`;
334
+ }
335
+ });
336
+ state.layerDropdown = dropdown;
337
+ const handleKeydown = (e) => {
338
+ if (!state.layerDropdown) return;
339
+ switch (e.key) {
340
+ case "ArrowDown":
341
+ e.preventDefault();
342
+ focusedIndex = Math.min(focusedIndex + 1, items.length - 1);
343
+ highlightItem(items, focusedIndex, layers, currentElement);
344
+ break;
345
+ case "ArrowUp":
346
+ e.preventDefault();
347
+ focusedIndex = Math.max(focusedIndex - 1, 0);
348
+ highlightItem(items, focusedIndex, layers, currentElement);
349
+ break;
350
+ case "Enter":
351
+ e.preventDefault();
352
+ if (focusedIndex >= 0 && focusedIndex < layers.length) {
353
+ removeLayerDropdown(state);
354
+ onSelectElement(layers[focusedIndex].element);
355
+ }
356
+ break;
357
+ case "Escape":
358
+ e.preventDefault();
359
+ removeLayerDropdown(state);
360
+ break;
361
+ }
362
+ };
363
+ handleLayerKeyboard.current = handleKeydown;
364
+ document.addEventListener("keydown", handleLayerKeyboard);
365
+ }
366
+ function handleLayerKeyboard(e) {
367
+ const current = handleLayerKeyboard.current;
368
+ if (current) current(e);
369
+ }
370
+ function highlightItem(items, index, layers, currentElement) {
371
+ items.forEach((item, i) => {
372
+ if (layers[i].element === currentElement) {
373
+ item.style.color = "#000000";
374
+ item.style.backgroundColor = i === index ? "rgba(255, 204, 0, 0.35)" : "rgba(255, 204, 0, 0.2)";
375
+ item.style.fontWeight = "600";
376
+ } else {
377
+ item.style.backgroundColor = i === index ? "#f1f5f9" : "transparent";
378
+ item.style.color = i === index ? "#334155" : "#64748b";
379
+ item.style.fontWeight = "normal";
380
+ }
381
+ });
382
+ }
383
+
384
+ // src/runtime/messages.ts
385
+ function reportElementSelected(state, element) {
386
+ const el = element;
387
+ const tagName = element.tagName;
388
+ const isTextElement = TEXT_TAGS.includes(tagName);
389
+ const computed = window.getComputedStyle(el);
390
+ const computedStyles = {
391
+ fontFamily: computed.fontFamily,
392
+ fontSize: computed.fontSize,
393
+ fontWeight: computed.fontWeight,
394
+ textAlign: computed.textAlign,
395
+ textDecoration: computed.textDecoration,
396
+ textDecorationLine: computed.textDecorationLine,
397
+ color: computed.color,
398
+ backgroundColor: computed.backgroundColor,
399
+ lineHeight: computed.lineHeight,
400
+ letterSpacing: computed.letterSpacing,
401
+ opacity: computed.opacity
402
+ };
403
+ try {
404
+ window.parent.postMessage(
405
+ {
406
+ type: "element-selected",
407
+ tagName,
408
+ classes: typeof el.className === "string" ? el.className : "",
409
+ visualSelectorId: getSourceId(element),
410
+ content: isTextElement ? el.innerText : void 0,
411
+ dataSourceLocation: el.dataset?.sourceLocation,
412
+ isDynamicContent: el.dataset?.dynamicContent === "true",
413
+ position: getElementPosition(element),
414
+ attributes: {},
415
+ isTextElement,
416
+ staticArrayName: null,
417
+ collectionId: null,
418
+ computedStyles
419
+ },
420
+ state.targetOrigin
421
+ );
422
+ } catch {
423
+ }
424
+ }
425
+ function reportPositionUpdate(state) {
426
+ if (!state.selectedId || !state.selectedElement?.isConnected) return;
427
+ const rect = state.selectedElement.getBoundingClientRect();
428
+ const isInViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
429
+ try {
430
+ window.parent.postMessage(
431
+ {
432
+ type: "element-position-update",
433
+ position: getElementPosition(state.selectedElement),
434
+ isInViewport,
435
+ visualSelectorId: state.selectedId
436
+ },
437
+ state.targetOrigin
438
+ );
439
+ } catch {
440
+ }
441
+ }
442
+ function handleMessage(state, e, callbacks) {
443
+ if (state.targetOrigin !== "*" && e.origin !== state.targetOrigin) return;
444
+ const msg = e.data;
445
+ if (!msg || typeof msg !== "object" || !msg.type) return;
446
+ switch (msg.type) {
447
+ case "toggle-visual-edit-mode":
448
+ callbacks.enableEditMode(msg.data?.enabled ?? false, msg.data?.specs);
449
+ break;
450
+ case "update-classes":
451
+ if (msg.data?.visualSelectorId && typeof msg.data?.classes === "string") {
452
+ findAllElementsById(msg.data.visualSelectorId).forEach(
453
+ (el) => el.setAttribute("class", msg.data.classes)
454
+ );
455
+ setTimeout(callbacks.updateAllOverlayPositions, 50);
456
+ }
457
+ break;
458
+ case "update-attribute":
459
+ if (msg.data?.visualSelectorId && msg.data?.attribute && msg.data?.value !== void 0) {
460
+ findAllElementsById(msg.data.visualSelectorId).forEach(
461
+ (el) => el.setAttribute(msg.data.attribute, msg.data.value)
462
+ );
463
+ }
464
+ break;
465
+ case "update-content":
466
+ if (msg.data?.visualSelectorId && msg.data?.content !== void 0) {
467
+ findAllElementsById(msg.data.visualSelectorId).forEach((el) => {
468
+ el.innerText = msg.data.content;
469
+ });
470
+ setTimeout(callbacks.updateAllOverlayPositions, 50);
471
+ }
472
+ break;
473
+ case "unselect-element":
474
+ if (state.editingElement) callbacks.stopInlineEditing();
475
+ state.selectionOverlays.forEach((o) => o.remove());
476
+ state.selectionOverlays = [];
477
+ state.selectedId = null;
478
+ state.selectedElement = null;
479
+ break;
480
+ case "update-theme-variables":
481
+ if (msg.data?.variables) {
482
+ const root = msg.data.mode === "dark" ? document.querySelector(".dark") : document.documentElement;
483
+ if (root) {
484
+ for (const [name, value] of Object.entries(msg.data.variables)) {
485
+ root.style.setProperty(name, value);
486
+ }
487
+ }
488
+ }
489
+ break;
490
+ case "toggle-inline-edit-mode":
491
+ if (msg.data) {
492
+ callbacks.handleInlineEdit(msg.data);
493
+ }
494
+ break;
495
+ case "inject-font-import":
496
+ if (msg.data?.fontUrl && /^https?:\/\//.test(msg.data.fontUrl)) {
497
+ const link = document.createElement("link");
498
+ link.rel = "stylesheet";
499
+ link.href = msg.data.fontUrl;
500
+ link.setAttribute(AGENT_ATTR, "");
501
+ document.head.appendChild(link);
502
+ }
503
+ break;
504
+ case "popover-drag-state":
505
+ case "dropdown-state":
506
+ break;
507
+ case "refresh-page":
508
+ window.location.reload();
509
+ break;
510
+ case "request-element-position":
511
+ reportPositionUpdate(state);
512
+ break;
513
+ case CONTROL_MESSAGE_TYPE: {
514
+ const controlMsg = msg;
515
+ if (typeof controlMsg.active === "boolean") {
516
+ callbacks.enableEditMode(controlMsg.active);
517
+ }
518
+ break;
519
+ }
520
+ case "update-element-style":
521
+ case "update-element-text":
522
+ case "update-element-class":
523
+ handleLegacyUpdateMessage(state, msg);
524
+ break;
525
+ }
526
+ }
527
+ function handleLegacyUpdateMessage(state, msg) {
528
+ if (!msg.dataSourceLocation) return;
529
+ const el = document.querySelector(
530
+ `[${state.attributeName}="${msg.dataSourceLocation}"]`
531
+ );
532
+ if (!el) return;
533
+ if (msg.type === "update-element-style" && msg.styles) {
534
+ for (const [prop, value] of Object.entries(msg.styles)) {
535
+ el.style.setProperty(
536
+ prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
537
+ value
538
+ );
539
+ }
540
+ } else if (msg.type === "update-element-text" && typeof msg.text === "string") {
541
+ el.textContent = msg.text;
542
+ } else if (msg.type === "update-element-class" && typeof msg.className === "string") {
543
+ el.className = msg.className;
544
+ }
545
+ }
546
+
547
+ // src/runtime/overlay.ts
548
+ function createOverlay(opts) {
549
+ const overlay = document.createElement("div");
550
+ overlay.setAttribute(AGENT_ATTR, "");
551
+ overlay.style.position = "absolute";
552
+ overlay.style.pointerEvents = "none";
553
+ overlay.style.zIndex = "9999";
554
+ overlay.style.boxSizing = "border-box";
555
+ if (opts.selected) {
556
+ overlay.style.boxShadow = "inset 0 0 0 2px #FC0";
557
+ } else {
558
+ overlay.style.boxShadow = "inset 0 0 0 2px rgba(255, 204, 0, 0.5)";
559
+ overlay.style.backgroundColor = "rgba(255, 204, 0, 0.05)";
560
+ }
561
+ return overlay;
562
+ }
563
+ function positionOverlay(overlay, target, tagMode = "none", callbacks) {
564
+ const rect = target.getBoundingClientRect();
565
+ overlay.style.top = `${rect.top + window.scrollY}px`;
566
+ overlay.style.left = `${rect.left + window.scrollX}px`;
567
+ overlay.style.width = `${rect.width}px`;
568
+ overlay.style.height = `${rect.height}px`;
569
+ if (tagMode !== "none") {
570
+ let tag = overlay.querySelector("[data-tag-label]");
571
+ if (!tag) {
572
+ tag = document.createElement("div");
573
+ tag.setAttribute(AGENT_ATTR, "");
574
+ tag.setAttribute("data-tag-label", "");
575
+ Object.assign(tag.style, {
576
+ position: "absolute",
577
+ padding: "2px 8px",
578
+ fontSize: "11px",
579
+ fontWeight: "500",
580
+ borderRadius: "3px",
581
+ minWidth: "24px",
582
+ textAlign: "center",
583
+ display: "flex",
584
+ alignItems: "center",
585
+ gap: "3px",
586
+ lineHeight: "1.4"
587
+ });
588
+ if (tagMode === "selected") {
589
+ tag.style.backgroundColor = "#FC0";
590
+ tag.style.color = "#000000";
591
+ tag.style.cursor = "pointer";
592
+ tag.style.pointerEvents = "auto";
593
+ const textSpan = document.createElement("span");
594
+ textSpan.textContent = target.tagName.toLowerCase();
595
+ tag.appendChild(textSpan);
596
+ const arrow = document.createElement("span");
597
+ arrow.setAttribute("data-tag-arrow", "");
598
+ arrow.textContent = "\u2304";
599
+ arrow.style.fontSize = "10px";
600
+ arrow.style.lineHeight = "1";
601
+ tag.appendChild(arrow);
602
+ if (callbacks?.onTagClick) {
603
+ const onTagClick = callbacks.onTagClick;
604
+ tag.addEventListener("click", (e) => {
605
+ e.stopPropagation();
606
+ onTagClick(target, tag);
607
+ });
608
+ }
609
+ } else {
610
+ tag.style.backgroundColor = "rgba(255, 204, 0, 0.7)";
611
+ tag.style.color = "#000000";
612
+ tag.style.pointerEvents = "none";
613
+ tag.textContent = target.tagName.toLowerCase();
614
+ }
615
+ overlay.appendChild(tag);
616
+ }
617
+ positionTag(tag, rect);
618
+ }
619
+ }
620
+ function positionTag(tag, rect) {
621
+ const isNearTop = rect.top < 27;
622
+ const isTallEnough = rect.height >= 54;
623
+ const isFullWidth = rect.width >= window.innerWidth - 4;
624
+ const leftOffset = isFullWidth ? 8 : 4;
625
+ if (isNearTop && isTallEnough) {
626
+ tag.style.top = "2px";
627
+ } else if (isNearTop) {
628
+ tag.style.top = `${rect.height + 2}px`;
629
+ } else {
630
+ tag.style.top = "-27px";
631
+ }
632
+ tag.style.left = `${leftOffset}px`;
633
+ requestAnimationFrame(() => {
634
+ const tagRect = tag.getBoundingClientRect();
635
+ if (tagRect.right > window.innerWidth - 4) {
636
+ const shift = tagRect.right - window.innerWidth + 8;
637
+ tag.style.left = `${leftOffset - shift}px`;
638
+ }
639
+ if (tagRect.left < 4) {
640
+ const shift = 4 - tagRect.left;
641
+ tag.style.left = `${leftOffset + shift}px`;
642
+ }
643
+ });
644
+ }
645
+ function clearHoverOverlays(state) {
646
+ state.hoverOverlays.forEach((o) => o.remove());
647
+ state.hoverOverlays = [];
648
+ state.currentHoverId = null;
649
+ }
650
+ function clearSelectionOverlays(state, removeDropdown) {
651
+ state.selectionOverlays.forEach((o) => o.remove());
652
+ state.selectionOverlays = [];
653
+ removeDropdown();
654
+ }
655
+ function clearAllOverlays(state, removeDropdown) {
656
+ clearHoverOverlays(state);
657
+ clearSelectionOverlays(state, removeDropdown);
658
+ state.selectedId = null;
659
+ state.selectedElement = null;
660
+ }
661
+ function freezeAnimations() {
662
+ document.documentElement.setAttribute("data-visual-edit-active", "");
663
+ if (!document.getElementById("freeze-animations")) {
664
+ const style = document.createElement("style");
665
+ style.id = "freeze-animations";
666
+ style.setAttribute(AGENT_ATTR, "");
667
+ style.textContent = `
668
+ [data-visual-edit-active] *:not([${AGENT_ATTR}]):not([${AGENT_ATTR}] *),
669
+ [data-visual-edit-active] *:not([${AGENT_ATTR}]):not([${AGENT_ATTR}] *)::before,
670
+ [data-visual-edit-active] *:not([${AGENT_ATTR}]):not([${AGENT_ATTR}] *)::after {
671
+ animation-play-state: paused !important;
672
+ transition: none !important;
673
+ }
674
+ `;
675
+ document.head.appendChild(style);
676
+ }
677
+ if (!document.getElementById("freeze-overflow")) {
678
+ const overflowStyle = document.createElement("style");
679
+ overflowStyle.id = "freeze-overflow";
680
+ overflowStyle.setAttribute(AGENT_ATTR, "");
681
+ overflowStyle.textContent = `
682
+ html[data-visual-edit-active] {
683
+ overflow-y: scroll !important;
684
+ overflow-x: hidden !important;
685
+ }
686
+ `;
687
+ document.head.appendChild(overflowStyle);
688
+ }
689
+ if (!document.getElementById("freeze-pointer-events")) {
690
+ const pointerStyle = document.createElement("style");
691
+ pointerStyle.id = "freeze-pointer-events";
692
+ pointerStyle.setAttribute(AGENT_ATTR, "");
693
+ pointerStyle.textContent = `
694
+ [data-visual-edit-active] * { pointer-events: none !important; }
695
+ [${AGENT_ATTR}], [${AGENT_ATTR}] * { pointer-events: auto !important; }
696
+ `;
697
+ document.head.appendChild(pointerStyle);
698
+ }
699
+ try {
700
+ document.getAnimations().forEach((anim) => {
701
+ const effect = anim.effect;
702
+ const target = effect?.target;
703
+ if (target instanceof Element && target.closest(`[${AGENT_ATTR}]`)) return;
704
+ try {
705
+ anim.finish();
706
+ } catch {
707
+ anim.pause();
708
+ }
709
+ });
710
+ } catch {
711
+ }
712
+ }
713
+ function unfreezeAnimations() {
714
+ document.documentElement.removeAttribute("data-visual-edit-active");
715
+ const freezeStyle = document.getElementById("freeze-animations");
716
+ if (freezeStyle) freezeStyle.remove();
717
+ const pointerStyle = document.getElementById("freeze-pointer-events");
718
+ if (pointerStyle) pointerStyle.remove();
719
+ const overflowStyle = document.getElementById("freeze-overflow");
720
+ if (overflowStyle) overflowStyle.remove();
721
+ }
722
+
723
+ // src/runtime/state.ts
724
+ function createAgentState(options = {}) {
725
+ return {
726
+ editModeEnabled: false,
727
+ selectedId: null,
728
+ selectedElement: null,
729
+ hoverOverlays: [],
730
+ selectionOverlays: [],
731
+ currentHoverId: null,
732
+ editingElement: null,
733
+ layerDropdown: null,
734
+ mutationObserver: null,
735
+ attributeName: options.attributeName ?? DEFAULT_ATTRIBUTE_NAME,
736
+ targetOrigin: options.targetOrigin ?? "*"
737
+ };
738
+ }
739
+
740
+ // src/runtime/index.ts
741
+ function setupVisualEditAgent(options = {}) {
742
+ const state = createAgentState(options);
743
+ function updateAllOverlayPositions() {
744
+ if (state.selectedId && state.selectedElement?.isConnected) {
745
+ const siblings = findAllElementsById(state.selectedId);
746
+ state.selectionOverlays.forEach((overlay, i) => {
747
+ if (siblings[i]) {
748
+ positionOverlay(overlay, siblings[i], i === 0 ? "selected" : "none", {
749
+ onTagClick
750
+ });
751
+ }
752
+ });
753
+ }
754
+ reportPositionUpdate(state);
755
+ }
756
+ function onTagClick(target, tag) {
757
+ const arrowEl = tag.querySelector("[data-tag-arrow]");
758
+ if (arrowEl) {
759
+ arrowEl.textContent = state.layerDropdown ? "\u2304" : "\u2303";
760
+ }
761
+ toggleLayerDropdown(state, target, tag, selectElement);
762
+ }
763
+ function findElementAtPoint(x, y) {
764
+ const freezeStyle = document.getElementById("freeze-pointer-events");
765
+ if (freezeStyle) freezeStyle.disabled = true;
766
+ const element = document.elementFromPoint(x, y);
767
+ if (freezeStyle) freezeStyle.disabled = false;
768
+ if (!element) return null;
769
+ if (element.closest(`[${AGENT_ATTR}]`)) return null;
770
+ return element.closest(`[${state.attributeName}], [data-visual-selector-id]`) ?? null;
771
+ }
772
+ function findHoverTarget(x, y, excludeId) {
773
+ const element = findElementAtPoint(x, y);
774
+ if (!element) return null;
775
+ const id = getSourceId(element);
776
+ if (!id || id === excludeId) return null;
777
+ return id;
778
+ }
779
+ function onMouseMove(e) {
780
+ if (!state.editModeEnabled) return;
781
+ requestAnimationFrame(() => {
782
+ const id = findHoverTarget(e.clientX, e.clientY, state.selectedId);
783
+ if (!id) {
784
+ clearHoverOverlays(state);
785
+ return;
786
+ }
787
+ if (id === state.currentHoverId) return;
788
+ clearHoverOverlays(state);
789
+ const elements = findAllElementsById(id);
790
+ elements.forEach((el, i) => {
791
+ const overlay = createOverlay({ selected: false });
792
+ overlay.setAttribute(AGENT_ATTR, "");
793
+ document.body.appendChild(overlay);
794
+ positionOverlay(overlay, el, i === 0 ? "hover" : "none");
795
+ state.hoverOverlays.push(overlay);
796
+ });
797
+ state.currentHoverId = id;
798
+ });
799
+ }
800
+ function onMouseLeave() {
801
+ clearHoverOverlays(state);
802
+ }
803
+ function onClick(e) {
804
+ if (!state.editModeEnabled) return;
805
+ const target = e.target;
806
+ if (target?.closest?.(`[data-layer-dropdown]`)) return;
807
+ if (target?.closest?.(`[data-tag-label]`) && state.selectionOverlays.some((o) => o.contains(target)))
808
+ return;
809
+ e.preventDefault();
810
+ e.stopPropagation();
811
+ e.stopImmediatePropagation();
812
+ clearHoverOverlays(state);
813
+ const element = findElementAtPoint(e.clientX, e.clientY);
814
+ if (!element) return;
815
+ selectElement(element);
816
+ }
817
+ function selectElement(element) {
818
+ const id = getSourceId(element);
819
+ if (!id) return;
820
+ if (state.editingElement) {
821
+ stopInlineEditing(state, updateAllOverlayPositions);
822
+ }
823
+ clearSelectionOverlays(state, () => removeLayerDropdown(state));
824
+ const siblings = findAllElementsById(id);
825
+ siblings.forEach((el, i) => {
826
+ const overlay = createOverlay({ selected: true });
827
+ overlay.setAttribute(AGENT_ATTR, "");
828
+ document.body.appendChild(overlay);
829
+ positionOverlay(overlay, el, i === 0 ? "selected" : "none", {
830
+ onTagClick
831
+ });
832
+ state.selectionOverlays.push(overlay);
833
+ });
834
+ state.selectedId = id;
835
+ state.selectedElement = element;
836
+ clearHoverOverlays(state);
837
+ reportElementSelected(state, element);
838
+ }
839
+ let rafPending = false;
840
+ const throttledPositionUpdate = () => {
841
+ if (rafPending) return;
842
+ rafPending = true;
843
+ requestAnimationFrame(() => {
844
+ rafPending = false;
845
+ reportPositionUpdate(state);
846
+ });
847
+ };
848
+ function enableEditMode(enabled, _specs) {
849
+ state.editModeEnabled = enabled;
850
+ if (enabled) {
851
+ document.body.style.cursor = "crosshair";
852
+ freezeAnimations();
853
+ document.addEventListener("mousemove", onMouseMove);
854
+ document.addEventListener("mouseleave", onMouseLeave);
855
+ document.addEventListener("click", onClick, true);
856
+ window.addEventListener("scroll", throttledPositionUpdate, true);
857
+ document.addEventListener("scroll", throttledPositionUpdate, true);
858
+ window.addEventListener("resize", updateAllOverlayPositions);
859
+ startMutationObserver();
860
+ } else {
861
+ document.body.style.cursor = "default";
862
+ unfreezeAnimations();
863
+ clearAllOverlays(state, () => removeLayerDropdown(state));
864
+ if (state.editingElement) {
865
+ stopInlineEditing(state, updateAllOverlayPositions);
866
+ }
867
+ document.removeEventListener("mousemove", onMouseMove);
868
+ document.removeEventListener("mouseleave", onMouseLeave);
869
+ document.removeEventListener("click", onClick, true);
870
+ window.removeEventListener("scroll", throttledPositionUpdate, true);
871
+ document.removeEventListener("scroll", throttledPositionUpdate, true);
872
+ window.removeEventListener("resize", updateAllOverlayPositions);
873
+ stopMutationObserver();
874
+ }
875
+ }
876
+ function startMutationObserver() {
877
+ if (state.mutationObserver) return;
878
+ state.mutationObserver = new MutationObserver((mutations) => {
879
+ const hasRelevantChange = mutations.some((m) => {
880
+ if (m.type === "attributes" && ["style", "class", "width", "height"].includes(m.attributeName ?? "") && containsTrackedElement(m.target))
881
+ return true;
882
+ if (m.type === "childList" && containsTrackedElement(m.target)) return true;
883
+ return false;
884
+ });
885
+ if (hasRelevantChange) {
886
+ setTimeout(updateAllOverlayPositions, 50);
887
+ }
888
+ });
889
+ state.mutationObserver.observe(document.body, {
890
+ attributes: true,
891
+ childList: true,
892
+ subtree: true,
893
+ attributeFilter: ["style", "class", "width", "height"]
894
+ });
895
+ }
896
+ function stopMutationObserver() {
897
+ if (state.mutationObserver) {
898
+ state.mutationObserver.disconnect();
899
+ state.mutationObserver = null;
900
+ }
901
+ }
902
+ function containsTrackedElement(node) {
903
+ if (!(node instanceof Element)) return false;
904
+ if (node.hasAttribute(AGENT_ATTR)) return false;
905
+ return hasSourceLocation(node) || !!node.querySelector(`[${state.attributeName}]`);
906
+ }
907
+ function setupSandboxMountObserver() {
908
+ if (window.self === window.top) return;
909
+ const mountObserver = new MutationObserver((mutations) => {
910
+ const hasChanges = mutations.some(
911
+ (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
912
+ );
913
+ if (!hasChanges) return;
914
+ const hasTrackedElements = document.body.querySelectorAll("[data-source-location], [data-dynamic-content]").length > 0;
915
+ try {
916
+ window.parent.postMessage(
917
+ {
918
+ type: hasTrackedElements ? "sandbox:onMounted" : "sandbox:onUnmounted"
919
+ },
920
+ state.targetOrigin
921
+ );
922
+ } catch {
923
+ }
924
+ });
925
+ if (document.body) {
926
+ mountObserver.observe(document.body, { childList: true, subtree: true });
927
+ } else {
928
+ document.addEventListener("DOMContentLoaded", () => {
929
+ mountObserver.observe(document.body, { childList: true, subtree: true });
930
+ });
931
+ }
932
+ }
933
+ const onMessage = (e) => {
934
+ handleMessage(state, e, {
935
+ enableEditMode,
936
+ updateAllOverlayPositions,
937
+ stopInlineEditing: () => stopInlineEditing(state, updateAllOverlayPositions),
938
+ handleInlineEdit: (data) => handleInlineEdit(state, data, updateAllOverlayPositions)
939
+ });
940
+ };
941
+ window.addEventListener("message", onMessage);
942
+ setupSandboxMountObserver();
943
+ try {
944
+ window.parent.postMessage({ type: "visual-edit-agent-ready" }, state.targetOrigin);
945
+ } catch {
946
+ }
947
+ return {
948
+ destroy() {
949
+ window.removeEventListener("message", onMessage);
950
+ if (state.editModeEnabled) {
951
+ enableEditMode(false);
952
+ }
953
+ },
954
+ enableEditMode,
955
+ selectElement,
956
+ clearSelection() {
957
+ clearAllOverlays(state, () => removeLayerDropdown(state));
958
+ },
959
+ getSelectedId() {
960
+ return state.selectedId;
961
+ }
962
+ };
963
+ }
71
964
  export {
965
+ setupVisualEditAgent,
72
966
  visualSelectorPlugin
73
967
  };
74
968
  //# sourceMappingURL=index.js.map