@node-edit-utils/core 2.2.7 → 2.2.9

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 (34) hide show
  1. package/dist/lib/node-tools/events/click/handleNodeClick.d.ts +2 -1
  2. package/dist/lib/node-tools/events/setupEventListener.d.ts +2 -1
  3. package/dist/lib/node-tools/highlight/createCornerHandles.d.ts +1 -1
  4. package/dist/lib/node-tools/highlight/createHighlightFrame.d.ts +1 -1
  5. package/dist/lib/node-tools/highlight/createToolsContainer.d.ts +1 -1
  6. package/dist/lib/node-tools/select/selectNode.d.ts +2 -1
  7. package/dist/lib/node-tools/text/helpers/enterTextEditMode.d.ts +2 -0
  8. package/dist/lib/node-tools/text/helpers/handleTextChange.d.ts +1 -0
  9. package/dist/lib/node-tools/text/helpers/shouldEnterTextEditMode.d.ts +1 -0
  10. package/dist/node-edit-utils.cjs.js +251 -81
  11. package/dist/node-edit-utils.esm.js +251 -81
  12. package/dist/node-edit-utils.umd.js +251 -81
  13. package/dist/node-edit-utils.umd.min.js +1 -1
  14. package/dist/styles.css +1 -1
  15. package/package.json +1 -1
  16. package/src/index.ts +0 -2
  17. package/src/lib/node-tools/createNodeTools.ts +2 -16
  18. package/src/lib/node-tools/events/click/handleNodeClick.ts +3 -2
  19. package/src/lib/node-tools/events/setupEventListener.ts +3 -2
  20. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +7 -2
  21. package/src/lib/node-tools/highlight/createCornerHandles.ts +12 -6
  22. package/src/lib/node-tools/highlight/createHighlightFrame.ts +25 -7
  23. package/src/lib/node-tools/highlight/createTagLabel.ts +25 -1
  24. package/src/lib/node-tools/highlight/createToolsContainer.ts +4 -1
  25. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +5 -1
  26. package/src/lib/node-tools/highlight/highlightNode.ts +17 -6
  27. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +37 -4
  28. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +4 -1
  29. package/src/lib/node-tools/select/selectNode.ts +24 -5
  30. package/src/lib/node-tools/text/events/setupMutationObserver.ts +53 -3
  31. package/src/lib/node-tools/text/helpers/enterTextEditMode.ts +9 -0
  32. package/src/lib/node-tools/text/helpers/handleTextChange.ts +29 -0
  33. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.ts +9 -0
  34. package/src/lib/styles/styles.css +23 -8
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Markup Canvas
3
3
  * High-performance markup canvas with zoom and pan capabilities
4
- * @version 2.2.7
4
+ * @version 2.2.9
5
5
  */
