@node-edit-utils/core 2.2.6 → 2.2.8

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 +227 -90
  11. package/dist/node-edit-utils.esm.js +227 -90
  12. package/dist/node-edit-utils.umd.js +227 -90
  13. package/dist/node-edit-utils.umd.min.js +1 -1
  14. package/dist/styles.css +1 -1
  15. package/package.json +4 -2
  16. package/src/index.ts +0 -2
  17. package/src/lib/node-tools/createNodeTools.ts +3 -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 +21 -11
  27. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +40 -7
  28. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +6 -1
  29. package/src/lib/node-tools/select/selectNode.ts +24 -5
  30. package/src/lib/node-tools/text/events/setupMutationObserver.ts +17 -3
  31. package/src/lib/node-tools/text/helpers/enterTextEditMode.ts +9 -0
  32. package/src/lib/node-tools/text/helpers/handleTextChange.ts +27 -0
  33. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.ts +9 -0
  34. package/src/lib/styles/styles.css +28 -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.6
4
+ * @version 2.2.8
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,38 +397,50 @@ 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
  }
353
- // Create tools wrapper with tag label - centered using translateX(-50%)
405
+ // Batch DOM reads
354
406
  const { left, top, height } = getScreenBounds(node);
355
407
  const bottomY = top + height;
408
+ // Create tools wrapper using CSS transform (GPU-accelerated)
356
409
  const toolsWrapper = document.createElement("div");
357
410
  toolsWrapper.classList.add("highlight-frame-tools-wrapper");
358
411
  if (isInstance) {
359
412
  toolsWrapper.classList.add("is-instance");
360
413
  }
361
- toolsWrapper.style.position = "fixed";
362
- toolsWrapper.style.left = `${left}px`;
363
- toolsWrapper.style.top = `${bottomY}px`;
364
- toolsWrapper.style.transform = "translateX(-50%)";
365
- toolsWrapper.style.transformOrigin = "center";
414
+ if (isTextEdit) {
415
+ toolsWrapper.classList.add("is-text-edit");
416
+ }
417
+ toolsWrapper.style.position = "absolute";
418
+ toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
419
+ toolsWrapper.style.transformOrigin = "left center";
366
420
  toolsWrapper.style.pointerEvents = "none";
367
- toolsWrapper.style.zIndex = "5000"; // Match --z-index-highlight (below canvas rulers)
368
- createToolsContainer(node, toolsWrapper, isInstance);
369
- document.body.appendChild(toolsWrapper);
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
+ }
370
429
  };
371
430
 
372
431
  const getComponentColor = () => {
373
432
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
374
433
  };
434
+ const getTextEditColor = () => {
435
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
436
+ };
375
437
  const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
376
438
  // Batch all DOM reads first (single layout pass)
377
439
  const frame = getHighlightFrameElement();
378
440
  if (!frame)
379
441
  return;
380
442
  const isInstance = isComponentInstance(node);
443
+ const isTextEdit = node.contentEditable === "true";
381
444
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
382
445
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
383
446
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
@@ -391,6 +454,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
391
454
  else {
392
455
  frame.classList.remove("is-instance");
393
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
+ }
394
464
  const group = frame.querySelector(".highlight-frame-group");
395
465
  if (!group)
396
466
  return;
@@ -401,15 +471,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
401
471
  if (isInstance) {
402
472
  rect.setAttribute("stroke", getComponentColor());
403
473
  }
474
+ else if (isTextEdit) {
475
+ rect.setAttribute("stroke", getTextEditColor());
476
+ }
404
477
  else {
405
478
  rect.removeAttribute("stroke"); // Use CSS default
406
479
  }
407
- 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");
408
483
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
409
484
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
410
485
  const bounds = getScreenBounds(node);
411
486
  // Calculate all values before any DOM writes
412
487
  const { top, left, width, height } = bounds;
488
+ // Ensure minimum width of 2px
489
+ const minWidth = Math.max(width, 3);
413
490
  const bottomY = top + height;
414
491
  // Update instance classes on tools wrapper and node tools
