@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 +1,2 @@
1
- export declare const handleNodeClick: (event: MouseEvent, nodeProvider: HTMLElement | null, editableNode: HTMLElement | null, onNodeSelected: (node: HTMLElement | null) => void) => void;
1
+ import type { NodeText } from "../../text/types";
2
+ export declare const handleNodeClick: (event: MouseEvent, nodeProvider: HTMLElement | null, text: NodeText, onNodeSelected: (node: HTMLElement | null) => void) => void;
@@ -1 +1,2 @@
1
- export declare const setupEventListener: (nodeProvider: HTMLElement | null, onNodeSelected: (node: HTMLElement | null) => void, onEscapePressed: () => void, getEditableNode: () => HTMLElement | null) => (() => void);
1
+ import type { NodeText } from "../text/types";
2
+ export declare const setupEventListener: (nodeProvider: HTMLElement | null, onNodeSelected: (node: HTMLElement | null) => void, onEscapePressed: () => void, text: NodeText) => (() => void);
@@ -1 +1 @@
1
- export declare const createCornerHandles: (group: SVGGElement, width: number, height: number, isInstance?: boolean) => void;
1
+ export declare const createCornerHandles: (group: SVGGElement, width: number, height: number, isInstance?: boolean, isTextEdit?: boolean) => void;
@@ -1 +1 @@
1
- export declare const createHighlightFrame: (node: HTMLElement, isInstance?: boolean) => SVGSVGElement;
1
+ export declare const createHighlightFrame: (node: HTMLElement, isInstance?: boolean, isTextEdit?: boolean) => SVGSVGElement;
@@ -1 +1 @@
1
- export declare const createToolsContainer: (node: HTMLElement, highlightFrame: HTMLElement, isInstance?: boolean) => void;
1
+ export declare const createToolsContainer: (node: HTMLElement, highlightFrame: HTMLElement, isInstance?: boolean, isTextEdit?: boolean) => void;
@@ -1 +1,2 @@
1
- export declare const selectNode: (event: MouseEvent, editableNode: HTMLElement | null) => HTMLElement | null;
1
+ import type { NodeText } from "../text/types";
2
+ export declare const selectNode: (event: MouseEvent, nodeProvider: HTMLElement | null, text: NodeText) => HTMLElement | null;
@@ -0,0 +1,2 @@
1
+ import type { NodeText } from "../types";
2
+ export declare const enterTextEditMode: (node: HTMLElement, nodeProvider: HTMLElement | null, text: NodeText) => void;
@@ -0,0 +1 @@
1
+ export declare const handleTextChange: (node: HTMLElement, mutations: MutationRecord[]) => void;
@@ -0,0 +1 @@
1
+ export declare const shouldEnterTextEditMode: (node: HTMLElement | null) => boolean;
@@ -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
  'use strict';
7
7
 
@@ -57,32 +57,6 @@ const connectResizeObserver = (element, handler) => {
57
57
  return resizeObserver;
58
58
  };
59
59
 
