@node-edit-utils/core 2.1.9 → 2.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.
Files changed (33) hide show
  1. package/dist/lib/node-tools/highlight/clearHighlightFrame.d.ts +1 -1
  2. package/dist/lib/node-tools/highlight/createCornerHandles.d.ts +1 -0
  3. package/dist/lib/node-tools/highlight/createHighlightFrame.d.ts +1 -1
  4. package/dist/lib/node-tools/highlight/createToolsContainer.d.ts +1 -1
  5. package/dist/lib/node-tools/highlight/helpers/getHighlightFrameElement.d.ts +1 -1
  6. package/dist/lib/node-tools/highlight/helpers/getScreenBounds.d.ts +6 -0
  7. package/dist/lib/node-tools/highlight/highlightNode.d.ts +1 -1
  8. package/dist/lib/node-tools/highlight/updateHighlightFrameVisibility.d.ts +1 -1
  9. package/dist/lib/node-tools/select/helpers/isComponentInstance.d.ts +1 -0
  10. package/dist/lib/node-tools/select/helpers/isInsideComponent.d.ts +1 -0
  11. package/dist/node-edit-utils.cjs.js +300 -155
  12. package/dist/node-edit-utils.esm.js +300 -155
  13. package/dist/node-edit-utils.umd.js +300 -155
  14. package/dist/node-edit-utils.umd.min.js +1 -1
  15. package/dist/styles.css +1 -1
  16. package/package.json +1 -1
  17. package/src/lib/canvas/createCanvasObserver.ts +8 -6
  18. package/src/lib/node-tools/createNodeTools.ts +27 -22
  19. package/src/lib/node-tools/events/click/handleNodeClick.ts +1 -1
  20. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +7 -8
  21. package/src/lib/node-tools/highlight/createCornerHandles.ts +31 -0
  22. package/src/lib/node-tools/highlight/createHighlightFrame.ts +45 -24
  23. package/src/lib/node-tools/highlight/createToolsContainer.ts +4 -1
  24. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -4
  25. package/src/lib/node-tools/highlight/helpers/getScreenBounds.ts +16 -0
  26. package/src/lib/node-tools/highlight/highlightNode.ts +28 -5
  27. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +113 -14
  28. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +5 -5
  29. package/src/lib/node-tools/select/helpers/isComponentInstance.ts +3 -0
  30. package/src/lib/node-tools/select/helpers/isInsideComponent.ts +18 -0
  31. package/src/lib/node-tools/select/selectNode.ts +5 -1
  32. package/src/lib/post-message/processPostMessage.ts +0 -2
  33. package/src/lib/styles/styles.css +57 -21
@@ -1 +1 @@
1
- export declare const clearHighlightFrame: (nodeProvider: HTMLElement | null) => void;
1
+ export declare const clearHighlightFrame: () => void;
@@ -0,0 +1 @@
1
+ export declare const createCornerHandles: (group: SVGGElement, width: number, height: number, isInstance?: boolean) => void;
@@ -1 +1 @@
1
- export declare const createHighlightFrame: (node: HTMLElement, nodeProvider: HTMLElement) => HTMLElement;
1
+ export declare const createHighlightFrame: (node: HTMLElement, isInstance?: boolean) => SVGSVGElement;
@@ -1 +1 @@
1
- export declare const createToolsContainer: (node: HTMLElement, highlightFrame: HTMLElement) => void;
1
+ export declare const createToolsContainer: (node: HTMLElement, highlightFrame: HTMLElement, isInstance?: boolean) => void;
@@ -1 +1 @@
1
- export declare function getHighlightFrameElement(nodeProvider: HTMLElement): HTMLElement | null;
1
+ export declare function getHighlightFrameElement(): SVGSVGElement | null;
@@ -0,0 +1,6 @@
1
+ export declare function getScreenBounds(element: HTMLElement): {
2
+ top: number;
3
+ left: number;
4
+ width: number;
5
+ height: number;
6
+ };
@@ -1 +1 @@
1
- export declare const highlightNode: (node: HTMLElement | null, nodeProvider: HTMLElement) => void;
1
+ export declare const highlightNode: (node: HTMLElement | null) => void;
@@ -1 +1 @@
1
- export declare const updateHighlightFrameVisibility: (node: HTMLElement, nodeProvider: HTMLElement) => void;
1
+ export declare const updateHighlightFrameVisibility: (node: HTMLElement) => void;
@@ -0,0 +1 @@
1
+ export declare const isComponentInstance: (element: Element) => boolean;
@@ -0,0 +1 @@
1
+ export declare const isInsideComponent: (element: Element) => boolean;
@@ -1,72 +1,10 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.1.9
4
+ * @version 2.2.0
5
5
  */
