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