6
6
  const getCanvasWindowValue = (path, canvasName = "canvas") => {
7
7
  // biome-ignore lint/suspicious/noExplicitAny: global window extension
@@ -55,32 +55,6 @@ const connectResizeObserver = (element, handler) => {
55
55
  return resizeObserver;
56
56
  };
57
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
-
84
58
  function sendPostMessage(action, data) {
85
59
  window.parent.postMessage({
86
60
  source: "node-edit-utils",
@@ -117,17 +91,30 @@ const processPostMessage = (event, onNodeSelected) => {
117
91
  }
118
92
  };
119
93
 
94
+ const getCanvasContainer = () => {
95
+ return document.querySelector(".canvas-container");
96
+ };
97
+
120
98
  const clearHighlightFrame = () => {
121
- const frame = document.body.querySelector(".highlight-frame-overlay");
99
+ const canvasContainer = getCanvasContainer();
100
+ const container = canvasContainer || document.body;
101
+ const frame = container.querySelector(".highlight-frame-overlay");
122
102
  if (frame) {
123
103
  frame.remove();
124
104
  }
125
- const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
105
+ const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
126
106
  if (toolsWrapper) {
127
107
  toolsWrapper.remove();
128
108
  }
129
109
  };
130
110
 
111
+ const enterTextEditMode = (node, nodeProvider, text) => {
112
+ if (!node || !nodeProvider) {
113
+ return;
114
+ }
115
+ text.enableEditMode(node, nodeProvider);
116
+ };
117
+
131
118
  const IGNORED_DOM_ELEMENTS = ["path", "rect", "circle", "ellipse", "polygon", "line", "polyline", "text", "text-noci"];
132
119
 
133
120
  const getElementsFromPoint = (clickX, clickY) => {
@@ -163,7 +150,8 @@ const targetSameCandidates = (cache, current) => cache.length === current.length
163
150
 
164
151
  let candidateCache = [];
165
152
  let attempt = 0;
166
- const selectNode = (event, editableNode) => {
153
+ let lastSelectedNode = null;
154
+ const selectNode = (event, nodeProvider, text) => {
167
155
  let selectedNode = null;
168
156
  const clickX = event.clientX;
169
157
  const clickY = event.clientY;
@@ -171,16 +159,23 @@ const selectNode = (event, editableNode) => {
171
159
  const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
172
160
  !element.classList.contains("select-none") &&
173
161
  !isInsideComponent(element));
162
+ const editableNode = text.getEditableNode();
174
163
  if (editableNode && candidates.includes(editableNode)) {
175
- return editableNode;
164
+ selectedNode = editableNode;
165
+ lastSelectedNode = selectedNode;
166
+ return selectedNode;
176
167
  }
177
168
  if (clickThrough) {
178
169
  candidateCache = [];
179
170
  selectedNode = candidates[0];
171
+ if (lastSelectedNode && lastSelectedNode === selectedNode) {
172
+ enterTextEditMode(selectedNode, nodeProvider, text);
173
+ }
174
+ lastSelectedNode = selectedNode;
180
175
  return selectedNode;
181
176
  }
182
177
  if (targetSameCandidates(candidateCache, candidates)) {
183
- attempt <= candidates.length && attempt++;
178
+ attempt <= candidates.length - 2 && attempt++;
184
179
  }
185
180
  else {
186
181
  attempt = 0;
@@ -188,10 +183,14 @@ const selectNode = (event, editableNode) => {
188
183
  const nodeIndex = candidates.length - 1 - attempt;
189
184
  selectedNode = candidates[nodeIndex];
190
185
  candidateCache = candidates;
186
+ if (lastSelectedNode && lastSelectedNode === selectedNode) {
187
+ enterTextEditMode(selectedNode, nodeProvider, text);
188
+ }
189
+ lastSelectedNode = selectedNode;
191
190
  return selectedNode;
192
191
  };
193
192
 
194
- const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
193
+ const handleNodeClick = (event, nodeProvider, text, onNodeSelected) => {
195
194
  event.preventDefault();
196
195
  event.stopPropagation();
197
196
  if (nodeProvider && !nodeProvider.contains(event.target)) {
@@ -199,16 +198,16 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
199
198
  onNodeSelected(null);
200
199
  return;
201
200
  }
202
- const selectedNode = selectNode(event, editableNode);
201
+ const selectedNode = selectNode(event, nodeProvider, text);
203
202
  onNodeSelected(selectedNode);
204
203
  };
205
204
 
206
- const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, getEditableNode) => {
205
+ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
207
206
  const messageHandler = (event) => {
208
207
  processPostMessage(event, onNodeSelected);
209
208
  };
210
209
  const documentClickHandler = (event) => {
211
- handleNodeClick(event, nodeProvider, getEditableNode(), onNodeSelected);
210
+ handleNodeClick(event, nodeProvider, text, onNodeSelected);
212
211
  };
213
212
  const documentKeydownHandler = (event) => {
214
213
  if (event.key === "Escape") {
@@ -235,7 +234,10 @@ const HANDLE_SIZE = 6;
235
234
  const getComponentColor$2 = () => {
236
235
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
237
236
  };
238
- const createCornerHandle = (group, x, y, className, isInstance = false) => {
237
+ const getTextEditColor$2 = () => {
238
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
239
+ };
240
+ const createCornerHandle = (group, x, y, className, isInstance = false, isTextEdit = false) => {
239
241
  const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
240
242
  // Position relative to group (offset by half handle size to center on corner)
241
243
  handle.setAttribute("x", (x - HANDLE_SIZE / 2).toString());
@@ -247,15 +249,18 @@ const createCornerHandle = (group, x, y, className, isInstance = false) => {
247
249
  if (isInstance) {
248
250
  handle.setAttribute("stroke", getComponentColor$2());
249
251
  }
252
+ else if (isTextEdit) {
253
+ handle.setAttribute("stroke", getTextEditColor$2());
254
+ }
250
255
  group.appendChild(handle);
251
256
  return handle;
252
257
  };
253
- const createCornerHandles = (group, width, height, isInstance = false) => {
258
+ const createCornerHandles = (group, width, height, isInstance = false, isTextEdit = false) => {
254
259
  // 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);
260
+ createCornerHandle(group, 0, 0, "handle-top-left", isInstance, isTextEdit);
261
+ createCornerHandle(group, width, 0, "handle-top-right", isInstance, isTextEdit);
262
+ createCornerHandle(group, width, height, "handle-bottom-right", isInstance, isTextEdit);
263
+ createCornerHandle(group, 0, height, "handle-bottom-left", isInstance, isTextEdit);
259
264
  };
260
265
 
261
266
  function getScreenBounds(element) {
@@ -271,23 +276,31 @@ function getScreenBounds(element) {
271
276
  const getComponentColor$1 = () => {
272
277
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
273
278
  };
274
- const createHighlightFrame = (node, isInstance = false) => {
279
+ const getTextEditColor$1 = () => {
280
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
281
+ };
282
+ const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
275
283
  const { top, left, width, height } = getScreenBounds(node);
284
+ // Ensure minimum width of 2px
285
+ const minWidth = Math.max(width, 3);
276
286
  // Create fixed SVG overlay
277
287
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
278
288
  svg.classList.add("highlight-frame-overlay");
279
289
  if (isInstance) {
280
290
  svg.classList.add("is-instance");
281
291
  }
292
+ if (isTextEdit) {
293
+ svg.classList.add("is-text-edit");
294
+ }
282
295
  svg.setAttribute("data-node-id", node.getAttribute("data-node-id") || "");
283
296
  // Set fixed positioning
284
- svg.style.position = "fixed";
297
+ svg.style.position = "absolute";
285
298
  svg.style.top = "0";
286
299
  svg.style.left = "0";
287
300
  svg.style.width = "100vw";
288
301
  svg.style.height = "100vh";
289
302
  svg.style.pointerEvents = "none";
290
- svg.style.zIndex = "5000";
303
+ svg.style.zIndex = "500";
291
304
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
292
305
  const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
293
306
  svg.setAttribute("width", viewportWidth.toString());
@@ -298,47 +311,85 @@ const createHighlightFrame = (node, isInstance = false) => {
298
311
  const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
299
312
  rect.setAttribute("x", "0");
300
313
  rect.setAttribute("y", "0");
301
- rect.setAttribute("width", width.toString());
314
+ rect.setAttribute("width", minWidth.toString());
302
315
  rect.setAttribute("height", height.toString());
303
316
  rect.setAttribute("vector-effect", "non-scaling-stroke");
304
317
  rect.classList.add("highlight-frame-rect");
305
- // Apply instance color if it's an instance
318
+ // Apply instance color if it's an instance, otherwise text edit color if in text edit mode
306
319
  if (isInstance) {
307
320
  rect.setAttribute("stroke", getComponentColor$1());
308
321
  }
322
+ else if (isTextEdit) {
323
+ rect.setAttribute("stroke", getTextEditColor$1());
324
+ }
309
325
  group.appendChild(rect);
310
- createCornerHandles(group, width, height, isInstance);
326
+ createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
311
327
  svg.appendChild(group);
312
- document.body.appendChild(svg);
328
+ const canvasContainer = getCanvasContainer();
329
+ if (canvasContainer) {
330
+ canvasContainer.appendChild(svg);
331
+ }
332
+ else {
333
+ document.body.appendChild(svg);
334
+ }
313
335
  return svg;
314
336
  };
315
337
 
338
+ const TAG_NAME_MAP = {
339
+ div: "Container",
340
+ h1: "Heading 1",
341
+ h2: "Heading 2",
342
+ h3: "Heading 3",
343
+ h4: "Heading 4",
344
+ h5: "Heading 5",
345
+ h6: "Heading 6",
346
+ p: "Text",
347
+ li: "List Item",
348
+ ul: "Unordered List",
349
+ ol: "Ordered List",
350
+ img: "Image",
351
+ a: "Link",
352
+ };
353
+ const capitalize = (str) => {
354
+ if (!str)
355
+ return str;
356
+ return str.charAt(0).toUpperCase() + str.slice(1);
357
+ };
316
358
  const createTagLabel = (node, nodeTools) => {
317
359
  const tagLabel = document.createElement("div");
318
360
  tagLabel.className = "tag-label";
319
- tagLabel.textContent = node.tagName.toLowerCase();
361
+ const instanceName = node.getAttribute("data-instance-name");
362
+ const tagName = node.tagName.toLowerCase();
363
+ const labelText = instanceName || TAG_NAME_MAP[tagName] || tagName;
364
+ tagLabel.textContent = capitalize(labelText);
320
365
  nodeTools.appendChild(tagLabel);
321
366
  };
322
367
 
323
- const createToolsContainer = (node, highlightFrame, isInstance = false) => {
368
+ const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
324
369
  const nodeTools = document.createElement("div");
325
370
  nodeTools.className = "node-tools";
326
371
  if (isInstance) {
327
372
  nodeTools.classList.add("is-instance");
328
373
  }
374
+ if (isTextEdit) {
375
+ nodeTools.classList.add("is-text-edit");
376
+ }
329
377
  highlightFrame.appendChild(nodeTools);
330
378
  createTagLabel(node, nodeTools);
331
379
  };
332
380
 
333
381
  function getHighlightFrameElement() {
334
- return document.body.querySelector(".highlight-frame-overlay");
382
+ const canvasContainer = getCanvasContainer();
383
+ const container = canvasContainer || document.body;
384
+ return container.querySelector(".highlight-frame-overlay");
335
385
  }
336
386
 
337
387
  const highlightNode = (node) => {
338
388
  if (!node)
339
389
  return;
340
390
  const existingHighlightFrame = getHighlightFrameElement();
341
- const existingToolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
391
+ const canvasContainer = getCanvasContainer();
392
+ const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
342
393
  if (existingHighlightFrame) {
343
394
  existingHighlightFrame.remove();
344
395
  }
@@ -346,7 +397,8 @@ const highlightNode = (node) => {
346
397
  existingToolsWrapper.remove();
347
398
  }
348
399
  const isInstance = isComponentInstance(node);
349
- const highlightFrame = createHighlightFrame(node, isInstance);
400
+ const isTextEdit = node.contentEditable === "true";
401
+ const highlightFrame = createHighlightFrame(node, isInstance, isTextEdit);
350
402
  if (node.contentEditable === "true") {
351
403
  highlightFrame.classList.add("is-editable");
352
404
  }
@@ -359,24 +411,36 @@ const highlightNode = (node) => {
359
411
  if (isInstance) {
360
412
  toolsWrapper.classList.add("is-instance");
361
413
  }
362
- toolsWrapper.style.position = "fixed";
414
+ if (isTextEdit) {
415
+ toolsWrapper.classList.add("is-text-edit");
416
+ }
417
+ toolsWrapper.style.position = "absolute";
363
418
  toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
364
419
  toolsWrapper.style.transformOrigin = "left center";
365
420
  toolsWrapper.style.pointerEvents = "none";
366
- toolsWrapper.style.zIndex = "5000"; // Match --z-index-highlight (below canvas rulers)
367
- createToolsContainer(node, toolsWrapper, isInstance);
368
- document.body.appendChild(toolsWrapper);
421
+ toolsWrapper.style.zIndex = "500";
422
+ createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
423
+ if (canvasContainer) {
424
+ canvasContainer.appendChild(toolsWrapper);
425
+ }
426
+ else {
427
+ document.body.appendChild(toolsWrapper);
428
+ }
369
429
  };
370
430
 
371
431
  const getComponentColor = () => {
372
432
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
373
433
  };
434
+ const getTextEditColor = () => {
435
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
436
+ };
374
437
  const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
375
438
  // Batch all DOM reads first (single layout pass)
376
439
  const frame = getHighlightFrameElement();
377
440
  if (!frame)
378
441
  return;
379
442
  const isInstance = isComponentInstance(node);
443
+ const isTextEdit = node.contentEditable === "true";
380
444
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
381
445
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
382
446
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
@@ -390,6 +454,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
390
454
  else {
391
455
  frame.classList.remove("is-instance");
392
456
  }
457
+ // Update text edit class
458
+ if (isTextEdit) {
459
+ frame.classList.add("is-text-edit");
460
+ }
461
+ else {
462
+ frame.classList.remove("is-text-edit");
463
+ }
393
464
  const group = frame.querySelector(".highlight-frame-group");
394
465
  if (!group)
395
466
  return;
@@ -400,15 +471,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
400
471
  if (isInstance) {
401
472
  rect.setAttribute("stroke", getComponentColor());
402
473
  }
474
+ else if (isTextEdit) {
475
+ rect.setAttribute("stroke", getTextEditColor());
476
+ }
403
477
  else {
404
478
  rect.removeAttribute("stroke"); // Use CSS default
405
479
  }
406
- const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
480
+ const canvasContainer = getCanvasContainer();
481
+ const container = canvasContainer || document.body;
482
+ const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
407
483
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
408
484
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
409
485
  const bounds = getScreenBounds(node);
410
486
  // Calculate all values before any DOM writes
411
487
  const { top, left, width, height } = bounds;
488
+ // Ensure minimum width of 2px
489
+ const minWidth = Math.max(width, 3);
412
490
  const bottomY = top + height;
413
491
  // Update instance classes on tools wrapper and node tools
414
492
  if (toolsWrapper) {
@@ -418,6 +496,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
418
496
  else {
419
497
  toolsWrapper.classList.remove("is-instance");
420
498
  }
499
+ // Update text edit class
500
+ if (isTextEdit) {
501
+ toolsWrapper.classList.add("is-text-edit");
502
+ }
503
+ else {
504
+ toolsWrapper.classList.remove("is-text-edit");
505
+ }
421
506
  }
422
507
  if (nodeTools) {
423
508
  if (isInstance) {
@@ -426,12 +511,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
426
511
  else {
427
512
  nodeTools.classList.remove("is-instance");
428
513
  }
514
+ // Update text edit class
515
+ if (isTextEdit) {
516
+ nodeTools.classList.add("is-text-edit");
517
+ }
518
+ else {
519
+ nodeTools.classList.remove("is-text-edit");
520
+ }
429
521
  }
430
522
  // Batch all DOM writes (single paint pass)
431
523
  // Update group transform to move entire group (rect + handles) at once
432
524
  group.setAttribute("transform", `translate(${left}, ${top})`);
433
525
  // Update rect dimensions (position is handled by group transform)
434
- rect.setAttribute("width", width.toString());
526
+ rect.setAttribute("width", minWidth.toString());
435
527
  rect.setAttribute("height", height.toString());
436
528
  // Update corner handles positions (relative to group, so only width/height matter)
437
529
  const topLeft = group.querySelector(".handle-top-left");
@@ -446,6 +538,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
446
538
  if (isInstance) {
447
539
  handle.setAttribute("stroke", getComponentColor());
448
540
  }
541
+ else if (isTextEdit) {
542
+ handle.setAttribute("stroke", getTextEditColor());
543
+ }
449
544
  else {
450
545
  handle.removeAttribute("stroke"); // Use CSS default
451
546
  }
@@ -456,11 +551,11 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
456
551
  topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
457
552
  }
458
553
  if (topRight) {
459
- topRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
554
+ topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
460
555
  topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
461
556
  }
462
557
  if (bottomRight) {
463
- bottomRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
558
+ bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
464
559
  bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
465
560
  }
466
561
  if (bottomLeft) {
@@ -489,7 +584,9 @@ const updateHighlightFrameVisibility = (node) => {
489
584
  const displayValue = hasHiddenClass ? "none" : "";
490
585
  // Batch DOM writes
491
586
  frame.style.display = displayValue;
492
- const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
587
+ const canvasContainer = getCanvasContainer();
588
+ const container = canvasContainer || document.body;
589
+ const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
493
590
  if (toolsWrapper) {
494
591
  toolsWrapper.style.display = displayValue;
495
592
  }
@@ -546,11 +643,73 @@ const connectMutationObserver = (element, handler) => {
546
643
  return mutationObserver;
547
644
  };
548
645
 
646
+ const handleTextChange = (node, mutations) => {
647
+ // Check if any mutation is a text content change
648
+ const hasTextChange = mutations.some((mutation) => {
649
+ return (mutation.type === "characterData" ||
650
+ (mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
651
+ });
652
+ if (!hasTextChange) {
653
+ return;
654
+ }
655
+ // Get the text content of the node
656
+ const textContent = node.textContent ?? "";
657
+ // Get the node ID
658
+ const nodeId = node.getAttribute("data-node-id");
659
+ console.log("textContentChanged", nodeId, textContent);
660
+ // Send postMessage with the text change
661
+ sendPostMessage("textContentChanged", {
662
+ nodeId,
663
+ textContent,
664
+ });
665
+ };
666
+
549
667
  const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
550
- const mutationObserver = connectMutationObserver(node, () => {
668
+ // Accumulate mutations instead of replacing them
669
+ let pendingMutations = [];
670
+ let rafId1 = null;
671
+ let rafId2 = null;
672
+ const processMutations = () => {
673
+ if (pendingMutations.length > 0) {
674
+ const mutationsToProcess = [...pendingMutations];
675
+ pendingMutations = [];
676
+ handleTextChange(node, mutationsToProcess);
677
+ }
678
+ };
679
+ const scheduleProcess = () => {
680
+ if (rafId1 === null) {
681
+ rafId1 = requestAnimationFrame(() => {
682
+ // First RAF: let browser complete layout
683
+ rafId2 = requestAnimationFrame(() => {
684
+ // Second RAF: read textContent after layout is complete
685
+ processMutations();
686
+ rafId1 = null;
687
+ rafId2 = null;
688
+ });
689
+ });
690
+ }
691
+ };
692
+ const cleanup = () => {
693
+ if (rafId1 !== null) {
694
+ cancelAnimationFrame(rafId1);
695
+ rafId1 = null;
696
+ }
697
+ if (rafId2 !== null) {
698
+ cancelAnimationFrame(rafId2);
699
+ rafId2 = null;
700
+ }
701
+ pendingMutations = [];
702
+ };
703
+ const mutationObserver = connectMutationObserver(node, (mutations) => {
704
+ // Accumulate mutations instead of replacing
705
+ pendingMutations.push(...mutations);
706
+ scheduleProcess();
551
707
  refreshHighlightFrame(node, nodeProvider, canvasName);
552
708
  });
553
- return () => mutationObserver.disconnect();
709
+ return () => {
710
+ mutationObserver.disconnect();
711
+ cleanup();
712
+ };
554
713
  };
555
714
 
556
715
  const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
@@ -635,11 +794,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
635
794
  let parentMutationObserver = null;
636
795
  let selectedNode = null;
637
796
  const text = nodeText(canvasName);
638
- // Combined throttled function for refresh + visibility update
639
- const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
640
- refreshHighlightFrame(node, nodeProvider, canvasName);
641
- updateHighlightFrameVisibility(node);
642
- });
643
797
  const handleEscape = () => {
644
798
  if (text.isEditing()) {
645
799
  text.blurEditMode();
@@ -668,7 +822,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
668
822
  mutationObserver?.disconnect();
669
823
  parentMutationObserver?.disconnect();
670
824
  if (node && nodeProvider) {
671
- text.enableEditMode(node, nodeProvider);
672
825
  // Check if node is still in DOM and handle cleanup if removed
673
826
  const checkNodeExists = () => {
674
827
  if (!document.contains(node)) {
@@ -685,9 +838,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
685
838
  mutationObserver = new MutationObserver(() => {
686
839
  checkNodeExists();
687
840
  if (!document.contains(node))
688
- return; // Exit early if node was removed
689
- // throttledRefreshAndVisibility(node, nodeProvider);
690
- console.log("mutationObserver", node);
841
+ return;
691
842
  refreshHighlightFrame(node, nodeProvider, canvasName);
692
843
  updateHighlightFrameVisibility(node);
693
844
  });
@@ -723,8 +874,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
723
874
  checkNodeExists();
724
875
  if (!document.contains(node))
725
876
  return; // Exit early if node was removed
726
- // throttledRefreshAndVisibility(node, nodeProvider);
727
- console.log("resizeObserver", node);
728
877
  refreshHighlightFrame(node, nodeProvider, canvasName);
729
878
  updateHighlightFrameVisibility(node);
730
879
  });
@@ -738,14 +887,13 @@ const createNodeTools = (element, canvasName = "canvas") => {
738
887
  }
739
888
  };
740
889
  // Setup event listener
741
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text.getEditableNode);
890
+ const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
742
891
  const cleanup = () => {
743
892
  removeListeners();
744
893
  resizeObserver?.disconnect();
745
894
  mutationObserver?.disconnect();
746
895
  parentMutationObserver?.disconnect();
747
896
  text.blurEditMode();
748
- throttledRefreshAndVisibility.cleanup();
749
897
  // Clear highlight frame and reset selected node
750
898
  clearHighlightFrame();
751
899
  selectedNode = null;
@@ -774,9 +922,31 @@ const createNodeTools = (element, canvasName = "canvas") => {
774
922
  return nodeTools;
775
923
  };
776
924
 
777
- const getCanvasContainer = () => {
778
- return document.querySelector(".canvas-container");
779
- };
925
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
926
+ function withRAFThrottle(func) {
927
+ let rafId = null;
928
+ let lastArgs = null;
929
+ const throttled = (...args) => {
930
+ lastArgs = args;
931
+ if (rafId === null) {
932
+ rafId = requestAnimationFrame(() => {
933
+ if (lastArgs) {
934
+ func(...lastArgs);
935
+ }
936
+ rafId = null;
937
+ lastArgs = null;
938
+ });
939
+ }
940
+ };
941
+ throttled.cleanup = () => {
942
+ if (rafId !== null) {
943
+ cancelAnimationFrame(rafId);
944
+ rafId = null;
945
+ lastArgs = null;
946
+ }
947
+ };
948
+ return throttled;
949
+ }
780
950
 
781
951
  const DEFAULT_WIDTH = 400;
782
952
  const RESIZE_CONFIG = {