60
- // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
61
- function withRAFThrottle(func) {
62
- let rafId = null;
63
- let lastArgs = null;
64
- const throttled = (...args) => {
65
- lastArgs = args;
66
- if (rafId === null) {
67
- rafId = requestAnimationFrame(() => {
68
- if (lastArgs) {
69
- func(...lastArgs);
70
- }
71
- rafId = null;
72
- lastArgs = null;
73
- });
74
- }
75
- };
76
- throttled.cleanup = () => {
77
- if (rafId !== null) {
78
- cancelAnimationFrame(rafId);
79
- rafId = null;
80
- lastArgs = null;
81
- }
82
- };
83
- return throttled;
84
- }
85
-
86
60
  function sendPostMessage(action, data) {
87
61
  window.parent.postMessage({
88
62
  source: "node-edit-utils",
@@ -119,17 +93,30 @@ const processPostMessage = (event, onNodeSelected) => {
119
93
  }
120
94
  };
121
95
 
96
+ const getCanvasContainer = () => {
97
+ return document.querySelector(".canvas-container");
98
+ };
99
+
122
100
  const clearHighlightFrame = () => {
123
- const frame = document.body.querySelector(".highlight-frame-overlay");
101
+ const canvasContainer = getCanvasContainer();
102
+ const container = canvasContainer || document.body;
103
+ const frame = container.querySelector(".highlight-frame-overlay");
124
104
  if (frame) {
125
105
  frame.remove();
126
106
  }
127
- const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
107
+ const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
128
108
  if (toolsWrapper) {
129
109
  toolsWrapper.remove();
130
110
  }
131
111
  };
132
112
 
113
+ const enterTextEditMode = (node, nodeProvider, text) => {
114
+ if (!node || !nodeProvider) {
115
+ return;
116
+ }
117
+ text.enableEditMode(node, nodeProvider);
118
+ };
119
+
133
120
  const IGNORED_DOM_ELEMENTS = ["path", "rect", "circle", "ellipse", "polygon", "line", "polyline", "text", "text-noci"];
134
121
 
135
122
  const getElementsFromPoint = (clickX, clickY) => {
@@ -165,7 +152,8 @@ const targetSameCandidates = (cache, current) => cache.length === current.length
165
152
 
166
153
  let candidateCache = [];
167
154
  let attempt = 0;
168
- const selectNode = (event, editableNode) => {
155
+ let lastSelectedNode = null;
156
+ const selectNode = (event, nodeProvider, text) => {
169
157
  let selectedNode = null;
170
158
  const clickX = event.clientX;
171
159
  const clickY = event.clientY;
@@ -173,16 +161,23 @@ const selectNode = (event, editableNode) => {
173
161
  const candidates = getElementsFromPoint(clickX, clickY).filter((element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
174
162
  !element.classList.contains("select-none") &&
175
163
  !isInsideComponent(element));
164
+ const editableNode = text.getEditableNode();
176
165
  if (editableNode && candidates.includes(editableNode)) {
177
- return editableNode;
166
+ selectedNode = editableNode;
167
+ lastSelectedNode = selectedNode;
168
+ return selectedNode;
178
169
  }
179
170
  if (clickThrough) {
180
171
  candidateCache = [];
181
172
  selectedNode = candidates[0];
173
+ if (lastSelectedNode && lastSelectedNode === selectedNode) {
174
+ enterTextEditMode(selectedNode, nodeProvider, text);
175
+ }
176
+ lastSelectedNode = selectedNode;
182
177
  return selectedNode;
183
178
  }
184
179
  if (targetSameCandidates(candidateCache, candidates)) {
185
- attempt <= candidates.length && attempt++;
180
+ attempt <= candidates.length - 2 && attempt++;
186
181
  }
187
182
  else {
188
183
  attempt = 0;
@@ -190,10 +185,14 @@ const selectNode = (event, editableNode) => {
190
185
  const nodeIndex = candidates.length - 1 - attempt;
191
186
  selectedNode = candidates[nodeIndex];
192
187
  candidateCache = candidates;
188
+ if (lastSelectedNode && lastSelectedNode === selectedNode) {
189
+ enterTextEditMode(selectedNode, nodeProvider, text);
190
+ }
191
+ lastSelectedNode = selectedNode;
193
192
  return selectedNode;
194
193
  };
195
194
 
196
- const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
195
+ const handleNodeClick = (event, nodeProvider, text, onNodeSelected) => {
197
196
  event.preventDefault();
198
197
  event.stopPropagation();
199
198
  if (nodeProvider && !nodeProvider.contains(event.target)) {
@@ -201,16 +200,16 @@ const handleNodeClick = (event, nodeProvider, editableNode, onNodeSelected) => {
201
200
  onNodeSelected(null);
202
201
  return;
203
202
  }
204
- const selectedNode = selectNode(event, editableNode);
203
+ const selectedNode = selectNode(event, nodeProvider, text);
205
204
  onNodeSelected(selectedNode);
206
205
  };
207
206
 
208
- const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, getEditableNode) => {
207
+ const setupEventListener$1 = (nodeProvider, onNodeSelected, onEscapePressed, text) => {
209
208
  const messageHandler = (event) => {
210
209
  processPostMessage(event, onNodeSelected);
211
210
  };
212
211
  const documentClickHandler = (event) => {
213
- handleNodeClick(event, nodeProvider, getEditableNode(), onNodeSelected);
212
+ handleNodeClick(event, nodeProvider, text, onNodeSelected);
214
213
  };
215
214
  const documentKeydownHandler = (event) => {
216
215
  if (event.key === "Escape") {
@@ -237,7 +236,10 @@ const HANDLE_SIZE = 6;
237
236
  const getComponentColor$2 = () => {
238
237
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
239
238
  };
240
- const createCornerHandle = (group, x, y, className, isInstance = false) => {
239
+ const getTextEditColor$2 = () => {
240
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
241
+ };
242
+ const createCornerHandle = (group, x, y, className, isInstance = false, isTextEdit = false) => {
241
243
  const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
242
244
  // Position relative to group (offset by half handle size to center on corner)
243
245
  handle.setAttribute("x", (x - HANDLE_SIZE / 2).toString());
@@ -249,15 +251,18 @@ const createCornerHandle = (group, x, y, className, isInstance = false) => {
249
251
  if (isInstance) {
250
252
  handle.setAttribute("stroke", getComponentColor$2());
251
253
  }
254
+ else if (isTextEdit) {
255
+ handle.setAttribute("stroke", getTextEditColor$2());
256
+ }
252
257
  group.appendChild(handle);
253
258
  return handle;
254
259
  };
255
- const createCornerHandles = (group, width, height, isInstance = false) => {
260
+ const createCornerHandles = (group, width, height, isInstance = false, isTextEdit = false) => {
256
261
  // Create corner handles using relative coordinates (group handles positioning)
257
- createCornerHandle(group, 0, 0, "handle-top-left", isInstance);
258
- createCornerHandle(group, width, 0, "handle-top-right", isInstance);
259
- createCornerHandle(group, width, height, "handle-bottom-right", isInstance);
260
- createCornerHandle(group, 0, height, "handle-bottom-left", isInstance);
262
+ createCornerHandle(group, 0, 0, "handle-top-left", isInstance, isTextEdit);
263
+ createCornerHandle(group, width, 0, "handle-top-right", isInstance, isTextEdit);
264
+ createCornerHandle(group, width, height, "handle-bottom-right", isInstance, isTextEdit);
265
+ createCornerHandle(group, 0, height, "handle-bottom-left", isInstance, isTextEdit);
261
266
  };
262
267
 
263
268
  function getScreenBounds(element) {
@@ -273,23 +278,31 @@ function getScreenBounds(element) {
273
278
  const getComponentColor$1 = () => {
274
279
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
275
280
  };
276
- const createHighlightFrame = (node, isInstance = false) => {
281
+ const getTextEditColor$1 = () => {
282
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
283
+ };
284
+ const createHighlightFrame = (node, isInstance = false, isTextEdit = false) => {
277
285
  const { top, left, width, height } = getScreenBounds(node);
286
+ // Ensure minimum width of 2px
287
+ const minWidth = Math.max(width, 3);
278
288
  // Create fixed SVG overlay
279
289
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
280
290
  svg.classList.add("highlight-frame-overlay");
281
291
  if (isInstance) {
282
292
  svg.classList.add("is-instance");
283
293
  }
294
+ if (isTextEdit) {
295
+ svg.classList.add("is-text-edit");
296
+ }
284
297
  svg.setAttribute("data-node-id", node.getAttribute("data-node-id") || "");
285
298
  // Set fixed positioning
286
- svg.style.position = "fixed";
299
+ svg.style.position = "absolute";
287
300
  svg.style.top = "0";
288
301
  svg.style.left = "0";
289
302
  svg.style.width = "100vw";
290
303
  svg.style.height = "100vh";
291
304
  svg.style.pointerEvents = "none";
292
- svg.style.zIndex = "5000";
305
+ svg.style.zIndex = "500";
293
306
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
294
307
  const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
295
308
  svg.setAttribute("width", viewportWidth.toString());
@@ -300,47 +313,85 @@ const createHighlightFrame = (node, isInstance = false) => {
300
313
  const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
301
314
  rect.setAttribute("x", "0");
302
315
  rect.setAttribute("y", "0");
303
- rect.setAttribute("width", width.toString());
316
+ rect.setAttribute("width", minWidth.toString());
304
317
  rect.setAttribute("height", height.toString());
305
318
  rect.setAttribute("vector-effect", "non-scaling-stroke");
306
319
  rect.classList.add("highlight-frame-rect");
307
- // Apply instance color if it's an instance
320
+ // Apply instance color if it's an instance, otherwise text edit color if in text edit mode
308
321
  if (isInstance) {
309
322
  rect.setAttribute("stroke", getComponentColor$1());
310
323
  }
324
+ else if (isTextEdit) {
325
+ rect.setAttribute("stroke", getTextEditColor$1());
326
+ }
311
327
  group.appendChild(rect);
312
- createCornerHandles(group, width, height, isInstance);
328
+ createCornerHandles(group, minWidth, height, isInstance, isTextEdit);
313
329
  svg.appendChild(group);
314
- document.body.appendChild(svg);
330
+ const canvasContainer = getCanvasContainer();
331
+ if (canvasContainer) {
332
+ canvasContainer.appendChild(svg);
333
+ }
334
+ else {
335
+ document.body.appendChild(svg);
336
+ }
315
337
  return svg;
316
338
  };
317
339
 
340
+ const TAG_NAME_MAP = {
341
+ div: "Container",
342
+ h1: "Heading 1",
343
+ h2: "Heading 2",
344
+ h3: "Heading 3",
345
+ h4: "Heading 4",
346
+ h5: "Heading 5",
347
+ h6: "Heading 6",
348
+ p: "Text",
349
+ li: "List Item",
350
+ ul: "Unordered List",
351
+ ol: "Ordered List",
352
+ img: "Image",
353
+ a: "Link",
354
+ };
355
+ const capitalize = (str) => {
356
+ if (!str)
357
+ return str;
358
+ return str.charAt(0).toUpperCase() + str.slice(1);
359
+ };
318
360
  const createTagLabel = (node, nodeTools) => {
319
361
  const tagLabel = document.createElement("div");
320
362
  tagLabel.className = "tag-label";
321
- tagLabel.textContent = node.tagName.toLowerCase();
363
+ const instanceName = node.getAttribute("data-instance-name");
364
+ const tagName = node.tagName.toLowerCase();
365
+ const labelText = instanceName || TAG_NAME_MAP[tagName] || tagName;
366
+ tagLabel.textContent = capitalize(labelText);
322
367
  nodeTools.appendChild(tagLabel);
323
368
  };
324
369
 
325
- const createToolsContainer = (node, highlightFrame, isInstance = false) => {
370
+ const createToolsContainer = (node, highlightFrame, isInstance = false, isTextEdit = false) => {
326
371
  const nodeTools = document.createElement("div");
327
372
  nodeTools.className = "node-tools";
328
373
  if (isInstance) {
329
374
  nodeTools.classList.add("is-instance");
330
375
  }
376
+ if (isTextEdit) {
377
+ nodeTools.classList.add("is-text-edit");
378
+ }
331
379
  highlightFrame.appendChild(nodeTools);
332
380
  createTagLabel(node, nodeTools);
333
381
  };
334
382
 
335
383
  function getHighlightFrameElement() {
336
- return document.body.querySelector(".highlight-frame-overlay");
384
+ const canvasContainer = getCanvasContainer();
385
+ const container = canvasContainer || document.body;
386
+ return container.querySelector(".highlight-frame-overlay");
337
387
  }
338
388
 
339
389
  const highlightNode = (node) => {
340
390
  if (!node)
341
391
  return;
342
392
  const existingHighlightFrame = getHighlightFrameElement();
343
- const existingToolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
393
+ const canvasContainer = getCanvasContainer();
394
+ const existingToolsWrapper = canvasContainer?.querySelector(".highlight-frame-tools-wrapper") || document.body.querySelector(".highlight-frame-tools-wrapper");
344
395
  if (existingHighlightFrame) {
345
396
  existingHighlightFrame.remove();
346
397
  }
@@ -348,38 +399,50 @@ const highlightNode = (node) => {
348
399
  existingToolsWrapper.remove();
349
400
  }
350
401
  const isInstance = isComponentInstance(node);
351
- const highlightFrame = createHighlightFrame(node, isInstance);
402
+ const isTextEdit = node.contentEditable === "true";
403
+ const highlightFrame = createHighlightFrame(node, isInstance, isTextEdit);
352
404
  if (node.contentEditable === "true") {
353
405
  highlightFrame.classList.add("is-editable");
354
406
  }
355
- // Create tools wrapper with tag label - centered using translateX(-50%)
407
+ // Batch DOM reads
356
408
  const { left, top, height } = getScreenBounds(node);
357
409
  const bottomY = top + height;
410
+ // Create tools wrapper using CSS transform (GPU-accelerated)
358
411
  const toolsWrapper = document.createElement("div");
359
412
  toolsWrapper.classList.add("highlight-frame-tools-wrapper");
360
413
  if (isInstance) {
361
414
  toolsWrapper.classList.add("is-instance");
362
415
  }
363
- toolsWrapper.style.position = "fixed";
364
- toolsWrapper.style.left = `${left}px`;
365
- toolsWrapper.style.top = `${bottomY}px`;
366
- toolsWrapper.style.transform = "translateX(-50%)";
367
- toolsWrapper.style.transformOrigin = "center";
416
+ if (isTextEdit) {
417
+ toolsWrapper.classList.add("is-text-edit");
418
+ }
419
+ toolsWrapper.style.position = "absolute";
420
+ toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
421
+ toolsWrapper.style.transformOrigin = "left center";
368
422
  toolsWrapper.style.pointerEvents = "none";
369
- toolsWrapper.style.zIndex = "5000"; // Match --z-index-highlight (below canvas rulers)
370
- createToolsContainer(node, toolsWrapper, isInstance);
371
- document.body.appendChild(toolsWrapper);
423
+ toolsWrapper.style.zIndex = "500";
424
+ createToolsContainer(node, toolsWrapper, isInstance, isTextEdit);
425
+ if (canvasContainer) {
426
+ canvasContainer.appendChild(toolsWrapper);
427
+ }
428
+ else {
429
+ document.body.appendChild(toolsWrapper);
430
+ }
372
431
  };
373
432
 
374
433
  const getComponentColor = () => {
375
434
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
376
435
  };
436
+ const getTextEditColor = () => {
437
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
438
+ };
377
439
  const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
378
440
  // Batch all DOM reads first (single layout pass)
379
441
  const frame = getHighlightFrameElement();
380
442
  if (!frame)
381
443
  return;
382
444
  const isInstance = isComponentInstance(node);
445
+ const isTextEdit = node.contentEditable === "true";
383
446
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
384
447
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
385
448
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
@@ -393,6 +456,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
393
456
  else {
394
457
  frame.classList.remove("is-instance");
395
458
  }
459
+ // Update text edit class
460
+ if (isTextEdit) {
461
+ frame.classList.add("is-text-edit");
462
+ }
463
+ else {
464
+ frame.classList.remove("is-text-edit");
465
+ }
396
466
  const group = frame.querySelector(".highlight-frame-group");
397
467
  if (!group)
398
468
  return;
@@ -403,15 +473,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
403
473
  if (isInstance) {
404
474
  rect.setAttribute("stroke", getComponentColor());
405
475
  }
476
+ else if (isTextEdit) {
477
+ rect.setAttribute("stroke", getTextEditColor());
478
+ }
406
479
  else {
407
480
  rect.removeAttribute("stroke"); // Use CSS default
408
481
  }
409
- const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
482
+ const canvasContainer = getCanvasContainer();
483
+ const container = canvasContainer || document.body;
484
+ const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
410
485
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
411
486
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
412
487
  const bounds = getScreenBounds(node);
413
488
  // Calculate all values before any DOM writes
414
489
  const { top, left, width, height } = bounds;
490
+ // Ensure minimum width of 2px
491
+ const minWidth = Math.max(width, 3);
415
492
  const bottomY = top + height;
416
493
  // Update instance classes on tools wrapper and node tools
417
494
  if (toolsWrapper) {
@@ -421,6 +498,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
421
498
  else {
422
499
  toolsWrapper.classList.remove("is-instance");
423
500
  }
501
+ // Update text edit class
502
+ if (isTextEdit) {
503
+ toolsWrapper.classList.add("is-text-edit");
504
+ }
505
+ else {
506
+ toolsWrapper.classList.remove("is-text-edit");
507
+ }
424
508
  }
425
509
  if (nodeTools) {
426
510
  if (isInstance) {
@@ -429,12 +513,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
429
513
  else {
430
514
  nodeTools.classList.remove("is-instance");
431
515
  }
516
+ // Update text edit class
517
+ if (isTextEdit) {
518
+ nodeTools.classList.add("is-text-edit");
519
+ }
520
+ else {
521
+ nodeTools.classList.remove("is-text-edit");
522
+ }
432
523
  }
433
524
  // Batch all DOM writes (single paint pass)
434
525
  // Update group transform to move entire group (rect + handles) at once
435
526
  group.setAttribute("transform", `translate(${left}, ${top})`);
436
527
  // Update rect dimensions (position is handled by group transform)
437
- rect.setAttribute("width", width.toString());
528
+ rect.setAttribute("width", minWidth.toString());
438
529
  rect.setAttribute("height", height.toString());
439
530
  // Update corner handles positions (relative to group, so only width/height matter)
440
531
  const topLeft = group.querySelector(".handle-top-left");
@@ -449,6 +540,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
449
540
  if (isInstance) {
450
541
  handle.setAttribute("stroke", getComponentColor());
451
542
  }
543
+ else if (isTextEdit) {
544
+ handle.setAttribute("stroke", getTextEditColor());
545
+ }
452
546
  else {
453
547
  handle.removeAttribute("stroke"); // Use CSS default
454
548
  }
@@ -459,22 +553,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
459
553
  topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
460
554
  }
461
555
  if (topRight) {
462
- topRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
556
+ topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
463
557
  topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
464
558
  }
465
559
  if (bottomRight) {
466
- bottomRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
560
+ bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
467
561
  bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
468
562
  }
469
563
  if (bottomLeft) {
470
564
  bottomLeft.setAttribute("x", (-HANDLE_SIZE / 2).toString());
471
565
  bottomLeft.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
472
566
  }
473
- // Update tools wrapper position (use calculated bounds, not rect attributes)
567
+ // Update tools wrapper position using CSS transform (GPU-accelerated)
474
568
  if (toolsWrapper) {
475
- toolsWrapper.style.left = `${left}px`;
476
- toolsWrapper.style.top = `${bottomY}px`;
569
+ toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
477
570
  }
571
+ // Update tool opacity
478
572
  if (zoom <= 10) {
479
573
  nodeProvider.style.setProperty("--tool-opacity", `1`);
480
574
  }
@@ -487,10 +581,14 @@ const updateHighlightFrameVisibility = (node) => {
487
581
  const frame = getHighlightFrameElement();
488
582
  if (!frame)
489
583
  return;
584
+ // Batch DOM reads
490
585
  const hasHiddenClass = node.classList.contains("hidden") || node.classList.contains("select-none");
491
586
  const displayValue = hasHiddenClass ? "none" : "";
587
+ // Batch DOM writes
492
588
  frame.style.display = displayValue;
493
- const toolsWrapper = document.body.querySelector(".highlight-frame-tools-wrapper");
589
+ const canvasContainer = getCanvasContainer();
590
+ const container = canvasContainer || document.body;
591
+ const toolsWrapper = container.querySelector(".highlight-frame-tools-wrapper");
494
592
  if (toolsWrapper) {
495
593
  toolsWrapper.style.display = displayValue;
496
594
  }
@@ -547,11 +645,64 @@ const connectMutationObserver = (element, handler) => {
547
645
  return mutationObserver;
548
646
  };
549
647
 
648
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
649
+ function withRAFThrottle(func) {
650
+ let rafId = null;
651
+ let lastArgs = null;
652
+ const throttled = (...args) => {
653
+ lastArgs = args;
654
+ if (rafId === null) {
655
+ rafId = requestAnimationFrame(() => {
656
+ if (lastArgs) {
657
+ func(...lastArgs);
658
+ }
659
+ rafId = null;
660
+ lastArgs = null;
661
+ });
662
+ }
663
+ };
664
+ throttled.cleanup = () => {
665
+ if (rafId !== null) {
666
+ cancelAnimationFrame(rafId);
667
+ rafId = null;
668
+ lastArgs = null;
669
+ }
670
+ };
671
+ return throttled;
672
+ }
673
+
674
+ const handleTextChange = (node, mutations) => {
675
+ // Check if any mutation is a text content change
676
+ const hasTextChange = mutations.some((mutation) => {
677
+ return (mutation.type === "characterData" ||
678
+ (mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)));
679
+ });
680
+ if (!hasTextChange) {
681
+ return;
682
+ }
683
+ // Get the text content of the node
684
+ const textContent = node.textContent ?? "";
685
+ // Get the node ID
686
+ const nodeId = node.getAttribute("data-node-id");
687
+ // Send postMessage with the text change
688
+ sendPostMessage("textContentChanged", {
689
+ nodeId,
690
+ textContent,
691
+ });
692
+ };
693
+
550
694
  const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
551
- const mutationObserver = connectMutationObserver(node, () => {
695
+ const throttledHandleTextChange = withRAFThrottle((mutations) => {
696
+ handleTextChange(node, mutations);
697
+ });
698
+ const mutationObserver = connectMutationObserver(node, (mutations) => {
699
+ throttledHandleTextChange(mutations);
552
700
  refreshHighlightFrame(node, nodeProvider, canvasName);
553
701
  });
554
- return () => mutationObserver.disconnect();
702
+ return () => {
703
+ mutationObserver.disconnect();
704
+ throttledHandleTextChange.cleanup();
705
+ };
555
706
  };
556
707
 
557
708
  const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
@@ -636,11 +787,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
636
787
  let parentMutationObserver = null;
637
788
  let selectedNode = null;
638
789
  const text = nodeText(canvasName);
639
- // Combined throttled function for refresh + visibility update
640
- const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
641
- refreshHighlightFrame(node, nodeProvider, canvasName);
642
- updateHighlightFrameVisibility(node);
643
- });
644
790
  const handleEscape = () => {
645
791
  if (text.isEditing()) {
646
792
  text.blurEditMode();
@@ -669,7 +815,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
669
815
  mutationObserver?.disconnect();
670
816
  parentMutationObserver?.disconnect();
671
817
  if (node && nodeProvider) {
672
- text.enableEditMode(node, nodeProvider);
673
818
  // Check if node is still in DOM and handle cleanup if removed
674
819
  const checkNodeExists = () => {
675
820
  if (!document.contains(node)) {
@@ -686,9 +831,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
686
831
  mutationObserver = new MutationObserver(() => {
687
832
  checkNodeExists();
688
833
  if (!document.contains(node))
689
- return; // Exit early if node was removed
690
- // throttledRefreshAndVisibility(node, nodeProvider);
691
- console.log("mutationObserver", node);
834
+ return;
692
835
  refreshHighlightFrame(node, nodeProvider, canvasName);
693
836
  updateHighlightFrameVisibility(node);
694
837
  });
@@ -724,8 +867,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
724
867
  checkNodeExists();
725
868
  if (!document.contains(node))
726
869
  return; // Exit early if node was removed
727
- // throttledRefreshAndVisibility(node, nodeProvider);
728
- console.log("resizeObserver", node);
729
870
  refreshHighlightFrame(node, nodeProvider, canvasName);
730
871
  updateHighlightFrameVisibility(node);
731
872
  });
@@ -735,17 +876,17 @@ const createNodeTools = (element, canvasName = "canvas") => {
735
876
  highlightNode(node) ?? null;
736
877
  if (node && nodeProvider) {
737
878
  updateHighlightFrameVisibility(node);
879
+ updateHighlightFrameVisibility(node);
738
880
  }
739
881
  };
740
882
  // Setup event listener
741
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text.getEditableNode);
883
+ const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
742
884
  const cleanup = () => {
743
885
  removeListeners();
744
886
  resizeObserver?.disconnect();
745
887
  mutationObserver?.disconnect();
746
888
  parentMutationObserver?.disconnect();
747
889
  text.blurEditMode();
748
- throttledRefreshAndVisibility.cleanup();
749
890
  // Clear highlight frame and reset selected node
750
891
  clearHighlightFrame();
751
892
  selectedNode = null;
@@ -774,10 +915,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
774
915
  return nodeTools;
775
916
  };
776
917
 
777
- const getCanvasContainer = () => {
778
- return document.querySelector(".canvas-container");
779
- };
780
-
781
918
  const DEFAULT_WIDTH = 400;
782
919
  const RESIZE_CONFIG = {
783
920
  minWidth: 320,