@node-edit-utils/core 2.1.8 → 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 -159
  12. package/dist/node-edit-utils.esm.js +300 -159
  13. package/dist/node-edit-utils.umd.js +300 -159
  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 -24
  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 +114 -11
  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 -6
  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.8
4
+ * @version 2.2.0
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",
@@ -136,11 +102,6 @@ const isLocked = (node) => {
136
102
  };
137
103
 
138
104
  const processPostMessage = (event, onNodeSelected) => {
139
- // if (event.data.source === "markup-canvas" && event.data.canvasName === "canvas") {
140
- // if (event.data.action === "zoom") {
141
- // // Zoom handling can be implemented here if needed
142
- // }
143
- // }
144
105
  if (event.data.source === "application") {
145
106
  if (event.data.action === "selectedNodeChanged") {
146
107
  const nodeId = event.data.data;
@@ -156,17 +117,14 @@ const processPostMessage = (event, onNodeSelected) => {
156
117
  }
157
118
  };
158
119
 
159
- function getHighlightFrameElement(nodeProvider) {
160
- return nodeProvider.querySelector(".highlight-frame");
161
- }
162
-
163
- const clearHighlightFrame = (nodeProvider) => {
164
- if (!nodeProvider) {
165
- return;
120
+ const clearHighlightFrame = () => {
121
+ const frame = document.body.querySelector(".highlight-frame-overlay");
122
+ if (frame) {
123
+ frame.remove();
166
124
  }
167
- const highlightFrame = getHighlightFrameElement(nodeProvider);
168
- if (highlightFrame) {
169
- highlightFrame.remove();
125
+ const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
126
+ if (toolsWrapper) {
127
+ toolsWrapper.remove();
170
128
  }
171
129
  };
172
130
 
@@ -186,6 +144,21 @@ const getElementsFromPoint = (clickX, clickY) => {
186
144
  }, { elements: [], found: false }).elements;
187
145
  };
188
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
+
189
162
  const targetSameCandidates = (cache, current) => cache.length === current.length && cache.every((el, i) => el === current[i]);
190
163
 
191
164
  let candidateCache = [];
@@ -195,7 +168,9 @@ const selectNode = (event, editableNode) => {
195
168
  const clickX = event.clientX;
196
169
  const clickY = event.clientY;
197
170
  const clickThrough = event.metaKey || event.ctrlKey;
198
- 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));
199
174
  if (editableNode && candidates.includes(editableNode)) {
200
175
  return editableNode;
201
176
  }
@@ -220,7 +195,7 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
220
195
  event.preventDefault();
221
196
  event.stopPropagation();
222
197
  if (nodeProvider && !nodeProvider.contains(event.target)) {
223
- clearHighlightFrame(nodeProvider);
198
+ clearHighlightFrame();
224
199
  onNodeSelected(null);
225
200
  return;
226
201
  }
@@ -252,48 +227,90 @@ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, get
252
227
  };
253
228
  };
254
229
 
255
- function getElementBounds(element, nodeProvider) {
256
- const elementRect = element.getBoundingClientRect();
257
- const componentRootRect = nodeProvider.getBoundingClientRect();
258
- const relativeTop = elementRect.top - componentRootRect.top;
259
- const relativeLeft = elementRect.left - componentRootRect.left;
260
- const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
261
- const top = parseFloat((relativeTop / zoom).toFixed(5));
262
- const left = parseFloat((relativeLeft / zoom).toFixed(5));
263
- const width = Math.max(4, parseFloat((elementRect.width / zoom).toFixed(5)));
264
- 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();
265
263
  return {
266
- top,
267
- left,
268
- width,
269
- height,
264
+ top: rect.top,
265
+ left: rect.left,
266
+ width: rect.width,
267
+ height: rect.height,
270
268
  };
271
269
  }
272
270
 