415
492
  if (toolsWrapper) {
@@ -419,6 +496,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
419
496
  else {
420
497
  toolsWrapper.classList.remove("is-instance");
421
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
+ }
422
506
  }
423
507
  if (nodeTools) {
424
508
  if (isInstance) {
@@ -427,12 +511,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
427
511
  else {
428
512
  nodeTools.classList.remove("is-instance");
429
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
+ }
430
521
  }
431
522
  // Batch all DOM writes (single paint pass)
432
523
  // Update group transform to move entire group (rect + handles) at once
433
524
  group.setAttribute("transform", `translate(${left}, ${top})`);
434
525
  // Update rect dimensions (position is handled by group transform)
435
- rect.setAttribute("width", width.toString());
526
+ rect.setAttribute("width", minWidth.toString());
436
527
  rect.setAttribute("height", height.toString());
437
528
  // Update corner handles positions (relative to group, so only width/height matter)
438
529
  const topLeft = group.querySelector(".handle-top-left");
@@ -447,6 +538,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
447
538
  if (isInstance) {
448
539
  handle.setAttribute("stroke", getComponentColor());
449
540
  }
541
+ else if (isTextEdit) {
542
+ handle.setAttribute("stroke", getTextEditColor());
543
+ }
450
544
  else {
451
545
  handle.removeAttribute("stroke"); // Use CSS default
452
546
  }
@@ -457,22 +551,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
457
551
  topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
458
552
  }
459
553
  if (topRight) {
460
- topRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
554
+ topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
461
555
  topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
462
556
  }
463
557
  if (bottomRight) {
464
- bottomRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
558
+ bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
465
559
  bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
466
560
  }
467
561
  if (bottomLeft) {
468
562
  bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
469
563
  bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
470
564
  }
471
- // Update tools wrapper position (use calculated bounds, not rect attributes)
565
+ // Update tools wrapper position using CSS transform (GPU-accelerated)
472
566
  if (toolsWrapper) {
473
- toolsWrapper.style.left = `${left}px`;
474
- toolsWrapper.style.top = `${bottomY}px`;
567
+ toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
475
568
  }
569
+ // Update tool opacity
476
570
  if (zoom <= 10) {
477
571
  nodeProvider.style.setProperty("--tool-opacity", `1`);
478
572
  }