6
6
  'use strict';
7
7
 
8
- // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
9
- function withRAFThrottle(func) {
10
- let rafId = null;
11
- let lastArgs = null;
12
- const throttled = (...args) => {
13
- lastArgs = args;
14
- if (rafId === null) {
15
- rafId = requestAnimationFrame(() => {
16
- if (lastArgs) {
17
- func(...lastArgs);
18
- }
19
- rafId = null;
20
- lastArgs = null;
21
- });
22
- }
23
- };
24
- throttled.cleanup = () => {
25
- if (rafId !== null) {
26
- cancelAnimationFrame(rafId);
27
- rafId = null;
28
- lastArgs = null;
29
- }
30
- };
31
- return throttled;
32
- }
33
- // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
34
- function withDoubleRAF(func) {
35
- let rafId1 = null;
36
- let rafId2 = null;
37
- let lastArgs = null;
38
- const throttled = (...args) => {
39
- lastArgs = args;
40
- if (rafId1 === null) {
41
- rafId1 = requestAnimationFrame(() => {
42
- // First RAF: let browser complete layout
43
- rafId2 = requestAnimationFrame(() => {
44
- // Second RAF: read bounds after layout is complete
45
- if (lastArgs) {
46
- const currentArgs = lastArgs;
47
- rafId1 = null;
48
- rafId2 = null;
49
- lastArgs = null;
50
- func(...currentArgs);
51
- }
52
- });
53
- });
54
- }
55
- };
56
- throttled.cleanup = () => {
57
- if (rafId1 !== null) {
58
- cancelAnimationFrame(rafId1);
59
- rafId1 = null;
60
- }
61
- if (rafId2 !== null) {
62
- cancelAnimationFrame(rafId2);
63
- rafId2 = null;
64
- }
65
- lastArgs = null;
66
- };
67
- return throttled;
68
- }
69
-
70
8
  const getCanvasWindowValue = (path) => {
71
9
  // biome-ignore lint/suspicious/noExplicitAny: global window extension
72
10
  const canvas = window.canvas;
@@ -88,11 +26,14 @@ function createCanvasObserver() {
88
26
  disconnect: () => { },
89
27
  };
90
28
  }
91
- const throttledUpdate = withRAFThrottle(() => {
92
- applyCanvasState();
93
- });
94
29
  const observer = new MutationObserver(() => {
95
- throttledUpdate();
30
+ applyCanvasState();
31
+ // Refresh highlight frame (throttled via withRAFThrottle)
32
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
33
+ const nodeTools = window.nodeTools;
34
+ if (nodeTools?.refreshHighlightFrame) {
35
+ nodeTools.refreshHighlightFrame();
36
+ }
96
37
  });
