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