@base44/vite-plugin 0.2.29 → 0.2.30

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.
@@ -18,6 +18,8 @@ export function getElementSelectorId(element: Element): string | null {
18
18
 
19
19
  export const ALLOWED_ATTRIBUTES: string[] = ["src"];
20
20
 
21
+ export const PLUGIN_ELEMENT_ATTR = "data-vite-plugin-element";
22
+
21
23
  /** Find elements by ID - first try data-source-location, fallback to data-visual-selector-id */
22
24
  export function findElementsById(id: string | null): Element[] {
23
25
  if (!id) return [];
@@ -64,3 +66,106 @@ export function collectAllowedAttributes(element: Element, allowedAttributes: st
64
66
  }
65
67
  return attributes;
66
68
  }
69
+
70
+ /**
71
+ * Freeze all CSS animations and transitions on the page by injecting
72
+ * scoped styles under `[data-visual-edit-active]` and programmatically
73
+ * finishing (or pausing) every running animation.
74
+ *
75
+ * Plugin-owned elements (`[data-vite-plugin-element]`) are excluded so
76
+ * the plugin UI stays animated.
77
+ */
78
+ export function stopAnimations(): void {
79
+ if (document.getElementById('freeze-animations')) return;
80
+
81
+ document.documentElement.setAttribute('data-visual-edit-active', '');
82
+
83
+ const animStyle = document.createElement('style');
84
+ animStyle.id = 'freeze-animations';
85
+ animStyle.textContent = `
86
+ [data-visual-edit-active] *:not([${PLUGIN_ELEMENT_ATTR}]):not([${PLUGIN_ELEMENT_ATTR}] *),
87
+ [data-visual-edit-active] *:not([${PLUGIN_ELEMENT_ATTR}]):not([${PLUGIN_ELEMENT_ATTR}] *)::before,
88
+ [data-visual-edit-active] *:not([${PLUGIN_ELEMENT_ATTR}]):not([${PLUGIN_ELEMENT_ATTR}] *)::after {
89
+ animation-play-state: paused !important;
90
+ transition: none !important;
91
+ }
92
+ `;
93
+
94
+ const pointerStyle = document.createElement('style');
95
+ pointerStyle.id = 'freeze-pointer-events';
96
+ pointerStyle.textContent = `
97
+ [data-visual-edit-active] * { pointer-events: none !important; }
98
+ [${PLUGIN_ELEMENT_ATTR}], [${PLUGIN_ELEMENT_ATTR}] * { pointer-events: auto !important; }
99
+ `;
100
+
101
+ const target = document.head || document.documentElement;
102
+ target.appendChild(animStyle);
103
+ target.appendChild(pointerStyle);
104
+
105
+ document.getAnimations().forEach((a) => {
106
+ // Skip animations on plugin UI elements
107
+ const animTarget = (a.effect as KeyframeEffect)?.target;
108
+ if (animTarget instanceof Element && animTarget.closest(`[${PLUGIN_ELEMENT_ATTR}]`)) return;
109
+
110
+ try {
111
+ a.finish(); // fast-forward to end state
112
+ } catch {
113
+ a.pause(); // finish() throws on infinite animations — pause instead
114
+ }
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Resume all previously frozen animations and remove the injected
120
+ * freeze styles. Cleans up the `data-visual-edit-active` attribute
121
+ * from `<html>` so scoped selectors no longer match.
122
+ */
123
+ export function resumeAnimations(): void {
124
+ const animStyle = document.getElementById('freeze-animations');
125
+ if (!animStyle) return;
126
+
127
+ animStyle.remove();
128
+ document.getElementById('freeze-pointer-events')?.remove();
129
+ document.documentElement.removeAttribute('data-visual-edit-active');
130
+
131
+ document.getAnimations().forEach((a) => {
132
+ if (a.playState === 'paused') {
133
+ try { a.play(); } catch { /* animation target may have been removed */ }
134
+ }
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Hit-test the page at (`x`, `y`) and walk up the DOM to find the
140
+ * nearest ancestor that carries instrumentation attributes
141
+ * (`data-source-location` or `data-visual-selector-id`).
142
+ *
143
+ * Temporarily disables the pointer-events freeze sheet so the
144
+ * browser's native `elementFromPoint` can reach the real target.
145
+ */
146
+ export function findInstrumentedElement(x: number, y: number): Element | null {
147
+ const pointerStyle = document.getElementById('freeze-pointer-events') as HTMLStyleElement | null;
148
+ if (pointerStyle) pointerStyle.disabled = true;
149
+
150
+ const el = document.elementFromPoint(x, y);
151
+
152
+ if (pointerStyle) pointerStyle.disabled = false;
153
+
154
+ return el?.closest('[data-source-location], [data-visual-selector-id]') ?? null;
155
+ }
156
+
157
+ /**
158
+ * Resolve which element should be hovered at (`x`, `y`), skipping the
159
+ * currently selected element. Returns the selector ID of the hovered
160
+ * element, or `null` if the point is empty or hits the selected element.
161
+ */
162
+ export function resolveHoverTarget(x: number, y: number, selectedElementId: string | null): string | null {
163
+ const element = findInstrumentedElement(x, y);
164
+ if (!element) return null;
165
+
166
+ const selectorId = getElementSelectorId(element);
167
+
168
+ if (selectorId === selectedElementId) return null;
169
+
170
+ return selectorId;
171
+ }
@@ -1,4 +1,4 @@
1
- import { findElementsById, updateElementClasses, updateElementAttribute, collectAllowedAttributes, ALLOWED_ATTRIBUTES, getElementSelectorId } from "./utils.js";
1
+ import { findElementsById, updateElementClasses, updateElementAttribute, collectAllowedAttributes, ALLOWED_ATTRIBUTES, getElementSelectorId, stopAnimations, resumeAnimations, findInstrumentedElement, resolveHoverTarget } from "./utils.js";
2
2
  import { createLayerController } from "./layer-dropdown/controller.js";
3
3
  import { LAYER_DROPDOWN_ATTR } from "./layer-dropdown/consts.js";
4
4
  import { createInlineEditController } from "../capabilities/inline-edit/index.js";
@@ -191,53 +191,19 @@ export function setupVisualEditAgent() {
191
191
  window.parent.postMessage({ type: "unselect-element" }, "*");
192
192
  };
193
193
 
194
- // Handle mouse over event
195
- const handleMouseOver = (e: MouseEvent) => {
196
- if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing()) return;
197
-
198
-
199
- const target = e.target as Element;
200
-
201
- // Prevent hover effects when a dropdown is open
202
- if (isDropdownOpen) {
203
- clearHoverOverlays();
204
- return;
205
- }
206
-
207
- // Prevent hover effects on SVG path elements
208
- if (target.tagName.toLowerCase() === "path") {
209
- clearHoverOverlays();
210
- return;
211
- }
212
-
213
- // Support both data-source-location and data-visual-selector-id
214
- const element = target.closest(
215
- "[data-source-location], [data-visual-selector-id]"
216
- );
217
- if (!element) {
218
- clearHoverOverlays();
219
- return;
220
- }
221
-
222
- // Prefer data-source-location, fallback to data-visual-selector-id
223
- const htmlElement = element as HTMLElement;
224
- const selectorId =
225
- htmlElement.dataset.sourceLocation ||
226
- htmlElement.dataset.visualSelectorId;
227
-
228
- // Skip if this element is already selected
229
- if (selectedElementId === selectorId) {
230
- clearHoverOverlays();
231
- return;
232
- }
194
+ // Hover detection via mousemove + elementFromPoint (since app elements have pointer-events: none)
195
+ let lastHoveredSelectorId: string | null = null;
196
+ let pendingMouseMoveRaf: number | null = null;
233
197
 
234
- // Find all elements with the same ID
235
- const elements = findElementsById(selectorId || null);
198
+ const clearHoverState = () => {
199
+ clearHoverOverlays();
200
+ lastHoveredSelectorId = null;
201
+ };
236
202
 
237
- // Clear previous hover overlays
203
+ const applyHoverOverlays = (selectorId: string) => {
204
+ const elements = findElementsById(selectorId);
238
205
  clearHoverOverlays();
239
206
 
240
- // Create overlays for all matching elements
241
207
  elements.forEach((el) => {
242
208
  const overlay = createOverlay(false);
243
209
  document.body.appendChild(overlay);
@@ -246,12 +212,33 @@ export function setupVisualEditAgent() {
246
212
  });
247
213
 
248
214
  currentHighlightedElements = elements;
215
+ lastHoveredSelectorId = selectorId;
249
216
  };
250
217
 
251
- // Handle mouse out event
252
- const handleMouseOut = () => {
253
- if (isPopoverDragging) return;
254
- clearHoverOverlays();
218
+ const handleMouseMove = (e: MouseEvent) => {
219
+ if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing()) return;
220
+
221
+ if (pendingMouseMoveRaf !== null) return;
222
+ pendingMouseMoveRaf = requestAnimationFrame(() => {
223
+ pendingMouseMoveRaf = null;
224
+
225
+ if (isDropdownOpen) { clearHoverState(); return; }
226
+
227
+ const selectorId = resolveHoverTarget(e.clientX, e.clientY, selectedElementId);
228
+ if (!selectorId) { clearHoverState(); return; }
229
+ if (lastHoveredSelectorId === selectorId) return;
230
+
231
+ applyHoverOverlays(selectorId);
232
+ });
233
+ };
234
+
235
+ // Clear hover overlays when mouse leaves the viewport
236
+ const handleMouseLeave = () => {
237
+ if (pendingMouseMoveRaf !== null) {
238
+ cancelAnimationFrame(pendingMouseMoveRaf);
239
+ pendingMouseMoveRaf = null;
240
+ }
241
+ clearHoverState();
255
242
  };
256
243
 
257
244
  // Handle element click
@@ -288,20 +275,12 @@ export function setupVisualEditAgent() {
288
275
  return;
289
276
  }
290
277
 
291
- // Prevent clicking on SVG path elements
292
- if (target.tagName.toLowerCase() === "path") {
293
- return;
294
- }
295
-
296
278
  // Prevent default behavior immediately when in visual edit mode
297
279
  e.preventDefault();
298
280
  e.stopPropagation();
299
281
  e.stopImmediatePropagation();
300
282
 
301
- // Support both data-source-location and data-visual-selector-id
302
- const element = target.closest(
303
- "[data-source-location], [data-visual-selector-id]"
304
- );
283
+ const element = findInstrumentedElement(e.clientX, e.clientY);
305
284
  if (!element) {
306
285
  return;
307
286
  }
@@ -435,21 +414,21 @@ export function setupVisualEditAgent() {
435
414
  isVisualEditMode = isEnabled;
436
415
 
437
416
  if (!isEnabled) {
417
+ resumeAnimations();
438
418
  inlineEdit.stopEditing();
439
419
  clearSelection();
440
420
  layerController.cleanup();
441
- clearHoverOverlays();
442
-
443
- currentHighlightedElements = [];
421
+ handleMouseLeave();
444
422
  document.body.style.cursor = "default";
445
423
 
446
- document.removeEventListener("mouseover", handleMouseOver);
447
- document.removeEventListener("mouseout", handleMouseOut);
424
+ document.removeEventListener("mousemove", handleMouseMove);
425
+ document.removeEventListener("mouseleave", handleMouseLeave);
448
426
  document.removeEventListener("click", handleElementClick, true);
449
427
  } else {
450
428
  document.body.style.cursor = "crosshair";
451
- document.addEventListener("mouseover", handleMouseOver);
452
- document.addEventListener("mouseout", handleMouseOut);
429
+ stopAnimations();
430
+ document.addEventListener("mousemove", handleMouseMove);
431
+ document.addEventListener("mouseleave", handleMouseLeave);
453
432
  document.addEventListener("click", handleElementClick, true);
454
433
  }
455
434
  };