@node-edit-utils/core 2.2.7 → 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 +217 -82
  11. package/dist/node-edit-utils.esm.js +217 -82
  12. package/dist/node-edit-utils.umd.js +217 -82
  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 +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 +23 -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.7
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,7 +399,8 @@ 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
  }
@@ -361,24 +413,36 @@ const highlightNode = (node) => {
361
413
  if (isInstance) {
362
414
  toolsWrapper.classList.add("is-instance");
363
415
  }
364
- toolsWrapper.style.position = "fixed";
416
+ if (isTextEdit) {
417
+ toolsWrapper.classList.add("is-text-edit");
418
+ }
419
+ toolsWrapper.style.position = "absolute";
365
420
  toolsWrapper.style.transform = `translate(${left}px, ${bottomY}px)`;
366
421
  toolsWrapper.style.transformOrigin = "left center";
367
422
  toolsWrapper.style.pointerEvents = "none";
368
- toolsWrapper.style.zIndex = "5000"; // Match --z-index-highlight (below canvas rulers)
369
- createToolsContainer(node, toolsWrapper, isInstance);
370
- 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
+ }
371
431
  };
372
432
 
373
433
  const getComponentColor = () => {
374
434
  return getComputedStyle(document.documentElement).getPropertyValue("--component-color").trim() || "oklch(65.6% 0.241 354.308)";
375
435
  };
436
+ const getTextEditColor = () => {
437
+ return getComputedStyle(document.documentElement).getPropertyValue("--text-edit-color").trim() || "oklch(62.3% 0.214 259.815)";
438
+ };
376
439
  const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
377
440
  // Batch all DOM reads first (single layout pass)
378
441
  const frame = getHighlightFrameElement();
379
442
  if (!frame)
380
443
  return;
381
444
  const isInstance = isComponentInstance(node);
445
+ const isTextEdit = node.contentEditable === "true";
382
446
  // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
383
447
  // Use clientWidth/Height to match getBoundingClientRect() coordinate system (excludes scrollbars)
384
448
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
@@ -392,6 +456,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
392
456
  else {
393
457
  frame.classList.remove("is-instance");
394
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
+ }
395
466
  const group = frame.querySelector(".highlight-frame-group");
396
467
  if (!group)
397
468
  return;
@@ -402,15 +473,22 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
402
473
  if (isInstance) {
403
474
  rect.setAttribute("stroke", getComponentColor());
404
475
  }
476
+ else if (isTextEdit) {
477
+ rect.setAttribute("stroke", getTextEditColor());
478
+ }
405
479
  else {
406
480
  rect.removeAttribute("stroke"); // Use CSS default
407
481
  }
408
- 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");
409
485
  const nodeTools = toolsWrapper?.querySelector(".node-tools");
410
486
  const zoom = getCanvasWindowValue(["zoom", "current"], canvasName) ?? 1;
411
487
  const bounds = getScreenBounds(node);
412
488
  // Calculate all values before any DOM writes
413
489
  const { top, left, width, height } = bounds;
490
+ // Ensure minimum width of 2px
491
+ const minWidth = Math.max(width, 3);
414
492
  const bottomY = top + height;
415
493
  // Update instance classes on tools wrapper and node tools
416
494
  if (toolsWrapper) {
@@ -420,6 +498,13 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
420
498
  else {
421
499
  toolsWrapper.classList.remove("is-instance");
422
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
+ }
423
508
  }
