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