273
- const createHighlightFrame = (node, nodeProvider) => {
274
- const { top, left, width, height } = getElementBounds(node, nodeProvider);
275
- const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
276
- document.body.style.setProperty("--zoom", zoom.toString());
277
- document.body.style.setProperty("--stroke-width", (2 / zoom).toFixed(3));
278
- const frame = document.createElement("div");
279
- frame.classList.add("highlight-frame");
280
- frame.style.setProperty("--frame-top", `${top}px`);
281
- frame.style.setProperty("--frame-left", `${left}px`);
282
- frame.style.setProperty("--frame-width", `${width}px`);
283
- frame.style.setProperty("--frame-height", `${height}px`);
284
- // 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
285
277
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
286
- 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})`);
287
298
  const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
288
299
  rect.setAttribute("x", "0");
289
300
  rect.setAttribute("y", "0");
290
- rect.setAttribute("width", "100%");
291
- rect.setAttribute("height", "100%");
301
+ rect.setAttribute("width", width.toString());
302
+ rect.setAttribute("height", height.toString());
303
+ rect.setAttribute("vector-effect", "non-scaling-stroke");
292
304
  rect.classList.add("highlight-frame-rect");
293
- svg.appendChild(rect);
294
- frame.appendChild(svg);
295
- const highlightFrame = frame;
296
- 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;
297
314
  };
298
315
 
299
316
  const createTagLabel = (node, nodeTools) => {
@@ -303,56 +320,177 @@ const createTagLabel = (node, nodeTools) => {
303
320
  nodeTools.appendChild(tagLabel);
304
321
  };
305
322
 
306
- const createToolsContainer = (node, highlightFrame) => {
323
+ const createToolsContainer = (node, highlightFrame, isInstance = false) => {
307
324
  const nodeTools = document.createElement("div");
308
325
  nodeTools.className = "node-tools";
326
+ if (isInstance) {
327
+ nodeTools.classList.add("is-instance");
328
+ }
309
329
  highlightFrame.appendChild(nodeTools);
310
330
  createTagLabel(node, nodeTools);
311
331
  };
312
332
 
313
- const highlightNode = (node, nodeProvider) => {
333
+ function getHighlightFrameElement() {
334
+ return document.body.querySelector(".highlight-frame-overlay");
335
+ }
336
+
337
+ const highlightNode = (node) => {
314
338
  if (!node)
315
339
  return;
316
- const existingHighlightFrame = getHighlightFrameElement(nodeProvider);
340
+ const existingHighlightFrame = getHighlightFrameElement();
341
+ const existingToolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
317
342
  if (existingHighlightFrame) {
318
343
  existingHighlightFrame.remove();
319
344
  }
320
- const highlightFrame = createHighlightFrame(node, nodeProvider);
345
+ if (existingToolsWrapper) {
346
+ existingToolsWrapper.remove();
347
+ }
348
+ const isInstance = isComponentInstance(node);
349
+ const highlightFrame = createHighlightFrame(node, isInstance);
321
350
  if (node.contentEditable === "true") {
322
351
  highlightFrame.classList.add("is-editable");
323
352
  }
324
- createToolsContainer(node, highlightFrame);
325
- 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);
326
370
  };
327
371
 
372
+ const getComponentColor = () => {
373
+ return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
374
+ };
328
375
  const refreshHighlightFrame = (node, nodeProvider) => {
329
- const frame = getHighlightFrameElement(nodeProvider);
330
- const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
376
+ // Batch all DOM reads first (single layout pass)
377
+ const frame = getHighlightFrameElement();
331
378
  if (!frame)
332
379
  return;
333
- 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) {
334
477
  nodeProvider.style.setProperty("--tool-opacity", `1`);
335
478
  }
336
479
  else {
337
480
  nodeProvider.style.setProperty("--tool-opacity", `0`);
338
481
  }
339
- const { top, left, width, height } = getElementBounds(node, nodeProvider);
340
- frame.style.setProperty("--frame-top", `${top}px`);
341
- frame.style.setProperty("--frame-left", `${left}px`);
342
- frame.style.setProperty("--frame-width", `${width}px`);
343
- frame.style.setProperty("--frame-height", `${height}px`);
344
482
  };
345
483
 
346
- const updateHighlightFrameVisibility = (node, nodeProvider) => {
347
- const frame = getHighlightFrameElement(nodeProvider);
484
+ const updateHighlightFrameVisibility = (node) => {
485
+ const frame = getHighlightFrameElement();
348
486
  if (!frame)
349
487
  return;
350
488
  const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
351
489
  const displayValue = hasHiddenClass ? "none" : "";
352
490
  frame.style.display = displayValue;
353
- const tagLabel = frame.querySelector(".tag-label");
354
- if (tagLabel) {
355
- tagLabel.style.display = displayValue;
491
+ const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
492
+ if (toolsWrapper) {
493
+ toolsWrapper.style.display = displayValue;
356
494
  }
357
495
  };
358
496
 
@@ -492,21 +630,23 @@ const nodeText = () => {
492
630
  const createNodeTools = (element) => {
493
631
  const nodeProvider = element;
494
632
  let resizeObserver = null;
495
- let nodeResizeObserver = null;
496
633
  let mutationObserver = null;
497
634
  let selectedNode = null;
498
635
  const text = nodeText();
499
- 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
+ });
500
641
  const handleEscape = () => {
501
642
  if (text.isEditing()) {
502
643
  text.blurEditMode();
503
644
  }
504
645
  if (selectedNode) {
505
646
  if (nodeProvider) {
506
- clearHighlightFrame(nodeProvider);
647
+ clearHighlightFrame();
507
648
  selectedNode = null;
508
649
  resizeObserver?.disconnect();
509
- nodeResizeObserver?.disconnect();
510
650
  mutationObserver?.disconnect();
511
651
  }
512
652
  }
@@ -522,33 +662,31 @@ const createNodeTools = (element) => {
522
662
  }
523
663
  }
524
664
  resizeObserver?.disconnect();
525
- nodeResizeObserver?.disconnect();
526
665
  mutationObserver?.disconnect();
527
666
  if (node && nodeProvider) {
528
667
  text.enableEditMode(node, nodeProvider);
529
- resizeObserver = connectResizeObserver(nodeProvider, () => {
530
- throttledFrameRefresh(node, nodeProvider);
531
- });
532
668
  mutationObserver = new MutationObserver(() => {
533
- throttledFrameRefresh(node, nodeProvider);
534
- updateHighlightFrameVisibility(node, nodeProvider);
669
+ // throttledRefreshAndVisibility(node, nodeProvider);
670
+ console.log("mutationObserver", node);
671
+ refreshHighlightFrame(node, nodeProvider);
672
+ updateHighlightFrameVisibility(node);
535
673
  });
536
674
  mutationObserver.observe(node, {
537
675
  attributes: true,
538
- subtree: true,
539
- childList: true,
540
676
  characterData: true,
541
677
  });
542
- nodeResizeObserver = connectResizeObserver(node, () => {
543
- throttledFrameRefresh(node, nodeProvider);
544
- updateHighlightFrameVisibility(node, nodeProvider);
678
+ resizeObserver = connectResizeObserver(node, () => {
679
+ // throttledRefreshAndVisibility(node, nodeProvider);
680
+ console.log("resizeObserver", node);
681
+ refreshHighlightFrame(node, nodeProvider);
682
+ updateHighlightFrameVisibility(node);
545
683
  });
546
684
  }
547
685
  selectedNode = node;
548
686
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
549
- highlightNode(node, nodeProvider) ?? null;
687
+ highlightNode(node) ?? null;
550
688
  if (node && nodeProvider) {
551
- updateHighlightFrameVisibility(node, nodeProvider);
689
+ updateHighlightFrameVisibility(node);
552
690
  }
553
691
  };
554
692
  // Setup event listener
@@ -556,22 +694,25 @@ const createNodeTools = (element) => {
556
694
  const cleanup = () => {
557
695
  removeListeners();
558
696
  resizeObserver?.disconnect();
559
- nodeResizeObserver?.disconnect();
560
697
  mutationObserver?.disconnect();
561
698
  text.blurEditMode();
562
- throttledFrameRefresh.cleanup();
699
+ throttledRefreshAndVisibility.cleanup();
563
700
  };
564
701
  const nodeTools = {
565
702
  selectNode,
566
703
  getSelectedNode: () => selectedNode,
567
704
  refreshHighlightFrame: () => {
568
- 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
+ }
569
711
  },
570
712
  clearSelectedNode: () => {
571
- clearHighlightFrame(nodeProvider);
713
+ clearHighlightFrame();
572
714
  selectedNode = null;
573
715
  resizeObserver?.disconnect();
574
- nodeResizeObserver?.disconnect();
575
716
  mutationObserver?.disconnect();
576
717
  },
577
718
  getEditableNode: () => text.getEditableNode(),