424
509
  if (nodeTools) {
425
510
  if (isInstance) {
@@ -428,12 +513,19 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
428
513
  else {
429
514
  nodeTools.classList.remove("is-instance");
430
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
+ }
431
523
  }
432
524
  // Batch all DOM writes (single paint pass)
433
525
  // Update group transform to move entire group (rect + handles) at once
434
526
  group.setAttribute("transform", `translate(${left}, ${top})`);
435
527
  // Update rect dimensions (position is handled by group transform)
436
- rect.setAttribute("width", width.toString());
528
+ rect.setAttribute("width", minWidth.toString());
437
529
  rect.setAttribute("height", height.toString());
438
530
  // Update corner handles positions (relative to group, so only width/height matter)
439
531
  const topLeft = group.querySelector(".handle-top-left");
@@ -448,6 +540,9 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
448
540
  if (isInstance) {
449
541
  handle.setAttribute("stroke", getComponentColor());
450
542
  }
543
+ else if (isTextEdit) {
544
+ handle.setAttribute("stroke", getTextEditColor());
545
+ }
451
546
  else {
452
547
  handle.removeAttribute("stroke"); // Use CSS default
453
548
  }
@@ -458,11 +553,11 @@ const refreshHighlightFrame = (node, nodeProvider, canvasName = "canvas") => {
458
553
  topLeft.setAttribute("y", (-HANDLE_SIZE / 2).toString());
459
554
  }
460
555
  if (topRight) {
461
- topRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
556
+ topRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
462
557
  topRight.setAttribute("y", (-HANDLE_SIZE / 2).toString());
463
558
  }
464
559
  if (bottomRight) {
465
- bottomRight.setAttribute("x", (width - HANDLE_SIZE / 2).toString());
560
+ bottomRight.setAttribute("x", (minWidth - HANDLE_SIZE / 2).toString());
466
561
  bottomRight.setAttribute("y", (height - HANDLE_SIZE / 2).toString());
467
562
  }
468
563
  if (bottomLeft) {
@@ -491,7 +586,9 @@ const updateHighlightFrameVisibility = (node) => {
491
586
  const displayValue = hasHiddenClass ? "none" : "";
492
587
  // Batch DOM writes
493
588
  frame.style.display = displayValue;
494
- 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");
495
592
  if (toolsWrapper) {
496
593
  toolsWrapper.style.display = displayValue;
497
594
  }
@@ -548,11 +645,64 @@ const connectMutationObserver = (element, handler) => {
548
645
  return mutationObserver;
549
646
  };
550
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
+
551
694
  const setupMutationObserver = (node, nodeProvider, canvasName = "canvas") => {
552
- const mutationObserver = connectMutationObserver(node, () => {
695
+ const throttledHandleTextChange = withRAFThrottle((mutations) => {
696
+ handleTextChange(node, mutations);
697
+ });
698
+ const mutationObserver = connectMutationObserver(node, (mutations) => {
699
+ throttledHandleTextChange(mutations);
553
700
  refreshHighlightFrame(node, nodeProvider, canvasName);
554
701
  });
555
- return () => mutationObserver.disconnect();
702
+ return () => {
703
+ mutationObserver.disconnect();
704
+ throttledHandleTextChange.cleanup();
705
+ };
556
706
  };
557
707
 
558
708
  const setupNodeListeners = (node, nodeProvider, blur, canvasName = "canvas") => {
@@ -637,11 +787,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
637
787
  let parentMutationObserver = null;
638
788
  let selectedNode = null;
639
789
  const text = nodeText(canvasName);
640
- // Combined throttled function for refresh + visibility update
641
- const throttledRefreshAndVisibility = withRAFThrottle((node, nodeProvider) => {
642
- refreshHighlightFrame(node, nodeProvider, canvasName);
643
- updateHighlightFrameVisibility(node);
644
- });
645
790
  const handleEscape = () => {
646
791
  if (text.isEditing()) {
647
792
  text.blurEditMode();
@@ -670,7 +815,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
670
815
  mutationObserver?.disconnect();
671
816
  parentMutationObserver?.disconnect();
672
817
  if (node && nodeProvider) {
673
- text.enableEditMode(node, nodeProvider);
674
818
  // Check if node is still in DOM and handle cleanup if removed
675
819
  const checkNodeExists = () => {
676
820
  if (!document.contains(node)) {
@@ -687,9 +831,7 @@ const createNodeTools = (element, canvasName = "canvas") => {
687
831
  mutationObserver = new MutationObserver(() => {
688
832
  checkNodeExists();
689
833
  if (!document.contains(node))
690
- return; // Exit early if node was removed
691
- // throttledRefreshAndVisibility(node, nodeProvider);
692
- console.log("mutationObserver", node);
834
+ return;
693
835
  refreshHighlightFrame(node, nodeProvider, canvasName);
694
836
  updateHighlightFrameVisibility(node);
695
837
  });
@@ -725,8 +867,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
725
867
  checkNodeExists();
726
868
  if (!document.contains(node))
727
869
  return; // Exit early if node was removed
728
- // throttledRefreshAndVisibility(node, nodeProvider);
729
- console.log("resizeObserver", node);
730
870
  refreshHighlightFrame(node, nodeProvider, canvasName);
731
871
  updateHighlightFrameVisibility(node);
732
872
  });
@@ -740,14 +880,13 @@ const createNodeTools = (element, canvasName = "canvas") => {
740
880
  }
741
881
  };
742
882
  // Setup event listener
743
- const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text.getEditableNode);
883
+ const removeListeners = setupEventListener$1(nodeProvider, selectNode, handleEscape, text);
744
884
  const cleanup = () => {
745
885
  removeListeners();
746
886
  resizeObserver?.disconnect();
747
887
  mutationObserver?.disconnect();
748
888
  parentMutationObserver?.disconnect();
749
889
  text.blurEditMode();
750
- throttledRefreshAndVisibility.cleanup();
751
890
  // Clear highlight frame and reset selected node
752
891
  clearHighlightFrame();
753
892
  selectedNode = null;
@@ -776,10 +915,6 @@ const createNodeTools = (element, canvasName = "canvas") => {
776
915
  return nodeTools;
777
916
  };
778
917
 
779
- const getCanvasContainer = () => {
780
- return document.querySelector(".canvas-container");
781
- };
782
-
783
918
  const DEFAULT_WIDTH = 400;
784
919
  const RESIZE_CONFIG = {
785
920
  minWidth: 320,