@@ -485,10 +579,14 @@ const updateHighlightFrameVisibility = (node) => {
485
579
  const frame = getHighlightFrameElement();
486
580
  if (!frame)
487
581
  return;
582
+ // Batch DOM reads
488
583
  const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
489
584
  const displayValue = hasHiddenClass ? "none" : "";
585
+ // Batch DOM writes
490
586
  frame.style.display = displayValue;
491
- 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");
492
590
  if (toolsWrapper) {
493
591
  toolsWrapper.style.display = displayValue;
494
592
  }
@@ -545,11 +643,64 @@ const connectMutationObserver = (element, handler) => {
545
643
  return mutationObserver;
546
644
  };
547
645
 
646
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
647
+ function withRAFThrottle(func) {
648
+ let rafId = null;
649
+ let lastArgs = null;
650
+ const throttled = (...args) => {
651
+ lastArgs = args;
652
+ if (rafId === null) {
653
+ rafId = requestAnimationFrame(() => {
654
+ if (lastArgs) {
655
+ func(...lastArgs);
656
+ }
657
+ rafId = null;
658
+ lastArgs = null;
659
+ });
660
+ }
661
+ };
662
+ throttled.cleanup = () => {
663
+ if (rafId !== null) {
664
+ cancelAnimationFrame(rafId);
665
+ rafId = null;
666
+ lastArgs = null;
667
+ }
668
+ };
669
+ return throttled;
670
+ }
671
+
672
+ const handleTextChange = (node, mutations) => {
673
+ // Check if any mutation is a text content change
674
+ const hasTextChange = mutations.some((mutation) => {
675
+ return (mutation.type === "characterData" ||
676
+ (mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
677
+ });
678
+ if (!hasTextChange) {
679
+ return;
680
+ }
681
+ // Get the text content of the node
682
+ const textContent = node.textContent ?? "";
683
+ // Get the node ID
684
+ const nodeId = node.getAttribute("data-node-id");
685
+ // Send postMessage with the text change
686
+ sendPostMessage("textContentChanged", {
687
+ nodeId,
688
+ textContent,
689
+ });
690
+ };
691
+
548
692
  const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
549
- const mutationObserver = connectMutationObserver(node, () => {
693
+ const throttledHandleTextChange = withRAFThrottle((mutations) => {
694
+ handleTextChange(node, mutations);
695
+ });
696
+ const mutationObserver = connectMutationObserver(node, (mutations) => {
697
+ throttledHandleTextChange(mutations);
550
698
  refreshHighlightFrame(node, nodeProvider, canvasName);
551
699
  });
552
- return () => mutationObserver.disconnect();
700
+ return () => {
701
+ mutationObserver.disconnect();
702
+ throttledHandleTextChange.cleanup();
703
+ };
553
704
  };
554
705
 
555
706
  const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
@@ -634,11 +785,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
634
785
  let parentMutationObserver = null;
635
786
  let selectedNode = null;
636
787
  const text = nodeText(canvasName);
637
- // Combined throttled function for refresh + visibility update
638
- const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
639
- refreshHighlightFrame(node, nodeProvider, canvasName);
640
- updateHighlightFrameVisibility(node);
641
- });
642
788
  const handleEscape = () => {
643
789
  if (text.isEditing()) {
644
790
  text.blurEditMode();
@@ -667,7 +813,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
667
813
  mutationObserver?.disconnect();
668
814
  parentMutationObserver?.disconnect();
669
815
  if (node && nodeProvider) {
670
- text.enableEditMode(node, nodeProvider);
671
816
  // Check if node is still in DOM and handle cleanup if removed
672
817
  const checkNodeExists = () => {
673
818
  if (!document.contains(node)) {
@@ -684,9 +829,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
684
829
  mutationObserver = new MutationObserver(() => {
685
830
  checkNodeExists();
686
831
  if (!document.contains(node))
687
- return; // Exit early if node was removed
688
- // throttledRefreshAndVisibility(node, nodeProvider);
689
- console.log("mutationObserver", node);
832
+ return;
690
833
  refreshHighlightFrame(node, nodeProvider, canvasName);
691
834
  updateHighlightFrameVisibility(node);
692
835
  });
@@ -722,8 +865,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
722
865
  checkNodeExists();
723
866
  if (!document.contains(node))
724
867
  return; // Exit early if node was removed
725
- // throttledRefreshAndVisibility(node, nodeProvider);
726
- console.log("resizeObserver", node);
727
868
  refreshHighlightFrame(node, nodeProvider, canvasName);
728
869
  updateHighlightFrameVisibility(node);
729
870
  });
@@ -733,17 +874,17 @@ const createNodeTools = (element, canvasName = "canvas") => {
733
874
  highlightNode(node) ?? null;
734
875
  if (node && nodeProvider) {
735
876
  updateHighlightFrameVisibility(node);
877
+ updateHighlightFrameVisibility(node);
736
878
  }
737
879
  };
738
880
  // Setup event listener
739
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text.getEditableNode);
881
+ const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
740
882
  const cleanup = () => {
741
883
  removeListeners();
742
884
  resizeObserver?.disconnect();
743
885
  mutationObserver?.disconnect();
744
886
  parentMutationObserver?.disconnect();
745
887
  text.blurEditMode();
746
- throttledRefreshAndVisibility.cleanup();
747
888
  // Clear highlight frame and reset selected node
748
889
  clearHighlightFrame();
749
890
  selectedNode = null;
@@ -772,10 +913,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
772
913
  return nodeTools;
773
914
  };
774
915
 
775
- const getCanvasContainer = () => {
776
- return document.querySelector(".canvas-container");
777
- };
778
-
779
916
  const DEFAULT_WIDTH = 400;
780
917
  const RESIZE_CONFIG = {
781
918
  minWidth: 320,