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