97
38
  observer.observe(transformLayer, {
98
39
  attributes: true,
@@ -101,7 +42,6 @@ function createCanvasObserver() {
101
42
  childList: true,
102
43
  });
103
44
  function disconnect() {
104
- throttledUpdate.cleanup();
105
45
  observer.disconnect();
106
46
  }
107
47
  return {
@@ -117,6 +57,32 @@ const connectResizeObserver = (element, handler) => {
117
57
  return resizeObserver;
118
58
  };
119
59
 
60
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
61
+ function withRAFThrottle(func) {
62
+ let rafId = null;
63
+ let lastArgs = null;
64
+ const throttled = (...args) => {
65
+ lastArgs = args;
66
+ if (rafId === null) {
67
+ rafId = requestAnimationFrame(() => {
68
+ if (lastArgs) {
69
+ func(...lastArgs);
70
+ }
71
+ rafId = null;
72
+ lastArgs = null;
73
+ });
74
+ }
75
+ };
76
+ throttled.cleanup = () => {
77
+ if (rafId !== null) {
78
+ cancelAnimationFrame(rafId);
79
+ rafId = null;
80
+ lastArgs = null;
81
+ }
82
+ };
83
+ return throttled;
84
+ }
85
+
120
86
  function sendPostMessage(action, data) {
121
87
  window.parent.postMessage({
122
88
  source: "node-edit-utils",
@@ -142,7 +108,6 @@ const processPostMessage = (event, onNodeSelected) => {
142
108
  if (event.data.action === "selectedNodeChanged") {
143
109
  const nodeId = event.data.data;
144
110
  const selectedNode = document.querySelector(`[data-node-id="${nodeId}"]`);
145
- console.log("selectedNode", selectedNode);
146
111
  if (isLocked(selectedNode)) {
147
112
  onNodeSelected?.(null);
148
113
  return;
@@ -154,17 +119,14 @@ const processPostMessage = (event, onNodeSelected) => {
154
119
  }
155
120
  };
156
121
 
157
- function getHighlightFrameElement(nodeProvider) {
158
- return nodeProvider.querySelector(".highlight-frame");
159
- }
160
-
161
- const clearHighlightFrame = (nodeProvider) => {
162
- if (!nodeProvider) {
163
- return;
122
+ const clearHighlightFrame = () => {
123
+ const frame = document.body.querySelector(".highlight-frame-overlay");
124
+ if (frame) {
125
+ frame.remove();
164
126
  }
165
- const highlightFrame = getHighlightFrameElement(nodeProvider);
166
- if (highlightFrame) {
167
- highlightFrame.remove();
127
+ const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
128
+ if (toolsWrapper) {
129
+ toolsWrapper.remove();
168
130
  }
169
131
  };
170
132
 
@@ -184,6 +146,21 @@ const getElementsFromPoint = (clickX, clickY) => {
184
146
  }, { elements: [], found: false }).elements;
185
147
  };
186
148
 
149
+ const isInsideComponent = (element) => {
150
+ let current = element.parentElement;
151
+ while (current) {
152
+ if (current.getAttribute("data-instance") === "true") {
153
+ return true;
154
+ }
155
+ // Stop at node-provider to avoid checking beyond the editable area
156
+ if (current.getAttribute("data-role") === "node-provider") {
157
+ break;
158
+ }
159
+ current = current.parentElement;
160
+ }
161
+ return false;
162
+ };
163
+
187
164
  const targetSameCandidates = (cache, current) => cache.length === current.length && cache.every((el, i) => el === current[i]);
188
165
 
189
166
  let candidateCache = [];
@@ -193,7 +170,9 @@ const selectNode = (event, editableNode) => {
193
170
  const clickX = event.clientX;
194
171
  const clickY = event.clientY;
195
172
  const clickThrough = event.metaKey || event.ctrlKey;
196
- const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) && !element.classList.contains("select-none"));
173
+ const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
174
+ !element.classList.contains("select-none") &&
175
+ !isInsideComponent(element));
197
176
  if (editableNode && candidates.includes(editableNode)) {
198
177
  return editableNode;
199
178
  }
@@ -218,7 +197,7 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
218
197
  event.preventDefault();
219
198
  event.stopPropagation();
220
199
  if (nodeProvider && !nodeProvider.contains(event.target)) {
221
- clearHighlightFrame(nodeProvider);
200
+ clearHighlightFrame();
222
201
  onNodeSelected(null);
223
202
  return;
224
203
  }
@@ -250,48 +229,90 @@ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, get
250
229
  };
251
230
  };
252
231
 
253
- function getElementBounds(element, nodeProvider) {
254
- const elementRect = element.getBoundingClientRect();
255
- const componentRootRect = nodeProvider.getBoundingClientRect();
256
- const relativeTop = elementRect.top - componentRootRect.top;
257
- const relativeLeft = elementRect.left - componentRootRect.left;
258
- const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
259
- const top = parseFloat((relativeTop / zoom).toFixed(5));
260
- const left = parseFloat((relativeLeft / zoom).toFixed(5));
261
- const width = Math.max(4, parseFloat((elementRect.width / zoom).toFixed(5)));
262
- const height = parseFloat((elementRect.height / zoom).toFixed(5));
232
+ const isComponentInstance = (element) => {
233
+ return element.getAttribute("data-instance") === "true";
234
+ };
235
+
236
+ const HANDLE_SIZE = 6;
237
+ const getComponentColor$2 = () => {
238
+ return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
239
+ };
240
+ const createCornerHandle = (group, x, y, className, isInstance = false) => {
241
+ const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
242
+ // Position relative to group (offset by half handle size to center on corner)
243
+ handle.setAttribute("x", (x - HANDLE_SIZE / 2).toString());
244
+ handle.setAttribute("y", (y - HANDLE_SIZE / 2).toString());
245
+ handle.setAttribute("width", HANDLE_SIZE.toString());
246
+ handle.setAttribute("height", HANDLE_SIZE.toString());
247
+ handle.setAttribute("vector-effect", "non-scaling-stroke");
248
+ handle.classList.add("highlight-frame-handle", className);
249
+ if (isInstance) {
250
+ handle.setAttribute("stroke", getComponentColor$2());
251
+ }
252
+ group.appendChild(handle);
253
+ return handle;
254
+ };
255
+ const createCornerHandles = (group, width, height, isInstance = false) => {
256
+ // Create corner handles using relative coordinates (group handles positioning)
257
+ createCornerHandle(group, 0, 0, "handle-top-left", isInstance);
258
+ createCornerHandle(group, width, 0, "handle-top-right", isInstance);
259
+ createCornerHandle(group, width, height, "handle-bottom-right", isInstance);
260
+ createCornerHandle(group, 0, height, "handle-bottom-left", isInstance);
261
+ };
262
+
263
+ function getScreenBounds(element) {
264
+ const rect = element.getBoundingClientRect();
263
265
  return {
264
- top,
265
- left,
266
- width,
267
- height,
266
+ top: rect.top,
267
+ left: rect.left,
268
+ width: rect.width,
269
+ height: rect.height,
268
270
  };
269
271
  }
270
272
 
271
- const createHighlightFrame = (node, nodeProvider) => {
272
- const { top, left, width, height } = getElementBounds(node, nodeProvider);
273
- const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
274
- document.body.style.setProperty("--zoom", zoom.toString());
275
- document.body.style.setProperty("--stroke-width", (2 / zoom).toFixed(3));
276
- const frame = document.createElement("div");
277
- frame.classList.add("highlight-frame");
278
- frame.style.setProperty("--frame-top", `${top}px`);
279
- frame.style.setProperty("--frame-left", `${left}px`);
280
- frame.style.setProperty("--frame-width", `${width}px`);
281
- frame.style.setProperty("--frame-height", `${height}px`);
282
- // Create SVG overlay for outline
273
+ const getComponentColor$1 = () => {
274
+ return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
275
+ };
276
+ const createHighlightFrame = (node, isInstance = false) => {
277
+ const { top, left, width, height } = getScreenBounds(node);
278
+ // Create fixed SVG overlay
283
279
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
284
- svg.classList.add("highlight-frame-svg");
280
+ svg.classList.add("highlight-frame-overlay");
281
+ if (isInstance) {
282
+ svg.classList.add("is-instance");
283
+ }
284
+ svg.setAttribute("data-node-id", node.getAttribute("data-node-id") || "");
285
+ // Set fixed positioning
286
+ svg.style.position = "fixed";
287
+ svg.style.top = "0";
288
+ svg.style.left = "0";
289
+ svg.style.width = "100vw";
290
+ svg.style.height = "100vh";
291
+ svg.style.pointerEvents = "none";
292
+ svg.style.zIndex = "5000";
293
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
294
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
295
+ svg.setAttribute("width", viewportWidth.toString());
296
+ svg.setAttribute("height", viewportHeight.toString());
297
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
298
+ group.classList.add("highlight-frame-group");
299
+ group.setAttribute("transform", `translate(${left}, ${top})`);
285
300
  const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
286
301
  rect.setAttribute("x", "0");
287
302
  rect.setAttribute("y", "0");
288
- rect.setAttribute("width", "100%");
289
- rect.setAttribute("height", "100%");
303
+ rect.setAttribute("width", width.toString());
304
+ rect.setAttribute("height", height.toString());
305
+ rect.setAttribute("vector-effect", "non-scaling-stroke");
290
306
  rect.classList.add("highlight-frame-rect");
291
- svg.appendChild(rect);
292
- frame.appendChild(svg);
293
- const highlightFrame = frame;
294
- return highlightFrame;
307
+ // Apply instance color if it's an instance
308
+ if (isInstance) {
309
+ rect.setAttribute("stroke", getComponentColor$1());
310
+ }
311
+ group.appendChild(rect);
312
+ createCornerHandles(group, width, height, isInstance);
313
+ svg.appendChild(group);
314
+ document.body.appendChild(svg);
315
+ return svg;
295
316
  };
296
317
 
297
318
  const createTagLabel = (node, nodeTools) => {
@@ -301,58 +322,177 @@ const createTagLabel = (node, nodeTools) => {
301
322
  nodeTools.appendChild(tagLabel);
302
323
  };
303
324
 
304
- const createToolsContainer = (node, highlightFrame) => {
325
+ const createToolsContainer = (node, highlightFrame, isInstance = false) => {
305
326
  const nodeTools = document.createElement("div");
306
327
  nodeTools.className = "node-tools";
328
+ if (isInstance) {
329
+ nodeTools.classList.add("is-instance");
330
+ }
307
331
  highlightFrame.appendChild(nodeTools);
308
332
  createTagLabel(node, nodeTools);
309
333
  };
310
334
 
311
- const highlightNode = (node, nodeProvider) => {
335
+ function getHighlightFrameElement() {
336
+ return document.body.querySelector(".highlight-frame-overlay");
337
+ }
338
+
339
+ const highlightNode = (node) => {
312
340
  if (!node)
313
341
  return;
314
- const existingHighlightFrame = getHighlightFrameElement(nodeProvider);
342
+ const existingHighlightFrame = getHighlightFrameElement();
343
+ const existingToolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
315
344
  if (existingHighlightFrame) {
316
345
  existingHighlightFrame.remove();
317
346
  }
318
- const highlightFrame = createHighlightFrame(node, nodeProvider);
347
+ if (existingToolsWrapper) {
348
+ existingToolsWrapper.remove();
349
+ }
350
+ const isInstance = isComponentInstance(node);
351
+ const highlightFrame = createHighlightFrame(node, isInstance);
319
352
  if (node.contentEditable === "true") {
320
353
  highlightFrame.classList.add("is-editable");
321
354
  }
322
- createToolsContainer(node, highlightFrame);
323
- nodeProvider.appendChild(highlightFrame);
355
+ // Create tools wrapper with tag label - centered using translateX(-50%)
356
+ const { left, top, height } = getScreenBounds(node);
357
+ const bottomY = top + height;
358
+ const toolsWrapper = document.createElement("div");
359
+ toolsWrapper.classList.add("highlight-frame-tools-wrapper");
360
+ if (isInstance) {
361
+ toolsWrapper.classList.add("is-instance");
362
+ }
363
+ toolsWrapper.style.position = "fixed";
364
+ toolsWrapper.style.left = `${left}px`;
365
+ toolsWrapper.style.top = `${bottomY}px`;
366
+ toolsWrapper.style.transform = "translateX(-50%)";
367
+ toolsWrapper.style.transformOrigin = "center";
368
+ toolsWrapper.style.pointerEvents = "none";
369
+ toolsWrapper.style.zIndex = "5000"; // Match --z-index-highlight (below canvas rulers)
370
+ createToolsContainer(node, toolsWrapper, isInstance);
371
+ document.body.appendChild(toolsWrapper);
324
372
  };
325
373
 
374
+ const getComponentColor = () => {
375
+ return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
376
+ };
326
377
  const refreshHighlightFrame = (node, nodeProvider) => {
327
- const frame = getHighlightFrameElement(nodeProvider);
328
- const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
329
- console.log("1. refreshHighlightFrame", node);
378
+ // Batch all DOM reads first (single layout pass)
379
+ const frame = getHighlightFrameElement();
330
380
  if (!frame)
331
381
  return;
332
- if (zoom >= 0.3) {
382
+ const isInstance = isComponentInstance(node);
383
+ // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
384
+ // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
385
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
386
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
387
+ frame.setAttribute("width", viewportWidth.toString());
388
+ frame.setAttribute("height", viewportHeight.toString());
389
+ // Update instance class
390
+ if (isInstance) {
391
+ frame.classList.add("is-instance");
392
+ }
393
+ else {
394
+ frame.classList.remove("is-instance");
395
+ }
396
+ const group = frame.querySelector(".highlight-frame-group");
397
+ if (!group)
398
+ return;
399
+ const rect = group.querySelector("rect");
400
+ if (!rect)
401
+ return;
402
+ // Update instance color
403
+ if (isInstance) {
404
+ rect.setAttribute("stroke", getComponentColor());
405
+ }
406
+ else {
407
+ rect.removeAttribute("stroke"); // Use CSS default
408
+ }
409
+ const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
410
+ const nodeTools = toolsWrapper?.querySelector(".node-tools");
411
+ const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
412
+ const bounds = getScreenBounds(node);
413
+ // Calculate all values before any DOM writes
414
+ const { top, left, width, height } = bounds;
415
+ const bottomY = top + height;
416
+ // Update instance classes on tools wrapper and node tools
417
+ if (toolsWrapper) {
418
+ if (isInstance) {
419
+ toolsWrapper.classList.add("is-instance");
420
+ }
421
+ else {
422
+ toolsWrapper.classList.remove("is-instance");
423
+ }
424
+ }
425
+ if (nodeTools) {
426
+ if (isInstance) {
427
+ nodeTools.classList.add("is-instance");
428
+ }
429
+ else {
430
+ nodeTools.classList.remove("is-instance");
431
+ }
432
+ }
433
+ // Batch all DOM writes (single paint pass)
434
+ // Update group transform to move entire group (rect + handles) at once
435
+ group.setAttribute("transform", `translate(${left}, ${top})`);
436
+ // Update rect dimensions (position is handled by group transform)
437
+ rect.setAttribute("width", width.toString());
438
+ rect.setAttribute("height", height.toString());
439
+ // Update corner handles positions (relative to group, so only width/height matter)
440
+ const topLeft = group.querySelector(".handle-top-left");
441
+ const topRight = group.querySelector(".handle-top-right");
442
+ const bottomRight = group.querySelector(".handle-bottom-right");
443
+ const bottomLeft = group.querySelector(".handle-bottom-left");
444
+ const HANDLE_SIZE = 6;
445
+ // Update handle colors and positions
446
+ const handles = [topLeft, topRight, bottomRight, bottomLeft];
447
+ handles.forEach((handle) => {
448
+ if (handle) {
449
+ if (isInstance) {
450
+ handle.setAttribute("stroke", getComponentColor());
451
+ }
452
+ else {
453
+ handle.removeAttribute("stroke"); // Use CSS default
454
+ }
455
+ }
456
+ });
457
+ if (topLeft) {
458
+ topLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
459
+ topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
460
+ }
461
+ if (topRight) {
462
+ topRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
463
+ topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
464
+ }
465
+ if (bottomRight) {
466
+ bottomRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
467
+ bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
468
+ }
469
+ if (bottomLeft) {
470
+ bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
471
+ bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
472
+ }
473
+ // Update tools wrapper position (use calculated bounds, not rect attributes)
474
+ if (toolsWrapper) {
475
+ toolsWrapper.style.left = `${left}px`;
476
+ toolsWrapper.style.top = `${bottomY}px`;
477
+ }
478
+ if (zoom <= 10) {
333
479
  nodeProvider.style.setProperty("--tool-opacity", `1`);
334
480
  }
335
481
  else {
336
482
  nodeProvider.style.setProperty("--tool-opacity", `0`);
337
483
  }
338
- const { top, left, width, height } = getElementBounds(node, nodeProvider);
339
- frame.style.setProperty("--frame-top", `${top}px`);
340
- frame.style.setProperty("--frame-left", `${left}px`);
341
- frame.style.setProperty("--frame-width", `${width}px`);
342
- frame.style.setProperty("--frame-height", `${height}px`);
343
- console.log("2. refreshHighlightFrame", top, left, width, height);
344
484
  };
345
485
 
346
- const updateHighlightFrameVisibility = (node, nodeProvider) => {
347
- const frame = getHighlightFrameElement(nodeProvider);
486
+ const updateHighlightFrameVisibility = (node) => {
487
+ const frame = getHighlightFrameElement();
348
488
  if (!frame)
349
489
  return;
350
490
  const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
351
491
  const displayValue = hasHiddenClass ? "none" : "";
352
492
  frame.style.display = displayValue;
353
- const tagLabel = frame.querySelector(".tag-label");
354
- if (tagLabel) {
355
- tagLabel.style.display = displayValue;
493
+ const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
494
+ if (toolsWrapper) {
495
+ toolsWrapper.style.display = displayValue;
356
496
  }
357
497
  };
358
498
 
@@ -492,21 +632,23 @@ const nodeText = () => {
492
632
  const createNodeTools = (element) => {
493
633
  const nodeProvider = element;
494
634
  let resizeObserver = null;
495
- let nodeResizeObserver = null;
496
635
  let mutationObserver = null;
497
636
  let selectedNode = null;
498
637
  const text = nodeText();
499
- const throttledFrameRefresh = withDoubleRAF(refreshHighlightFrame);
638
+ // Combined throttled function for refresh + visibility update
639
+ const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
640
+ refreshHighlightFrame(node, nodeProvider);
641
+ updateHighlightFrameVisibility(node);
642
+ });
500
643
  const handleEscape = () => {
501
644
  if (text.isEditing()) {
502
645
  text.blurEditMode();
503
646
  }
504
647
  if (selectedNode) {
505
648
  if (nodeProvider) {
506
- clearHighlightFrame(nodeProvider);
649
+ clearHighlightFrame();
507
650
  selectedNode = null;
508
651
  resizeObserver?.disconnect();
509
- nodeResizeObserver?.disconnect();
510
652
  mutationObserver?.disconnect();
511
653
  }
512
654
  }
@@ -522,31 +664,31 @@ const createNodeTools = (element) => {
522
664
  }
523
665
  }
524
666
  resizeObserver?.disconnect();
525
- nodeResizeObserver?.disconnect();
526
667
  mutationObserver?.disconnect();
527
668
  if (node && nodeProvider) {
528
669
  text.enableEditMode(node, nodeProvider);
529
- resizeObserver = connectResizeObserver(nodeProvider, () => {
530
- throttledFrameRefresh(node, nodeProvider);
531
- });
532
670
  mutationObserver = new MutationObserver(() => {
533
- throttledFrameRefresh(node, nodeProvider);
534
- updateHighlightFrameVisibility(node, nodeProvider);
671
+ // throttledRefreshAndVisibility(node, nodeProvider);
672
+ console.log("mutationObserver", node);
673
+ refreshHighlightFrame(node, nodeProvider);
674
+ updateHighlightFrameVisibility(node);
535
675
  });
536
676
  mutationObserver.observe(node, {
537
677
  attributes: true,
538
678
  characterData: true,
539
679
  });
540
- nodeResizeObserver = connectResizeObserver(node, () => {
541
- throttledFrameRefresh(node, nodeProvider);
542
- updateHighlightFrameVisibility(node, nodeProvider);
680
+ resizeObserver = connectResizeObserver(node, () => {
681
+ // throttledRefreshAndVisibility(node, nodeProvider);
682
+ console.log("resizeObserver", node);
683
+ refreshHighlightFrame(node, nodeProvider);
684
+ updateHighlightFrameVisibility(node);
543
685
  });
544
686
  }
545
687
  selectedNode = node;
546
688
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
547
- highlightNode(node, nodeProvider) ?? null;
689
+ highlightNode(node) ?? null;
548
690
  if (node && nodeProvider) {
549
- updateHighlightFrameVisibility(node, nodeProvider);
691
+ updateHighlightFrameVisibility(node);
550
692
  }
551
693
  };
552
694
  // Setup event listener
@@ -554,22 +696,25 @@ const createNodeTools = (element) => {
554
696
  const cleanup = () => {
555
697
  removeListeners();
556
698
  resizeObserver?.disconnect();
557
- nodeResizeObserver?.disconnect();
558
699
  mutationObserver?.disconnect();
559
700
  text.blurEditMode();
560
- throttledFrameRefresh.cleanup();
701
+ throttledRefreshAndVisibility.cleanup();
561
702
  };
562
703
  const nodeTools = {
563
704
  selectNode,
564
705
  getSelectedNode: () => selectedNode,
565
706
  refreshHighlightFrame: () => {
566
- throttledFrameRefresh(selectedNode, nodeProvider);
707
+ if (selectedNode && nodeProvider) {
708
+ // Call directly (not throttled) since this is typically called from already-throttled contexts
709
+ // to avoid double RAF
710
+ refreshHighlightFrame(selectedNode, nodeProvider);
711
+ updateHighlightFrameVisibility(selectedNode);
712
+ }
567
713
  },
568
714
  clearSelectedNode: () => {
569
- clearHighlightFrame(nodeProvider);
715
+ clearHighlightFrame();
570
716
  selectedNode = null;
571
717
  resizeObserver?.disconnect();
572
- nodeResizeObserver?.disconnect();
573
718
  mutationObserver?.disconnect();
574
719
  },
575
720
  getEditableNode: () => text.getEditableNode(),