f1ow 0.1.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/f1ow.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
- import React, { useCallback, useRef, useMemo, createContext, useContext, useState, useEffect, forwardRef, createElement, useImperativeHandle } from "react";
5
+ import React, { useCallback, useRef, useMemo, createContext, useContext, useState, useEffect, forwardRef, createElement, useImperativeHandle, useLayoutEffect } from "react";
6
6
  import { Shape, Rect, Ellipse, Line, Group as Group$1, Path, Text, Image as Image$2, Transformer, Circle as Circle$1, Stage, Layer } from "react-konva";
7
7
  import Konva from "konva";
8
8
  import { create } from "zustand";
@@ -873,6 +873,58 @@ function simplifyElbowPath(points) {
873
873
  result.push(points[points.length - 2], points[points.length - 1]);
874
874
  return result;
875
875
  }
876
+ const CURVE_RATIO = 0.2;
877
+ function computeCurveControlPoint(start, end, ratio = CURVE_RATIO) {
878
+ const mx = (start.x + end.x) / 2;
879
+ const my = (start.y + end.y) / 2;
880
+ const dx = end.x - start.x;
881
+ const dy = end.y - start.y;
882
+ const len2 = Math.sqrt(dx * dx + dy * dy);
883
+ if (len2 === 0) return { x: mx, y: my };
884
+ const nx = -dy / len2;
885
+ const ny = dx / len2;
886
+ const offset = len2 * ratio;
887
+ return { x: mx + nx * offset, y: my + ny * offset };
888
+ }
889
+ function quadBezierAt(p0, cp, p2, t) {
890
+ const mt = 1 - t;
891
+ return {
892
+ x: mt * mt * p0.x + 2 * mt * t * cp.x + t * t * p2.x,
893
+ y: mt * mt * p0.y + 2 * mt * t * cp.y + t * t * p2.y
894
+ };
895
+ }
896
+ function quadBezierTangent(p0, cp, p2, t) {
897
+ const mt = 1 - t;
898
+ return {
899
+ x: 2 * mt * (cp.x - p0.x) + 2 * t * (p2.x - cp.x),
900
+ y: 2 * mt * (cp.y - p0.y) + 2 * t * (p2.y - cp.y)
901
+ };
902
+ }
903
+ function curveArrowPrev(start, cp, end, atEnd) {
904
+ if (atEnd) {
905
+ const tan = quadBezierTangent(start, cp, end, 1);
906
+ const len2 = Math.sqrt(tan.x * tan.x + tan.y * tan.y) || 1;
907
+ return { x: end.x - tan.x / len2 * 20, y: end.y - tan.y / len2 * 20 };
908
+ } else {
909
+ const tan = quadBezierTangent(start, cp, end, 0);
910
+ const len2 = Math.sqrt(tan.x * tan.x + tan.y * tan.y) || 1;
911
+ return { x: start.x + tan.x / len2 * 20, y: start.y + tan.y / len2 * 20 };
912
+ }
913
+ }
914
+ function curvatureFromDragPoint(start, end, dragWorld) {
915
+ const dx = end.x - start.x;
916
+ const dy = end.y - start.y;
917
+ const len2 = Math.sqrt(dx * dx + dy * dy);
918
+ if (len2 === 0) return CURVE_RATIO;
919
+ const nx = -dy / len2;
920
+ const ny = dx / len2;
921
+ const mx = (start.x + end.x) / 2;
922
+ const my = (start.y + end.y) / 2;
923
+ const vx = dragWorld.x - mx;
924
+ const vy = dragWorld.y - my;
925
+ const projDist = vx * nx + vy * ny;
926
+ return 2 * projDist / len2;
927
+ }
876
928
  const CONNECTABLE_TYPES = /* @__PURE__ */ new Set(["rectangle", "ellipse", "diamond", "text", "image"]);
877
929
  function isConnectable(el) {
878
930
  return CONNECTABLE_TYPES.has(el.type);
@@ -1299,6 +1351,71 @@ function syncBoundElements(connectorId, connectorType, oldBinding, newBinding, e
1299
1351
  }
1300
1352
  }
1301
1353
  }
1354
+ function computeConnectorLabelPosition(connector, textWidth, textHeight) {
1355
+ const pts = connector.points;
1356
+ const startPt = { x: pts[0], y: pts[1] };
1357
+ const endPt = { x: pts[pts.length - 2], y: pts[pts.length - 1] };
1358
+ let midX;
1359
+ let midY;
1360
+ if (connector.lineType === "curved") {
1361
+ const curvature = connector.curvature ?? CURVE_RATIO;
1362
+ const cp = computeCurveControlPoint(startPt, endPt, curvature);
1363
+ const mid = quadBezierAt(startPt, cp, endPt, 0.5);
1364
+ midX = connector.x + mid.x;
1365
+ midY = connector.y + mid.y;
1366
+ } else if (connector.lineType === "elbow" && pts.length >= 4) {
1367
+ const segCount = pts.length / 2 - 1;
1368
+ let totalLen = 0;
1369
+ for (let i = 0; i < segCount; i++) {
1370
+ const dx = pts[(i + 1) * 2] - pts[i * 2];
1371
+ const dy = pts[(i + 1) * 2 + 1] - pts[i * 2 + 1];
1372
+ totalLen += Math.sqrt(dx * dx + dy * dy);
1373
+ }
1374
+ const half = totalLen / 2;
1375
+ let walked = 0;
1376
+ midX = connector.x + (startPt.x + endPt.x) / 2;
1377
+ midY = connector.y + (startPt.y + endPt.y) / 2;
1378
+ for (let i = 0; i < segCount; i++) {
1379
+ const ax = pts[i * 2], ay = pts[i * 2 + 1];
1380
+ const bx = pts[(i + 1) * 2], by = pts[(i + 1) * 2 + 1];
1381
+ const dx = bx - ax, dy = by - ay;
1382
+ const segLen = Math.sqrt(dx * dx + dy * dy);
1383
+ if (walked + segLen >= half && segLen > 0) {
1384
+ const t = (half - walked) / segLen;
1385
+ midX = connector.x + ax + dx * t;
1386
+ midY = connector.y + ay + dy * t;
1387
+ break;
1388
+ }
1389
+ walked += segLen;
1390
+ }
1391
+ } else {
1392
+ midX = connector.x + (startPt.x + endPt.x) / 2;
1393
+ midY = connector.y + (startPt.y + endPt.y) / 2;
1394
+ }
1395
+ return {
1396
+ x: midX - textWidth / 2,
1397
+ y: midY - textHeight / 2
1398
+ };
1399
+ }
1400
+ function syncConnectorLabels(connectorIds, elMap) {
1401
+ const updates = [];
1402
+ for (const connId of connectorIds) {
1403
+ const conn = elMap.get(connId);
1404
+ if (!conn || conn.type !== "arrow" && conn.type !== "line") continue;
1405
+ if (!conn.boundElements) continue;
1406
+ const connector = conn;
1407
+ for (const be of conn.boundElements) {
1408
+ if (be.type !== "text") continue;
1409
+ const txt = elMap.get(be.id);
1410
+ if (!txt) continue;
1411
+ const textW = Math.max(10, txt.width || 60);
1412
+ const textH = txt.height || 30;
1413
+ const pos = computeConnectorLabelPosition(connector, textW, textH);
1414
+ updates.push({ id: txt.id, updates: { x: pos.x, y: pos.y } });
1415
+ }
1416
+ }
1417
+ return updates;
1418
+ }
1302
1419
  const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1303
1420
  let nanoid = (size = 21) => {
1304
1421
  let id = "";
@@ -1823,7 +1940,7 @@ function batchElementUpdates(elements, updates) {
1823
1940
  function toSet(arr) {
1824
1941
  return new Set(arr);
1825
1942
  }
1826
- const ZOOM_STEPS = [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5];
1943
+ const ZOOM_STEPS = [0.1, 0.25, 0.33, 0.5, 0.67, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
1827
1944
  const DEFAULT_ANIMATION_DURATION = 280;
1828
1945
  function zoomAtPoint({ viewport, point, targetScale }) {
1829
1946
  const clampedScale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, targetScale));
@@ -2568,58 +2685,6 @@ function getStrokeDash(style, strokeWidth) {
2568
2685
  return [];
2569
2686
  }
2570
2687
  }
2571
- const CURVE_RATIO = 0.2;
2572
- function computeCurveControlPoint(start, end, ratio = CURVE_RATIO) {
2573
- const mx = (start.x + end.x) / 2;
2574
- const my = (start.y + end.y) / 2;
2575
- const dx = end.x - start.x;
2576
- const dy = end.y - start.y;
2577
- const len2 = Math.sqrt(dx * dx + dy * dy);
2578
- if (len2 === 0) return { x: mx, y: my };
2579
- const nx = -dy / len2;
2580
- const ny = dx / len2;
2581
- const offset = len2 * ratio;
2582
- return { x: mx + nx * offset, y: my + ny * offset };
2583
- }
2584
- function quadBezierAt(p0, cp, p2, t) {
2585
- const mt = 1 - t;
2586
- return {
2587
- x: mt * mt * p0.x + 2 * mt * t * cp.x + t * t * p2.x,
2588
- y: mt * mt * p0.y + 2 * mt * t * cp.y + t * t * p2.y
2589
- };
2590
- }
2591
- function quadBezierTangent(p0, cp, p2, t) {
2592
- const mt = 1 - t;
2593
- return {
2594
- x: 2 * mt * (cp.x - p0.x) + 2 * t * (p2.x - cp.x),
2595
- y: 2 * mt * (cp.y - p0.y) + 2 * t * (p2.y - cp.y)
2596
- };
2597
- }
2598
- function curveArrowPrev(start, cp, end, atEnd) {
2599
- if (atEnd) {
2600
- const tan = quadBezierTangent(start, cp, end, 1);
2601
- const len2 = Math.sqrt(tan.x * tan.x + tan.y * tan.y) || 1;
2602
- return { x: end.x - tan.x / len2 * 20, y: end.y - tan.y / len2 * 20 };
2603
- } else {
2604
- const tan = quadBezierTangent(start, cp, end, 0);
2605
- const len2 = Math.sqrt(tan.x * tan.x + tan.y * tan.y) || 1;
2606
- return { x: start.x + tan.x / len2 * 20, y: start.y + tan.y / len2 * 20 };
2607
- }
2608
- }
2609
- function curvatureFromDragPoint(start, end, dragWorld) {
2610
- const dx = end.x - start.x;
2611
- const dy = end.y - start.y;
2612
- const len2 = Math.sqrt(dx * dx + dy * dy);
2613
- if (len2 === 0) return CURVE_RATIO;
2614
- const nx = -dy / len2;
2615
- const ny = dx / len2;
2616
- const mx = (start.x + end.x) / 2;
2617
- const my = (start.y + end.y) / 2;
2618
- const vx = dragWorld.x - mx;
2619
- const vy = dragWorld.y - my;
2620
- const projDist = vx * nx + vy * ny;
2621
- return 2 * projDist / len2;
2622
- }
2623
2688
  const selectTool = {
2624
2689
  name: "select",
2625
2690
  onMouseDown(e, pos, ctx) {
@@ -3322,7 +3387,7 @@ function createImageElement(src, naturalWidth, naturalHeight, x, y, style = { ..
3322
3387
  width: dims.width,
3323
3388
  height: dims.height,
3324
3389
  rotation: 0,
3325
- style: { ...style, fillColor: "transparent" },
3390
+ style: { ...style, fillColor: "transparent", strokeColor: "transparent", strokeWidth: 0 },
3326
3391
  isLocked: false,
3327
3392
  isVisible: true,
3328
3393
  boundElements: null,
@@ -5574,7 +5639,34 @@ const FreeDrawShape = ({ element, isSelected, isGrouped, onSelect, onChange, onD
5574
5639
  );
5575
5640
  };
5576
5641
  const FreeDrawShape$1 = React.memo(FreeDrawShape);
5577
- const LINE_HEIGHT = 1.18;
5642
+ const LABEL_PADDING_H = 8;
5643
+ const LABEL_PADDING_V = 4;
5644
+ const LABEL_CORNER = 4;
5645
+ const LABEL_LINE_HEIGHT = 1.18;
5646
+ const LABEL_MIN_WIDTH = 10;
5647
+ let _measureCanvas$1 = null;
5648
+ function getMeasureCtx() {
5649
+ if (!_measureCanvas$1) {
5650
+ _measureCanvas$1 = document.createElement("canvas");
5651
+ }
5652
+ return _measureCanvas$1.getContext("2d");
5653
+ }
5654
+ function measureLabelText(text, fontSize, fontFamily) {
5655
+ const ctx = getMeasureCtx();
5656
+ ctx.font = `${fontSize}px ${fontFamily}`;
5657
+ const metrics = ctx.measureText(text || " ");
5658
+ return {
5659
+ width: Math.ceil(metrics.width),
5660
+ height: Math.ceil(fontSize * LABEL_LINE_HEIGHT)
5661
+ };
5662
+ }
5663
+ function computePillSize(textWidth, textHeight) {
5664
+ return {
5665
+ width: Math.max(LABEL_MIN_WIDTH, textWidth) + LABEL_PADDING_H * 2,
5666
+ height: textHeight + LABEL_PADDING_V * 2
5667
+ };
5668
+ }
5669
+ const LINE_HEIGHT = LABEL_LINE_HEIGHT;
5578
5670
  const TextShape = ({
5579
5671
  element,
5580
5672
  isSelected,
@@ -5591,6 +5683,7 @@ const TextShape = ({
5591
5683
  const { id, x, y, width, height, rotation, style, text, containerId, textAlign, verticalAlign, isLocked } = element;
5592
5684
  const textRef = useRef(null);
5593
5685
  const isEditingRef = useRef(false);
5686
+ const [isEditingState, setIsEditingState] = useState(false);
5594
5687
  const autoEditDoneRef = useRef(false);
5595
5688
  const isBound = !!containerId;
5596
5689
  const isDraggable = !isBound && !isLocked && !isGrouped;
@@ -5599,36 +5692,12 @@ const TextShape = ({
5599
5692
  return allElements.find((el) => el.id === containerId) ?? null;
5600
5693
  }, [containerId, allElements]);
5601
5694
  const boundPos = useMemo(() => {
5602
- var _a, _b;
5603
5695
  if (!container) return { x, y };
5604
- if (container.type === "arrow" || container.type === "line") {
5605
- const conn = container;
5606
- const pts = conn.points;
5607
- const startPt = { x: pts[0], y: pts[1] };
5608
- const endPt = { x: pts[pts.length - 2], y: pts[pts.length - 1] };
5609
- let midX, midY;
5610
- if (conn.lineType === "curved") {
5611
- const cp = computeCurveControlPoint(startPt, endPt, conn.curvature ?? CURVE_RATIO);
5612
- const mid = quadBezierAt(startPt, cp, endPt, 0.5);
5613
- midX = conn.x + mid.x;
5614
- midY = conn.y + mid.y;
5615
- } else {
5616
- midX = conn.x + (startPt.x + endPt.x) / 2;
5617
- midY = conn.y + (startPt.y + endPt.y) / 2;
5618
- }
5619
- const textWidth2 = Math.max(80, width || 80);
5620
- const textHeight = ((_a = textRef.current) == null ? void 0 : _a.height()) ?? height;
5621
- return {
5622
- x: midX - textWidth2 / 2,
5623
- y: midY - textHeight / 2,
5624
- width: textWidth2
5625
- };
5626
- }
5627
5696
  const PADDING2 = 4;
5628
5697
  const cw = container.width - PADDING2 * 2;
5629
5698
  const textWidth = Math.max(20, cw);
5630
5699
  const bx = container.x + PADDING2;
5631
- const textActualHeight = ((_b = textRef.current) == null ? void 0 : _b.height()) ?? height;
5700
+ const textActualHeight = height;
5632
5701
  let by;
5633
5702
  if (verticalAlign === "top") {
5634
5703
  by = container.y + PADDING2;
@@ -5638,7 +5707,7 @@ const TextShape = ({
5638
5707
  by = container.y + (container.height - textActualHeight) / 2;
5639
5708
  }
5640
5709
  return { x: bx, y: by, width: textWidth };
5641
- }, [container, x, y, height, verticalAlign]);
5710
+ }, [container, x, y, width, height, verticalAlign]);
5642
5711
  const syncSize = useCallback(() => {
5643
5712
  const node = textRef.current;
5644
5713
  if (!node || isEditingRef.current) return;
@@ -5660,8 +5729,14 @@ const TextShape = ({
5660
5729
  onChange(id, updates);
5661
5730
  }
5662
5731
  }, [id, height, width, isBound, onChange]);
5732
+ const syncSizeInitRef = useRef(true);
5663
5733
  useEffect(() => {
5664
- requestAnimationFrame(syncSize);
5734
+ if (syncSizeInitRef.current) {
5735
+ syncSizeInitRef.current = false;
5736
+ if (text && height > 0) return;
5737
+ }
5738
+ const id2 = requestAnimationFrame(syncSize);
5739
+ return () => cancelAnimationFrame(id2);
5665
5740
  }, [text, style.fontSize, style.fontFamily, syncSize]);
5666
5741
  const openEditor = useCallback(() => {
5667
5742
  const textNode = textRef.current;
@@ -5669,6 +5744,7 @@ const TextShape = ({
5669
5744
  const stage = textNode.getStage();
5670
5745
  if (!stage) return;
5671
5746
  isEditingRef.current = true;
5747
+ setIsEditingState(true);
5672
5748
  onEditStart == null ? void 0 : onEditStart(id);
5673
5749
  const stageContainer = stage.container();
5674
5750
  const absTransform = textNode.getAbsoluteTransform().copy();
@@ -5691,7 +5767,7 @@ const TextShape = ({
5691
5767
  containerScreenH = container.height * stageScaleX;
5692
5768
  }
5693
5769
  const nodeWidth = textNode.width();
5694
- const screenWidth = isBound ? containerScreenW - PADDING_SCREEN * 2 : Math.max(nodeWidth, 100) * stageScaleX;
5770
+ const screenWidth = isBound ? containerScreenW - PADDING_SCREEN * 2 : Math.max(nodeWidth, 60) * stageScaleX;
5695
5771
  const screenLeft = isBound ? containerScreenLeft + PADDING_SCREEN : absPos.x;
5696
5772
  const originalText = text;
5697
5773
  const textarea = document.createElement("textarea");
@@ -5765,7 +5841,8 @@ const TextShape = ({
5765
5841
  }
5766
5842
  if (!isBound) {
5767
5843
  textarea.style.width = "auto";
5768
- textarea.style.width = `${Math.max(textarea.scrollWidth, 100 * stageScaleX)}px`;
5844
+ const minW = 100 * stageScaleX;
5845
+ textarea.style.width = `${Math.max(textarea.scrollWidth, minW)}px`;
5769
5846
  }
5770
5847
  };
5771
5848
  textarea.addEventListener("input", autoGrow);
@@ -5778,6 +5855,7 @@ const TextShape = ({
5778
5855
  const finishEdit = () => {
5779
5856
  if (!isEditingRef.current) return;
5780
5857
  isEditingRef.current = false;
5858
+ setIsEditingState(false);
5781
5859
  const newText = cancelled ? originalText : textarea.value;
5782
5860
  const isEmpty = newText.trim() === "";
5783
5861
  textarea.removeEventListener("input", autoGrow);
@@ -5793,9 +5871,7 @@ const TextShape = ({
5793
5871
  }
5794
5872
  onEditEnd == null ? void 0 : onEditEnd(id, isEmpty);
5795
5873
  };
5796
- const handleBlur = () => {
5797
- finishEdit();
5798
- };
5874
+ const handleBlur = () => finishEdit();
5799
5875
  const handleKeyDown = (e) => {
5800
5876
  e.stopPropagation();
5801
5877
  if (e.key === "Escape") {
@@ -5846,7 +5922,7 @@ const TextShape = ({
5846
5922
  wrap: isBound ? "word" : "none",
5847
5923
  rotation: isBound ? (container == null ? void 0 : container.rotation) ?? rotation : rotation,
5848
5924
  transformsEnabled: (isBound ? (container == null ? void 0 : container.rotation) ?? rotation : rotation) ? "all" : "position",
5849
- visible: !(autoEdit && !isEditingRef.current && !text),
5925
+ visible: !isEditingState && !(autoEdit && !text),
5850
5926
  opacity: text ? style.opacity : isBound ? 0 : 0.4,
5851
5927
  draggable: isDraggable,
5852
5928
  listening: !isBound,
@@ -5896,6 +5972,263 @@ const TextShape = ({
5896
5972
  );
5897
5973
  };
5898
5974
  const TextShape$1 = React.memo(TextShape);
5975
+ const TextLabel = ({
5976
+ element,
5977
+ connector,
5978
+ onChange,
5979
+ autoEdit,
5980
+ onEditStart,
5981
+ onEditEnd
5982
+ }) => {
5983
+ const { id, x, y, width, height, style, text } = element;
5984
+ const labelFontSize = style.fontSize * 0.9;
5985
+ const textRef = useRef(null);
5986
+ const bgRectRef = useRef(null);
5987
+ const isEditingRef = useRef(false);
5988
+ const [isEditingState, setIsEditingState] = useState(false);
5989
+ const autoEditDoneRef = useRef(false);
5990
+ const boundPos = useMemo(() => {
5991
+ const textWidth = Math.max(LABEL_MIN_WIDTH, width || 60);
5992
+ const pos = computeConnectorLabelPosition(connector, textWidth, height);
5993
+ return { x: pos.x, y: pos.y, width: textWidth };
5994
+ }, [connector, width, height]);
5995
+ const effectiveX = boundPos.x;
5996
+ const effectiveY = boundPos.y;
5997
+ const syncSize = useCallback(() => {
5998
+ const node = textRef.current;
5999
+ if (!node || isEditingRef.current) return;
6000
+ const measuredHeight = node.height();
6001
+ const updates = {};
6002
+ let needsUpdate = false;
6003
+ if (Math.abs(measuredHeight - height) > 1) {
6004
+ updates.height = measuredHeight;
6005
+ needsUpdate = true;
6006
+ }
6007
+ const measuredWidth = measureLabelText(text || " ", labelFontSize, style.fontFamily).width;
6008
+ if (Math.abs(measuredWidth - width) > 1) {
6009
+ updates.width = measuredWidth;
6010
+ needsUpdate = true;
6011
+ }
6012
+ if (needsUpdate) {
6013
+ onChange(id, updates);
6014
+ }
6015
+ }, [id, text, height, width, labelFontSize, style.fontFamily, onChange]);
6016
+ const syncSizeInitRef = useRef(true);
6017
+ useEffect(() => {
6018
+ if (syncSizeInitRef.current) {
6019
+ syncSizeInitRef.current = false;
6020
+ if (text && height > 0) return;
6021
+ }
6022
+ const rafId = requestAnimationFrame(syncSize);
6023
+ return () => cancelAnimationFrame(rafId);
6024
+ }, [text, labelFontSize, style.fontFamily, syncSize]);
6025
+ const openEditor = useCallback(() => {
6026
+ var _a;
6027
+ const textNode = textRef.current;
6028
+ if (!textNode || isEditingRef.current) return;
6029
+ const stage = textNode.getStage();
6030
+ if (!stage) return;
6031
+ isEditingRef.current = true;
6032
+ setIsEditingState(true);
6033
+ onEditStart == null ? void 0 : onEditStart(id);
6034
+ const stageContainer = stage.container();
6035
+ const absTransform = textNode.getAbsoluteTransform().copy();
6036
+ const absPos = absTransform.point({ x: 0, y: 0 });
6037
+ const stageScaleX = stage.scaleX();
6038
+ const screenFontSize = labelFontSize * stageScaleX;
6039
+ const nodeWidth = textNode.width();
6040
+ const scaledPadH = LABEL_PADDING_H * stageScaleX;
6041
+ const scaledPadV = LABEL_PADDING_V * stageScaleX;
6042
+ const initMeasured = measureLabelText(text || " ", labelFontSize, style.fontFamily);
6043
+ const initPillW = Math.max(LABEL_MIN_WIDTH, initMeasured.width) * stageScaleX + scaledPadH * 2;
6044
+ const initPillH = initMeasured.height * stageScaleX + scaledPadV * 2;
6045
+ const connMidScreenX = absPos.x + nodeWidth * stageScaleX / 2;
6046
+ const connMidScreenY = absPos.y + textNode.height() * stageScaleX / 2;
6047
+ const originalText = text;
6048
+ const textarea = document.createElement("textarea");
6049
+ stageContainer.appendChild(textarea);
6050
+ textarea.value = text;
6051
+ textarea.style.position = "absolute";
6052
+ textarea.style.top = `${connMidScreenY - initPillH / 2}px`;
6053
+ textarea.style.left = `${connMidScreenX - initPillW / 2}px`;
6054
+ textarea.style.width = `${initPillW}px`;
6055
+ textarea.style.height = `${initPillH}px`;
6056
+ textarea.style.fontSize = `${screenFontSize}px`;
6057
+ textarea.style.fontFamily = style.fontFamily;
6058
+ textarea.style.color = style.strokeColor;
6059
+ textarea.style.lineHeight = `${LABEL_LINE_HEIGHT}`;
6060
+ textarea.style.border = "none";
6061
+ textarea.style.margin = "0";
6062
+ textarea.style.outline = "none";
6063
+ textarea.style.resize = "none";
6064
+ textarea.style.overflow = "hidden";
6065
+ textarea.style.zIndex = "1000";
6066
+ textarea.rows = 1;
6067
+ textarea.style.minHeight = `${Math.max(20, screenFontSize * LABEL_LINE_HEIGHT)}px`;
6068
+ textarea.style.boxSizing = "border-box";
6069
+ textarea.style.transformOrigin = "left top";
6070
+ textarea.style.letterSpacing = "normal";
6071
+ textarea.style.caretColor = style.strokeColor;
6072
+ textarea.style.background = "#f8f9fa";
6073
+ textarea.style.borderRadius = `${LABEL_CORNER * stageScaleX}px`;
6074
+ textarea.style.padding = `${scaledPadV}px ${scaledPadH}px`;
6075
+ textarea.style.textAlign = "center";
6076
+ textarea.style.whiteSpace = "nowrap";
6077
+ textarea.style.wordBreak = "normal";
6078
+ const autoGrow = () => {
6079
+ const currentText = textarea.value || " ";
6080
+ const measured = measureLabelText(currentText, labelFontSize, style.fontFamily);
6081
+ const newTextW = Math.max(LABEL_MIN_WIDTH, measured.width) * stageScaleX;
6082
+ const pillW = newTextW + scaledPadH * 2;
6083
+ const pillH = measured.height * stageScaleX + scaledPadV * 2;
6084
+ textarea.style.width = `${pillW}px`;
6085
+ textarea.style.height = `${pillH}px`;
6086
+ textarea.style.left = `${connMidScreenX - pillW / 2}px`;
6087
+ textarea.style.top = `${connMidScreenY - pillH / 2}px`;
6088
+ };
6089
+ textarea.addEventListener("input", autoGrow);
6090
+ requestAnimationFrame(autoGrow);
6091
+ textNode.hide();
6092
+ (_a = bgRectRef.current) == null ? void 0 : _a.hide();
6093
+ stage.batchDraw();
6094
+ textarea.focus();
6095
+ textarea.select();
6096
+ let cancelled = false;
6097
+ const finishEdit = () => {
6098
+ var _a2;
6099
+ if (!isEditingRef.current) return;
6100
+ isEditingRef.current = false;
6101
+ setIsEditingState(false);
6102
+ const newText = cancelled ? originalText : textarea.value;
6103
+ const isEmpty = newText.trim() === "";
6104
+ textarea.removeEventListener("input", autoGrow);
6105
+ textarea.removeEventListener("blur", handleBlur);
6106
+ textarea.removeEventListener("keydown", handleKeyDown);
6107
+ if (textarea.parentNode) {
6108
+ textarea.parentNode.removeChild(textarea);
6109
+ }
6110
+ textNode.show();
6111
+ (_a2 = bgRectRef.current) == null ? void 0 : _a2.show();
6112
+ stage.batchDraw();
6113
+ if (!cancelled) {
6114
+ const measured = measureLabelText(newText || " ", labelFontSize, style.fontFamily);
6115
+ onChange(id, {
6116
+ text: newText,
6117
+ width: measured.width,
6118
+ height: measured.height
6119
+ });
6120
+ }
6121
+ onEditEnd == null ? void 0 : onEditEnd(id, isEmpty);
6122
+ };
6123
+ const handleBlur = () => finishEdit();
6124
+ const handleKeyDown = (e) => {
6125
+ e.stopPropagation();
6126
+ if (e.key === "Escape") {
6127
+ cancelled = true;
6128
+ textarea.blur();
6129
+ }
6130
+ if (e.key === "Enter" && !e.shiftKey) {
6131
+ e.preventDefault();
6132
+ textarea.blur();
6133
+ }
6134
+ if (e.key === "Tab") {
6135
+ e.preventDefault();
6136
+ textarea.blur();
6137
+ }
6138
+ };
6139
+ textarea.addEventListener("blur", handleBlur);
6140
+ textarea.addEventListener("keydown", handleKeyDown);
6141
+ }, [id, text, style, onChange, onEditStart, onEditEnd]);
6142
+ useEffect(() => {
6143
+ if (!autoEdit) {
6144
+ autoEditDoneRef.current = false;
6145
+ }
6146
+ }, [autoEdit]);
6147
+ useEffect(() => {
6148
+ if (autoEdit && !autoEditDoneRef.current && textRef.current) {
6149
+ autoEditDoneRef.current = true;
6150
+ openEditor();
6151
+ }
6152
+ }, [autoEdit, openEditor]);
6153
+ const pillTextW = Math.max(LABEL_MIN_WIDTH, width || 60);
6154
+ const labelW = pillTextW + LABEL_PADDING_H * 2;
6155
+ const labelH = height + LABEL_PADDING_V * 2;
6156
+ const isVisible = !isEditingState && !(autoEdit && !text);
6157
+ const labelOpacity = text ? style.opacity : 0;
6158
+ return /* @__PURE__ */ jsxs(
6159
+ Group$1,
6160
+ {
6161
+ x: (effectiveX ?? 0) - LABEL_PADDING_H,
6162
+ y: (effectiveY ?? 0) - LABEL_PADDING_V,
6163
+ visible: isVisible,
6164
+ opacity: labelOpacity,
6165
+ listening: false,
6166
+ perfectDrawEnabled: false,
6167
+ children: [
6168
+ /* @__PURE__ */ jsx(
6169
+ Rect,
6170
+ {
6171
+ ref: bgRectRef,
6172
+ width: labelW,
6173
+ height: labelH,
6174
+ fill: "#f8f9fa",
6175
+ cornerRadius: LABEL_CORNER,
6176
+ listening: false,
6177
+ perfectDrawEnabled: false
6178
+ }
6179
+ ),
6180
+ /* @__PURE__ */ jsx(
6181
+ Text,
6182
+ {
6183
+ ref: textRef,
6184
+ id,
6185
+ x: LABEL_PADDING_H,
6186
+ y: LABEL_PADDING_V,
6187
+ text: text || "",
6188
+ fontSize: labelFontSize,
6189
+ fontFamily: style.fontFamily,
6190
+ fill: style.strokeColor,
6191
+ lineHeight: LABEL_LINE_HEIGHT,
6192
+ wrap: "none",
6193
+ listening: false,
6194
+ perfectDrawEnabled: false,
6195
+ onDblClick: openEditor,
6196
+ onDblTap: openEditor
6197
+ }
6198
+ )
6199
+ ]
6200
+ }
6201
+ );
6202
+ };
6203
+ const TextLabel$1 = React.memo(TextLabel);
6204
+ const IMAGE_CACHE_LIMIT = 50;
6205
+ const _imgCache = /* @__PURE__ */ new Map();
6206
+ function cachedLoadImage(src, onLoad, onError) {
6207
+ const cached = _imgCache.get(src);
6208
+ if (cached) {
6209
+ onLoad(cached);
6210
+ return () => {
6211
+ };
6212
+ }
6213
+ const img = new window.Image();
6214
+ if (!src.startsWith("data:")) {
6215
+ img.crossOrigin = "anonymous";
6216
+ }
6217
+ img.onload = () => {
6218
+ if (_imgCache.size >= IMAGE_CACHE_LIMIT) {
6219
+ const oldest = _imgCache.keys().next().value;
6220
+ if (oldest !== void 0) _imgCache.delete(oldest);
6221
+ }
6222
+ _imgCache.set(src, img);
6223
+ onLoad(img);
6224
+ };
6225
+ img.onerror = () => onError();
6226
+ img.src = src;
6227
+ return () => {
6228
+ img.onload = null;
6229
+ img.onerror = null;
6230
+ };
6231
+ }
5899
6232
  const ImageShape = ({
5900
6233
  element,
5901
6234
  isSelected,
@@ -5908,8 +6241,8 @@ const ImageShape = ({
5908
6241
  onDragSnap
5909
6242
  }) => {
5910
6243
  const { id, x, y, width, height, rotation, src, style, crop, cornerRadius, scaleMode, isLocked } = element;
5911
- const [image, setImage] = useState(null);
5912
- const [loadError, setLoadError] = useState(false);
6244
+ const [image, setImage] = useState(() => _imgCache.get(src) ?? null);
6245
+ const [loadError, setLoadError] = useState(() => !src);
5913
6246
  const [liveSize, setLiveSize] = useState({ w: width, h: height });
5914
6247
  useEffect(() => {
5915
6248
  setLiveSize({ w: width, h: height });
@@ -5922,21 +6255,17 @@ const ImageShape = ({
5922
6255
  setLoadError(true);
5923
6256
  return;
5924
6257
  }
5925
- const img = new window.Image();
5926
- img.crossOrigin = "anonymous";
5927
- img.onload = () => {
5928
- setImage(img);
5929
- setLoadError(false);
5930
- };
5931
- img.onerror = () => {
5932
- setImage(null);
5933
- setLoadError(true);
5934
- };
5935
- img.src = src;
5936
- return () => {
5937
- img.onload = null;
5938
- img.onerror = null;
5939
- };
6258
+ return cachedLoadImage(
6259
+ src,
6260
+ (img) => {
6261
+ setImage(img);
6262
+ setLoadError(false);
6263
+ },
6264
+ () => {
6265
+ setImage(null);
6266
+ setLoadError(true);
6267
+ }
6268
+ );
5940
6269
  }, [src]);
5941
6270
  const handleDragMove = useCallback((e) => {
5942
6271
  let nx = e.target.x(), ny = e.target.y();
@@ -6078,13 +6407,14 @@ const ImageShape = ({
6078
6407
  shadowBlur: isSelected ? 6 : 0,
6079
6408
  shadowOpacity: isSelected ? 0.5 : 0,
6080
6409
  children: [
6081
- scaleMode === "fit" && /* @__PURE__ */ jsx(
6410
+ /* @__PURE__ */ jsx(
6082
6411
  Rect,
6083
6412
  {
6084
6413
  width: rw,
6085
6414
  height: rh,
6086
- fill: style.fillColor === "transparent" ? void 0 : style.fillColor,
6087
- cornerRadius
6415
+ fill: scaleMode === "fit" ? style.fillColor === "transparent" ? void 0 : style.fillColor : "transparent",
6416
+ cornerRadius,
6417
+ perfectDrawEnabled: false
6088
6418
  }
6089
6419
  ),
6090
6420
  /* @__PURE__ */ jsx(
@@ -6176,7 +6506,22 @@ const CanvasElementRenderer = ({
6176
6506
  return /* @__PURE__ */ jsx(ArrowShape$1, { element, isSelected, isEditing, isGrouped, onSelect, onChange, onDragMove, onDoubleClick, gridSnap, allElements });
6177
6507
  case "freedraw":
6178
6508
  return /* @__PURE__ */ jsx(FreeDrawShape$1, { element, isSelected, isGrouped, onSelect, onChange, onDragMove, onDoubleClick, gridSnap, onDragSnap });
6179
- case "text":
6509
+ case "text": {
6510
+ const connContainer = element.containerId && allElements ? allElements.find((el) => el.id === element.containerId) : null;
6511
+ const isConnectorLabel = connContainer && (connContainer.type === "arrow" || connContainer.type === "line");
6512
+ if (isConnectorLabel) {
6513
+ return /* @__PURE__ */ jsx(
6514
+ TextLabel$1,
6515
+ {
6516
+ element,
6517
+ connector: connContainer,
6518
+ onChange,
6519
+ autoEdit: autoEditText,
6520
+ onEditStart: onTextEditStart,
6521
+ onEditEnd: onTextEditEnd
6522
+ }
6523
+ );
6524
+ }
6180
6525
  return /* @__PURE__ */ jsx(
6181
6526
  TextShape$1,
6182
6527
  {
@@ -6193,6 +6538,7 @@ const CanvasElementRenderer = ({
6193
6538
  gridSnap
6194
6539
  }
6195
6540
  );
6541
+ }
6196
6542
  case "image":
6197
6543
  return /* @__PURE__ */ jsx(ImageShape$1, { element, isSelected, isGrouped, onSelect, onChange, onDragMove, onDoubleClick, gridSnap, onDragSnap });
6198
6544
  default:
@@ -6529,6 +6875,23 @@ const LinearElementHandles = ({
6529
6875
  const isCurved = "lineType" in element && element.lineType === "curved";
6530
6876
  const isElbow = "lineType" in element && element.lineType === "elbow";
6531
6877
  const endpointsOnly = isCurved || isElbow;
6878
+ const labelRect = useMemo(() => {
6879
+ if (!element.boundElements) return null;
6880
+ for (const be of element.boundElements) {
6881
+ if (be.type !== "text") continue;
6882
+ const txt = allElements.find((e) => e.id === be.id);
6883
+ if (!txt || !txt.text) continue;
6884
+ const textW = Math.max(LABEL_MIN_WIDTH, txt.width || 60);
6885
+ const textH = txt.height || 30;
6886
+ return {
6887
+ x: txt.x - LABEL_PADDING_H,
6888
+ y: txt.y - LABEL_PADDING_V,
6889
+ w: textW + LABEL_PADDING_H * 2,
6890
+ h: textH + LABEL_PADDING_V * 2
6891
+ };
6892
+ }
6893
+ return null;
6894
+ }, [element.boundElements, allElements]);
6532
6895
  const isEndpoint = useCallback(
6533
6896
  (idx) => idx === 0 || idx === pairs.length - 1,
6534
6897
  [pairs.length]
@@ -6708,6 +7071,12 @@ const LinearElementHandles = ({
6708
7071
  !endpointsOnly && pairs.length >= 2 && pairs.slice(0, -1).map((pt, segIdx) => {
6709
7072
  const next = pairs[segIdx + 1];
6710
7073
  const mid = midpoint$1(pt, next);
7074
+ if (labelRect) {
7075
+ const wx = x + mid.x, wy = y + mid.y;
7076
+ if (wx >= labelRect.x && wx <= labelRect.x + labelRect.w && wy >= labelRect.y && wy <= labelRect.y + labelRect.h) {
7077
+ return null;
7078
+ }
7079
+ }
6711
7080
  const isHovered = hoveredMidpointIndex === segIdx;
6712
7081
  return /* @__PURE__ */ jsx(
6713
7082
  Circle$1,
@@ -6764,6 +7133,12 @@ const LinearElementHandles = ({
6764
7133
  const curvature = element.curvature ?? CURVE_RATIO;
6765
7134
  const cp = computeCurveControlPoint(localStart, localEnd, curvature);
6766
7135
  const curveMid = quadBezierAt(localStart, cp, localEnd, 0.5);
7136
+ if (labelRect) {
7137
+ const wx = x + curveMid.x, wy = y + curveMid.y;
7138
+ if (wx >= labelRect.x && wx <= labelRect.x + labelRect.w && wy >= labelRect.y && wy <= labelRect.y + labelRect.h) {
7139
+ return null;
7140
+ }
7141
+ }
6767
7142
  return /* @__PURE__ */ jsx(
6768
7143
  Circle$1,
6769
7144
  {
@@ -8363,28 +8738,6 @@ const ArrowheadLineIcon = ({ arrowhead, end, color }) => {
8363
8738
  ] }) });
8364
8739
  };
8365
8740
  const SvgIcon = ({ children, color, size = 12 }) => /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children });
8366
- const LayerIcons = {
8367
- sendToBack: (color) => /* @__PURE__ */ jsxs(SvgIcon, { color, children: [
8368
- /* @__PURE__ */ jsx("path", { d: "M14 3v4a1 1 0 0 0 1 1h4" }),
8369
- /* @__PURE__ */ jsx("path", { d: "M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z", opacity: "0.3" }),
8370
- /* @__PURE__ */ jsx("polyline", { points: "12 12 12 18" }),
8371
- /* @__PURE__ */ jsx("polyline", { points: "9 15 12 18 15 15" })
8372
- ] }),
8373
- sendBackward: (color) => /* @__PURE__ */ jsxs(SvgIcon, { color, children: [
8374
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
8375
- /* @__PURE__ */ jsx("polyline", { points: "19 12 12 19 5 12" })
8376
- ] }),
8377
- bringForward: (color) => /* @__PURE__ */ jsxs(SvgIcon, { color, children: [
8378
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "19", x2: "12", y2: "5" }),
8379
- /* @__PURE__ */ jsx("polyline", { points: "5 12 12 5 19 12" })
8380
- ] }),
8381
- bringToFront: (color) => /* @__PURE__ */ jsxs(SvgIcon, { color, children: [
8382
- /* @__PURE__ */ jsx("path", { d: "M14 3v4a1 1 0 0 0 1 1h4" }),
8383
- /* @__PURE__ */ jsx("path", { d: "M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z", opacity: "0.3" }),
8384
- /* @__PURE__ */ jsx("polyline", { points: "12 18 12 12" }),
8385
- /* @__PURE__ */ jsx("polyline", { points: "9 15 12 12 15 15" })
8386
- ] })
8387
- };
8388
8741
  const ActionIcons = {
8389
8742
  duplicate: (color) => /* @__PURE__ */ jsxs(SvgIcon, { color, children: [
8390
8743
  /* @__PURE__ */ jsx("rect", { x: "8", y: "8", width: "12", height: "12", rx: "2" }),
@@ -8418,10 +8771,6 @@ const StylePanel = ({ theme }) => {
8418
8771
  const toggleLockElements = useCanvasStore((s) => s.toggleLockElements);
8419
8772
  const deleteElements = useCanvasStore((s) => s.deleteElements);
8420
8773
  const duplicateElements = useCanvasStore((s) => s.duplicateElements);
8421
- const bringToFront = useCanvasStore((s) => s.bringToFront);
8422
- const sendToBack = useCanvasStore((s) => s.sendToBack);
8423
- const bringForward = useCanvasStore((s) => s.bringForward);
8424
- const sendBackward = useCanvasStore((s) => s.sendBackward);
8425
8774
  const activeTool = useCanvasStore((s) => s.activeTool);
8426
8775
  const currentLineType = useCanvasStore((s) => s.currentLineType);
8427
8776
  const setCurrentLineType = useCanvasStore((s) => s.setCurrentLineType);
@@ -8914,31 +9263,22 @@ const StylePanel = ({ theme }) => {
8914
9263
  {
8915
9264
  theme,
8916
9265
  style: { width: "100%", padding: "5px 0", borderRadius: 4 },
8917
- onClick: () => {
8918
- const input = document.createElement("input");
8919
- input.type = "file";
8920
- input.accept = "image/*";
8921
- input.onchange = async () => {
8922
- var _a;
8923
- const file = (_a = input.files) == null ? void 0 : _a[0];
9266
+ onClick: async () => {
9267
+ const targetId = selectedImage.id;
9268
+ try {
9269
+ const files = await openImageFilePicker();
9270
+ const file = files[0];
8924
9271
  if (!file) return;
8925
- const reader = new FileReader();
8926
- reader.onload = () => {
8927
- const dataURL = reader.result;
8928
- const img = new window.Image();
8929
- img.onload = () => {
8930
- updateElement(selectedImage.id, {
8931
- src: dataURL,
8932
- naturalWidth: img.naturalWidth,
8933
- naturalHeight: img.naturalHeight
8934
- });
8935
- pushHistory();
8936
- };
8937
- img.src = dataURL;
8938
- };
8939
- reader.readAsDataURL(file);
8940
- };
8941
- input.click();
9272
+ const dataURL = await fileToDataURL(file);
9273
+ const img = await loadImage(dataURL);
9274
+ updateElement(targetId, {
9275
+ src: dataURL,
9276
+ naturalWidth: img.naturalWidth,
9277
+ naturalHeight: img.naturalHeight
9278
+ });
9279
+ pushHistory();
9280
+ } catch {
9281
+ }
8942
9282
  },
8943
9283
  children: "Replace Image…"
8944
9284
  }
@@ -8946,27 +9286,6 @@ const StylePanel = ({ theme }) => {
8946
9286
  ] }),
8947
9287
  hasSelection && /* @__PURE__ */ jsxs(Fragment, { children: [
8948
9288
  /* @__PURE__ */ jsx("hr", { style: dividerStyle }),
8949
- /* @__PURE__ */ jsx(PanelSection, { label: "Layers", theme, children: /* @__PURE__ */ jsx(ButtonRow, { columns: 4, children: [
8950
- { fn: sendToBack, icon: LayerIcons.sendToBack, tip: "Send to back" },
8951
- { fn: sendBackward, icon: LayerIcons.sendBackward, tip: "Send backward" },
8952
- { fn: bringForward, icon: LayerIcons.bringForward, tip: "Bring forward" },
8953
- { fn: bringToFront, icon: LayerIcons.bringToFront, tip: "Bring to front" }
8954
- ].map(({ fn, icon, tip }) => /* @__PURE__ */ jsx(
8955
- PanelButton,
8956
- {
8957
- variant: "action",
8958
- theme,
8959
- title: tip,
8960
- width: "100%",
8961
- height: 32,
8962
- onClick: () => {
8963
- fn(selectedIds);
8964
- pushHistory();
8965
- },
8966
- children: (hl) => icon(hl ? theme.activeToolColor : theme.textColor)
8967
- },
8968
- tip
8969
- )) }) }),
8970
9289
  /* @__PURE__ */ jsx(PanelSection, { label: "Actions", theme, children: /* @__PURE__ */ jsxs(ButtonRow, { columns: 4, children: [
8971
9290
  /* @__PURE__ */ jsx(
8972
9291
  PanelButton,
@@ -9664,48 +9983,14 @@ function computeAlignGuides(movingBounds, others, excludeIds, threshold = SNAP_T
9664
9983
  return result;
9665
9984
  }
9666
9985
  function useKeyboardShortcuts(enabled = true, containerRef) {
9667
- const setActiveTool = useCanvasStore((s) => s.setActiveTool);
9668
- const undo = useCanvasStore((s) => s.undo);
9669
- const redo = useCanvasStore((s) => s.redo);
9670
- const selectedIds = useCanvasStore((s) => s.selectedIds);
9671
- const elements = useCanvasStore((s) => s.elements);
9672
- const deleteElements = useCanvasStore((s) => s.deleteElements);
9673
- const duplicateElements = useCanvasStore((s) => s.duplicateElements);
9674
- const bringToFront = useCanvasStore((s) => s.bringToFront);
9675
- const sendToBack = useCanvasStore((s) => s.sendToBack);
9676
- const bringForward = useCanvasStore((s) => s.bringForward);
9677
- const sendBackward = useCanvasStore((s) => s.sendBackward);
9678
- const toggleGrid = useCanvasStore((s) => s.toggleGrid);
9679
- const showGrid = useCanvasStore((s) => s.showGrid);
9680
- const zoomIn = useCanvasStore((s) => s.zoomIn);
9681
- const zoomOut = useCanvasStore((s) => s.zoomOut);
9682
- const resetZoom = useCanvasStore((s) => s.resetZoom);
9683
- const zoomToFit = useCanvasStore((s) => s.zoomToFit);
9684
- const zoomToSelection = useCanvasStore((s) => s.zoomToSelection);
9685
- const clearSelection = useCanvasStore((s) => s.clearSelection);
9686
- const updateElement = useCanvasStore((s) => s.updateElement);
9687
- const addElement = useCanvasStore((s) => s.addElement);
9688
- const setSelectedIds = useCanvasStore((s) => s.setSelectedIds);
9689
- const pushHistory = useCanvasStore((s) => s.pushHistory);
9690
- const linearEdit = useLinearEditStore();
9691
9986
  const copyElements = useCallback(() => {
9987
+ const { selectedIds, elements } = useCanvasStore.getState();
9692
9988
  if (selectedIds.length === 0) return;
9693
9989
  setClipboard(gatherElementsForCopy(selectedIds, elements));
9694
- }, [selectedIds, elements]);
9695
- const pasteElements = useCallback(() => {
9696
- const clip = getClipboard();
9697
- if (clip.length === 0) return;
9698
- const PASTE_OFFSET = 20;
9699
- const { clones, selectedCloneIds } = cloneAndRemapElements(clip, clip, PASTE_OFFSET);
9700
- clones.forEach((el) => addElement(el));
9701
- setSelectedIds(selectedCloneIds.length > 0 ? selectedCloneIds : clones.map((c) => c.id));
9702
- pushHistory();
9703
- setClipboard(
9704
- clip.map((el) => ({ ...el, x: el.x + PASTE_OFFSET, y: el.y + PASTE_OFFSET }))
9705
- );
9706
- }, [addElement, setSelectedIds, pushHistory]);
9990
+ }, []);
9707
9991
  const nudge = useCallback(
9708
9992
  (dx, dy) => {
9993
+ const { selectedIds, elements, updateElement, pushHistory } = useCanvasStore.getState();
9709
9994
  if (selectedIds.length === 0) return;
9710
9995
  selectedIds.forEach((id) => {
9711
9996
  const el = elements.find((e) => e.id === id);
@@ -9715,7 +10000,7 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9715
10000
  });
9716
10001
  pushHistory();
9717
10002
  },
9718
- [selectedIds, elements, updateElement, pushHistory]
10003
+ []
9719
10004
  );
9720
10005
  useEffect(() => {
9721
10006
  if (!enabled) return;
@@ -9723,6 +10008,8 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9723
10008
  const tag = e.target.tagName;
9724
10009
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
9725
10010
  const isCmd = e.metaKey || e.ctrlKey;
10011
+ const store = useCanvasStore.getState();
10012
+ const linearEdit = useLinearEditStore.getState();
9726
10013
  if (!isCmd && !e.shiftKey) {
9727
10014
  const toolMap = {
9728
10015
  v: "select",
@@ -9740,30 +10027,29 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9740
10027
  const tool = toolMap[e.key.toLowerCase()];
9741
10028
  if (tool) {
9742
10029
  e.preventDefault();
9743
- setActiveTool(tool);
10030
+ store.setActiveTool(tool);
9744
10031
  return;
9745
10032
  }
9746
10033
  }
9747
10034
  if (isCmd && e.key === "z" && !e.shiftKey) {
9748
10035
  e.preventDefault();
9749
- undo();
10036
+ store.undo();
9750
10037
  return;
9751
10038
  }
9752
10039
  if (isCmd && e.key === "z" && e.shiftKey) {
9753
10040
  e.preventDefault();
9754
- redo();
10041
+ store.redo();
9755
10042
  return;
9756
10043
  }
9757
10044
  if (isCmd && e.key === "y") {
9758
10045
  e.preventDefault();
9759
- redo();
10046
+ store.redo();
9760
10047
  return;
9761
10048
  }
9762
10049
  if ((e.key === "Delete" || e.key === "Backspace") && !isCmd) {
9763
10050
  if (linearEdit.isEditing && linearEdit.selectedPointIndices.length > 0) {
9764
10051
  e.preventDefault();
9765
- const { elements: elements2, updateElement: updateElement2, pushHistory: pushHistory2 } = useCanvasStore.getState();
9766
- const el = elements2.find((e2) => e2.id === linearEdit.elementId);
10052
+ const el = store.elements.find((e2) => e2.id === linearEdit.elementId);
9767
10053
  if (el) {
9768
10054
  const pointCount = el.points.length / 2;
9769
10055
  const indicesToDelete = new Set(linearEdit.selectedPointIndices);
@@ -9801,75 +10087,75 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9801
10087
  if (indicesToDelete.has(pointCount - 1)) {
9802
10088
  pointUpdates.endBinding = null;
9803
10089
  }
9804
- updateElement2(el.id, pointUpdates);
9805
- pushHistory2();
10090
+ store.updateElement(el.id, pointUpdates);
10091
+ store.pushHistory();
9806
10092
  linearEdit.setSelectedPoints([]);
9807
10093
  }
9808
10094
  }
9809
10095
  return;
9810
10096
  }
9811
- if (selectedIds.length > 0) {
10097
+ if (store.selectedIds.length > 0) {
9812
10098
  e.preventDefault();
9813
- const unlocked = selectedIds.filter((sid) => {
9814
- const el = elements.find((e2) => e2.id === sid);
10099
+ const unlocked = store.selectedIds.filter((sid) => {
10100
+ const el = store.elements.find((e2) => e2.id === sid);
9815
10101
  return el && !el.isLocked;
9816
10102
  });
9817
- if (unlocked.length > 0) deleteElements(unlocked);
10103
+ if (unlocked.length > 0) store.deleteElements(unlocked);
9818
10104
  }
9819
10105
  return;
9820
10106
  }
9821
10107
  if (isCmd && e.key === "d") {
9822
10108
  e.preventDefault();
9823
- if (selectedIds.length > 0) {
9824
- duplicateElements(selectedIds);
10109
+ if (store.selectedIds.length > 0) {
10110
+ store.duplicateElements(store.selectedIds);
9825
10111
  }
9826
10112
  return;
9827
10113
  }
9828
10114
  if (e.key === "]" && isCmd && e.shiftKey) {
9829
10115
  e.preventDefault();
9830
- bringToFront(selectedIds);
10116
+ store.bringToFront(store.selectedIds);
9831
10117
  return;
9832
10118
  }
9833
10119
  if (e.key === "[" && isCmd && e.shiftKey) {
9834
10120
  e.preventDefault();
9835
- sendToBack(selectedIds);
10121
+ store.sendToBack(store.selectedIds);
9836
10122
  return;
9837
10123
  }
9838
10124
  if (e.key === "]" && isCmd && !e.shiftKey) {
9839
10125
  e.preventDefault();
9840
- bringForward(selectedIds);
10126
+ store.bringForward(store.selectedIds);
9841
10127
  return;
9842
10128
  }
9843
10129
  if (e.key === "[" && isCmd && !e.shiftKey) {
9844
10130
  e.preventDefault();
9845
- sendBackward(selectedIds);
10131
+ store.sendBackward(store.selectedIds);
9846
10132
  return;
9847
10133
  }
9848
10134
  if (e.key === "g" && !isCmd) {
9849
10135
  e.preventDefault();
9850
- toggleGrid();
10136
+ store.toggleGrid();
9851
10137
  return;
9852
10138
  }
9853
10139
  if (isCmd && (e.key === "=" || e.key === "+")) {
9854
10140
  e.preventDefault();
9855
- zoomIn();
10141
+ store.zoomIn();
9856
10142
  return;
9857
10143
  }
9858
10144
  if (isCmd && e.key === "-") {
9859
10145
  e.preventDefault();
9860
- zoomOut();
10146
+ store.zoomOut();
9861
10147
  return;
9862
10148
  }
9863
10149
  if (isCmd && e.key === "0") {
9864
10150
  e.preventDefault();
9865
- resetZoom();
10151
+ store.resetZoom();
9866
10152
  return;
9867
10153
  }
9868
10154
  if (isCmd && e.shiftKey && e.key === "1") {
9869
10155
  e.preventDefault();
9870
10156
  if (containerRef == null ? void 0 : containerRef.current) {
9871
10157
  const rect = containerRef.current.getBoundingClientRect();
9872
- zoomToFit(rect.width, rect.height, void 0, { animate: true });
10158
+ store.zoomToFit(rect.width, rect.height, void 0, { animate: true });
9873
10159
  }
9874
10160
  return;
9875
10161
  }
@@ -9877,14 +10163,14 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9877
10163
  e.preventDefault();
9878
10164
  if (containerRef == null ? void 0 : containerRef.current) {
9879
10165
  const rect = containerRef.current.getBoundingClientRect();
9880
- zoomToSelection(rect.width, rect.height, { animate: true });
10166
+ store.zoomToSelection(rect.width, rect.height, { animate: true });
9881
10167
  }
9882
10168
  return;
9883
10169
  }
9884
10170
  if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key) && !isCmd) {
9885
- if (selectedIds.length > 0 && !linearEdit.isEditing) {
10171
+ if (store.selectedIds.length > 0 && !linearEdit.isEditing) {
9886
10172
  e.preventDefault();
9887
- const baseStep = showGrid ? GRID_SIZE : 1;
10173
+ const baseStep = store.showGrid ? GRID_SIZE : 1;
9888
10174
  const step = e.shiftKey ? baseStep * 10 : baseStep;
9889
10175
  const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
9890
10176
  const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
@@ -9893,7 +10179,7 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9893
10179
  }
9894
10180
  }
9895
10181
  if (isCmd && e.key === "c") {
9896
- if (selectedIds.length > 0) {
10182
+ if (store.selectedIds.length > 0) {
9897
10183
  e.preventDefault();
9898
10184
  copyElements();
9899
10185
  }
@@ -9903,34 +10189,31 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9903
10189
  return;
9904
10190
  }
9905
10191
  if (isCmd && e.key === "x") {
9906
- if (selectedIds.length > 0) {
10192
+ if (store.selectedIds.length > 0) {
9907
10193
  e.preventDefault();
9908
10194
  copyElements();
9909
- deleteElements(selectedIds);
10195
+ store.deleteElements(store.selectedIds);
9910
10196
  }
9911
10197
  return;
9912
10198
  }
9913
10199
  if (isCmd && e.key === "g" && !e.shiftKey) {
9914
10200
  e.preventDefault();
9915
- if (selectedIds.length >= 2) {
9916
- const { groupElements } = useCanvasStore.getState();
9917
- groupElements(selectedIds);
10201
+ if (store.selectedIds.length >= 2) {
10202
+ store.groupElements(store.selectedIds);
9918
10203
  }
9919
10204
  return;
9920
10205
  }
9921
10206
  if (isCmd && e.key === "g" && e.shiftKey) {
9922
10207
  e.preventDefault();
9923
- if (selectedIds.length > 0) {
9924
- const { ungroupElements } = useCanvasStore.getState();
9925
- ungroupElements(selectedIds);
10208
+ if (store.selectedIds.length > 0) {
10209
+ store.ungroupElements(store.selectedIds);
9926
10210
  }
9927
10211
  return;
9928
10212
  }
9929
10213
  if (isCmd && e.shiftKey && e.key === "l") {
9930
10214
  e.preventDefault();
9931
- if (selectedIds.length > 0) {
9932
- const { toggleLockElements } = useCanvasStore.getState();
9933
- toggleLockElements(selectedIds);
10215
+ if (store.selectedIds.length > 0) {
10216
+ store.toggleLockElements(store.selectedIds);
9934
10217
  }
9935
10218
  return;
9936
10219
  }
@@ -9940,46 +10223,20 @@ function useKeyboardShortcuts(enabled = true, containerRef) {
9940
10223
  linearEdit.exitEditMode();
9941
10224
  return;
9942
10225
  }
9943
- clearSelection();
9944
- setActiveTool("select");
10226
+ store.clearSelection();
10227
+ store.setActiveTool("select");
9945
10228
  return;
9946
10229
  }
9947
10230
  if (isCmd && e.key === "a") {
9948
10231
  e.preventDefault();
9949
- const { elements: elements2, setSelectedIds: setSelectedIds2 } = useCanvasStore.getState();
9950
- setSelectedIds2(elements2.map((el) => el.id));
10232
+ const { elements, setSelectedIds } = useCanvasStore.getState();
10233
+ setSelectedIds(elements.map((el) => el.id));
9951
10234
  return;
9952
10235
  }
9953
10236
  };
9954
10237
  window.addEventListener("keydown", handleKeyDown);
9955
10238
  return () => window.removeEventListener("keydown", handleKeyDown);
9956
- }, [
9957
- enabled,
9958
- setActiveTool,
9959
- undo,
9960
- redo,
9961
- selectedIds,
9962
- elements,
9963
- deleteElements,
9964
- duplicateElements,
9965
- bringToFront,
9966
- sendToBack,
9967
- bringForward,
9968
- sendBackward,
9969
- toggleGrid,
9970
- showGrid,
9971
- zoomIn,
9972
- zoomOut,
9973
- resetZoom,
9974
- zoomToFit,
9975
- zoomToSelection,
9976
- containerRef,
9977
- clearSelection,
9978
- linearEdit,
9979
- nudge,
9980
- copyElements,
9981
- pasteElements
9982
- ]);
10239
+ }, [enabled, containerRef, nudge, copyElements]);
9983
10240
  }
9984
10241
  function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
9985
10242
  while (right > left) {
@@ -10941,6 +11198,70 @@ function disposeExportWorkerManager() {
10941
11198
  _config = void 0;
10942
11199
  }
10943
11200
  }
11201
+ const BOUND_TEXT_PADDING = 4;
11202
+ const CONTAINER_TYPES = /* @__PURE__ */ new Set([
11203
+ "rectangle",
11204
+ "ellipse",
11205
+ "diamond",
11206
+ "image"
11207
+ ]);
11208
+ function computeBoundTextPosition(container, text) {
11209
+ const tw = Math.max(20, container.width - BOUND_TEXT_PADDING * 2);
11210
+ let ty;
11211
+ if (text.verticalAlign === "top") {
11212
+ ty = container.y + BOUND_TEXT_PADDING;
11213
+ } else if (text.verticalAlign === "bottom") {
11214
+ ty = container.y + container.height - text.height - BOUND_TEXT_PADDING;
11215
+ } else {
11216
+ ty = container.y + (container.height - text.height) / 2;
11217
+ }
11218
+ return { x: container.x + BOUND_TEXT_PADDING, y: ty, width: tw };
11219
+ }
11220
+ function syncAfterDrag(movedIds, elements, skipIds) {
11221
+ const elMap = /* @__PURE__ */ new Map();
11222
+ for (const el of elements) elMap.set(el.id, el);
11223
+ const updates = [];
11224
+ const processedConnectors = /* @__PURE__ */ new Set();
11225
+ for (const id of movedIds) {
11226
+ const connectors = findConnectorsForElement(id, elements);
11227
+ for (const conn of connectors) {
11228
+ if (processedConnectors.has(conn.id)) continue;
11229
+ processedConnectors.add(conn.id);
11230
+ if (skipIds == null ? void 0 : skipIds.has(conn.id)) continue;
11231
+ const freshConn = elMap.get(conn.id);
11232
+ if (!freshConn) continue;
11233
+ const recomputed = recomputeBoundPoints(freshConn, elements);
11234
+ if (recomputed) updates.push({ id: freshConn.id, updates: recomputed });
11235
+ }
11236
+ const el = elMap.get(id);
11237
+ if ((el == null ? void 0 : el.boundElements) && CONTAINER_TYPES.has(el.type)) {
11238
+ for (const be of el.boundElements) {
11239
+ if (be.type !== "text") continue;
11240
+ if (skipIds == null ? void 0 : skipIds.has(be.id)) continue;
11241
+ const txt = elMap.get(be.id);
11242
+ if (!txt) continue;
11243
+ updates.push({ id: be.id, updates: computeBoundTextPosition(el, txt) });
11244
+ }
11245
+ }
11246
+ }
11247
+ if (processedConnectors.size > 0 && updates.length > 0) {
11248
+ const tempMap = new Map(elMap);
11249
+ for (const u of updates) {
11250
+ const existing = tempMap.get(u.id);
11251
+ if (existing) {
11252
+ tempMap.set(u.id, { ...existing, ...u.updates });
11253
+ }
11254
+ }
11255
+ const labelUpdates = syncConnectorLabels(
11256
+ processedConnectors,
11257
+ tempMap
11258
+ );
11259
+ for (const lu of labelUpdates) {
11260
+ updates.push(lu);
11261
+ }
11262
+ }
11263
+ return { updates, processedConnectorIds: processedConnectors };
11264
+ }
10944
11265
  const DEFAULT_THEME = {
10945
11266
  canvasBackground: "#f8f9fa",
10946
11267
  gridColor: "#e5e5e5",
@@ -11661,6 +11982,86 @@ const CursorOverlay = ({
11661
11982
  }) });
11662
11983
  };
11663
11984
  const CursorOverlay$1 = React.memo(CursorOverlay);
11985
+ const AnnotationItem = React.memo(({
11986
+ element: el,
11987
+ viewport,
11988
+ renderAnnotation
11989
+ }) => {
11990
+ const screenX = el.x * viewport.scale + viewport.x;
11991
+ const screenY = el.y * viewport.scale + viewport.y;
11992
+ const screenW = el.width * viewport.scale;
11993
+ const screenH = el.height * viewport.scale;
11994
+ const ctx = {
11995
+ element: el,
11996
+ screenBounds: { x: screenX, y: screenY, width: screenW, height: screenH },
11997
+ scale: viewport.scale
11998
+ };
11999
+ const annotation = renderAnnotation(ctx);
12000
+ if (!annotation) return null;
12001
+ const transforms = [`scale(${viewport.scale})`];
12002
+ if (el.rotation) transforms.push(`rotate(${el.rotation}deg)`);
12003
+ return /* @__PURE__ */ jsx(
12004
+ "div",
12005
+ {
12006
+ style: {
12007
+ position: "absolute",
12008
+ left: screenX,
12009
+ top: screenY,
12010
+ width: el.width,
12011
+ // world-space; CSS scale handles screen sizing
12012
+ height: el.height,
12013
+ // world-space
12014
+ pointerEvents: "none",
12015
+ transform: transforms.join(" "),
12016
+ transformOrigin: "top left"
12017
+ },
12018
+ children: annotation
12019
+ }
12020
+ );
12021
+ });
12022
+ AnnotationItem.displayName = "AnnotationItem";
12023
+ const CONTAINER_STYLE = {
12024
+ position: "absolute",
12025
+ top: 0,
12026
+ left: 0,
12027
+ width: "100%",
12028
+ height: "100%",
12029
+ pointerEvents: "none",
12030
+ overflow: "hidden",
12031
+ zIndex: 10
12032
+ };
12033
+ const AnnotationsOverlay = React.memo(({
12034
+ elements,
12035
+ viewport,
12036
+ containerWidth,
12037
+ containerHeight,
12038
+ renderAnnotation
12039
+ }) => {
12040
+ const visibleElements = useMemo(() => {
12041
+ const margin = 100;
12042
+ return elements.filter((el) => {
12043
+ if (el.type === "text" && el.containerId) return false;
12044
+ if (!el.isVisible) return false;
12045
+ const sx = el.x * viewport.scale + viewport.x;
12046
+ const sy = el.y * viewport.scale + viewport.y;
12047
+ const sw = el.width * viewport.scale;
12048
+ const sh = el.height * viewport.scale;
12049
+ if (sx + sw < -margin || sy + sh < -margin) return false;
12050
+ if (sx > containerWidth + margin || sy > containerHeight + margin) return false;
12051
+ return true;
12052
+ });
12053
+ }, [elements, viewport.scale, viewport.x, viewport.y, containerWidth, containerHeight]);
12054
+ if (visibleElements.length === 0) return null;
12055
+ return /* @__PURE__ */ jsx("div", { style: CONTAINER_STYLE, children: visibleElements.map((el) => /* @__PURE__ */ jsx(
12056
+ AnnotationItem,
12057
+ {
12058
+ element: el,
12059
+ viewport,
12060
+ renderAnnotation
12061
+ },
12062
+ el.id
12063
+ )) });
12064
+ });
11664
12065
  Konva.showWarnings = false;
11665
12066
  function arraysShallowEqual(a, b) {
11666
12067
  if (a.length !== b.length) return false;
@@ -11687,9 +12088,11 @@ const StaticElementsLayer = ({
11687
12088
  onGroupDragEnd
11688
12089
  }) => {
11689
12090
  const layerRef = useRef(null);
11690
- useEffect(() => {
12091
+ useLayoutEffect(() => {
11691
12092
  const layer = layerRef.current;
11692
- if (!layer || elements.length === 0) return;
12093
+ if (!layer) return;
12094
+ layer.clearCache();
12095
+ if (elements.length === 0) return;
11693
12096
  const dpr2 = window.devicePixelRatio || 1;
11694
12097
  const cachePixelRatio = dpr2 * Math.max(1, viewportScale);
11695
12098
  const rafId = requestAnimationFrame(() => {
@@ -11708,7 +12111,6 @@ const StaticElementsLayer = ({
11708
12111
  });
11709
12112
  return () => {
11710
12113
  cancelAnimationFrame(rafId);
11711
- layer.clearCache();
11712
12114
  };
11713
12115
  }, [elements, viewportScale, autoEditTextId]);
11714
12116
  const { ungrouped, groups } = useMemo(() => {
@@ -11859,6 +12261,7 @@ const FlowCanvas = forwardRef((props, ref) => {
11859
12261
  className,
11860
12262
  contextMenuItems: contextMenuItemsProp,
11861
12263
  renderContextMenu,
12264
+ renderAnnotation,
11862
12265
  collaboration: collaborationConfig,
11863
12266
  workerConfig,
11864
12267
  customElementTypes
@@ -11917,7 +12320,9 @@ const FlowCanvas = forwardRef((props, ref) => {
11917
12320
  const { isDraggingPoint: isLinearDragging, elementId: linearEditId } = linearEdit;
11918
12321
  const prevResolvedRef = useRef([]);
11919
12322
  const resolvedElements = useMemo(() => {
12323
+ var _a, _b;
11920
12324
  let result = null;
12325
+ const changedConnectorIds = /* @__PURE__ */ new Set();
11921
12326
  for (let i = 0; i < elements.length; i++) {
11922
12327
  const el = elements[i];
11923
12328
  if (el.type !== "line" && el.type !== "arrow") {
@@ -11926,10 +12331,18 @@ const FlowCanvas = forwardRef((props, ref) => {
11926
12331
  }
11927
12332
  const conn = el;
11928
12333
  if (!conn.startBinding && !conn.endBinding) {
12334
+ if ((_a = conn.boundElements) == null ? void 0 : _a.some((be) => be.type === "text")) {
12335
+ changedConnectorIds.add(conn.id);
12336
+ if (!result) result = elements.slice(0, i);
12337
+ }
11929
12338
  if (result) result[i] = el;
11930
12339
  continue;
11931
12340
  }
11932
12341
  if (isLinearDragging && linearEditId === el.id) {
12342
+ if ((_b = conn.boundElements) == null ? void 0 : _b.some((be) => be.type === "text")) {
12343
+ if (!result) result = elements.slice(0, i);
12344
+ changedConnectorIds.add(conn.id);
12345
+ }
11933
12346
  if (result) result[i] = el;
11934
12347
  continue;
11935
12348
  }
@@ -11939,10 +12352,31 @@ const FlowCanvas = forwardRef((props, ref) => {
11939
12352
  result = elements.slice(0, i);
11940
12353
  }
11941
12354
  result[i] = { ...conn, ...recomputed };
12355
+ changedConnectorIds.add(conn.id);
11942
12356
  } else {
11943
12357
  if (result) result[i] = el;
11944
12358
  }
11945
12359
  }
12360
+ if (changedConnectorIds.size > 0 && result) {
12361
+ const patchMap = /* @__PURE__ */ new Map();
12362
+ for (const el of result) patchMap.set(el.id, el);
12363
+ for (const connId of changedConnectorIds) {
12364
+ const conn = patchMap.get(connId);
12365
+ if (!(conn == null ? void 0 : conn.boundElements)) continue;
12366
+ for (const be of conn.boundElements) {
12367
+ if (be.type !== "text") continue;
12368
+ const txtIdx = result.findIndex((e) => e.id === be.id);
12369
+ if (txtIdx === -1) continue;
12370
+ const txt = result[txtIdx];
12371
+ const textW = Math.max(10, txt.width || 60);
12372
+ const textH = txt.height || 30;
12373
+ const pos = computeConnectorLabelPosition(conn, textW, textH);
12374
+ if (Math.abs(txt.x - pos.x) > 0.01 || Math.abs(txt.y - pos.y) > 0.01) {
12375
+ result[txtIdx] = { ...txt, x: pos.x, y: pos.y };
12376
+ }
12377
+ }
12378
+ }
12379
+ }
11946
12380
  const finalResult = result ?? elements;
11947
12381
  const prev = prevResolvedRef.current;
11948
12382
  if (finalResult.length === prev.length) {
@@ -11974,7 +12408,7 @@ const FlowCanvas = forwardRef((props, ref) => {
11974
12408
  const prevStaticRef = useRef([]);
11975
12409
  const prevInteractiveRef = useRef([]);
11976
12410
  const { staticElements, interactiveElements } = useMemo(() => {
11977
- var _a, _b;
12411
+ var _a, _b, _c, _d;
11978
12412
  let effectiveSelected = drawingElementId ? /* @__PURE__ */ new Set([...selectedIdsSet, drawingElementId]) : selectedIdsSet;
11979
12413
  if (effectiveSelected.size > 0) {
11980
12414
  const expanded = new Set(effectiveSelected);
@@ -11988,6 +12422,32 @@ const FlowCanvas = forwardRef((props, ref) => {
11988
12422
  }
11989
12423
  }
11990
12424
  }
12425
+ for (const el of visibleElements) {
12426
+ if (!expanded.has(el.id)) continue;
12427
+ if (el.boundElements) {
12428
+ for (const be of el.boundElements) {
12429
+ if (be.type === "text") expanded.add(be.id);
12430
+ }
12431
+ }
12432
+ if (el.type === "text" && el.containerId) {
12433
+ expanded.add(el.containerId);
12434
+ }
12435
+ }
12436
+ for (const el of visibleElements) {
12437
+ if (el.type !== "line" && el.type !== "arrow") continue;
12438
+ if (expanded.has(el.id)) continue;
12439
+ const conn = el;
12440
+ const startBound = (_c = conn.startBinding) == null ? void 0 : _c.elementId;
12441
+ const endBound = (_d = conn.endBinding) == null ? void 0 : _d.elementId;
12442
+ if (startBound && expanded.has(startBound) || endBound && expanded.has(endBound)) {
12443
+ expanded.add(el.id);
12444
+ if (el.boundElements) {
12445
+ for (const be of el.boundElements) {
12446
+ if (be.type === "text") expanded.add(be.id);
12447
+ }
12448
+ }
12449
+ }
12450
+ }
11991
12451
  if (expanded.size !== effectiveSelected.size) {
11992
12452
  effectiveSelected = expanded;
11993
12453
  }
@@ -12146,7 +12606,6 @@ const FlowCanvas = forwardRef((props, ref) => {
12146
12606
  const files = getImageFilesFromDataTransfer(e.dataTransfer);
12147
12607
  if (files.length === 0) return;
12148
12608
  const rect = container.getBoundingClientRect();
12149
- stageRef.current;
12150
12609
  const vp = useCanvasStore.getState().viewport;
12151
12610
  const dropX = (e.clientX - rect.left - vp.x) / vp.scale;
12152
12611
  const dropY = (e.clientY - rect.top - vp.y) / vp.scale;
@@ -12312,6 +12771,12 @@ const FlowCanvas = forwardRef((props, ref) => {
12312
12771
  if (!pos) return;
12313
12772
  if (contextMenu) setContextMenu(null);
12314
12773
  if (ctx.activeTool === "hand" || isSpacePanning) return;
12774
+ if (editingTextId) {
12775
+ const activeEl = document.activeElement;
12776
+ if ((activeEl == null ? void 0 : activeEl.tagName) === "TEXTAREA") {
12777
+ activeEl.blur();
12778
+ }
12779
+ }
12315
12780
  const handler = getToolHandler(ctx.activeTool);
12316
12781
  handler == null ? void 0 : handler.onMouseDown(e, pos, ctx);
12317
12782
  if (currentElementIdRef.current) {
@@ -12319,7 +12784,7 @@ const FlowCanvas = forwardRef((props, ref) => {
12319
12784
  }
12320
12785
  },
12321
12786
  // eslint-disable-next-line react-hooks/exhaustive-deps
12322
- [readOnly, contextMenu, isSpacePanning]
12787
+ [readOnly, contextMenu, isSpacePanning, editingTextId]
12323
12788
  );
12324
12789
  const handleMouseMoveCore = useCallback((e) => {
12325
12790
  if (spaceKeyRef.current) return;
@@ -12366,7 +12831,7 @@ const FlowCanvas = forwardRef((props, ref) => {
12366
12831
  if (!stage) return;
12367
12832
  const pointer = stage.getPointerPosition();
12368
12833
  if (!pointer) return;
12369
- const scaleBy = 1.05;
12834
+ const scaleBy = 1.02;
12370
12835
  const dir2 = e.evt.deltaY > 0 ? -1 : 1;
12371
12836
  const targetScale = Math.min(MAX_ZOOM, Math.max(
12372
12837
  MIN_ZOOM,
@@ -12376,13 +12841,14 @@ const FlowCanvas = forwardRef((props, ref) => {
12376
12841
  },
12377
12842
  [viewport, setViewport]
12378
12843
  );
12379
- const handleStageDragEnd = useCallback(
12844
+ const handleStageDragMove = useCallback(
12380
12845
  (e) => {
12381
12846
  if (activeTool !== "hand" && !spaceKeyRef.current && !isSpacePanning) return;
12382
12847
  setViewport({ x: e.target.x(), y: e.target.y() });
12383
12848
  },
12384
12849
  [activeTool, setViewport, isSpacePanning]
12385
12850
  );
12851
+ const handleStageDragEnd = handleStageDragMove;
12386
12852
  const handleElementSelect = useCallback(
12387
12853
  (id) => {
12388
12854
  var _a;
@@ -12430,6 +12896,32 @@ const FlowCanvas = forwardRef((props, ref) => {
12430
12896
  useCanvasStore.getState().batchUpdateElements(updates);
12431
12897
  }, []);
12432
12898
  const unboundConnectorIdsRef = useRef(/* @__PURE__ */ new Set());
12899
+ const syncBoundTextNodes = useCallback(
12900
+ (el, newX, newY, newW, newH) => {
12901
+ var _a, _b;
12902
+ if (!el.boundElements || !stageRef.current) return;
12903
+ if (!CONTAINER_TYPES.has(el.type)) return;
12904
+ const elements2 = useCanvasStore.getState().elements;
12905
+ const shapeW = newW ?? el.width;
12906
+ const shapeH = newH ?? el.height;
12907
+ for (const be of el.boundElements) {
12908
+ if (be.type !== "text") continue;
12909
+ const txt = elements2.find((e) => e.id === be.id);
12910
+ if (!txt) continue;
12911
+ const textNode = stageRef.current.findOne("#" + be.id);
12912
+ if (!textNode) continue;
12913
+ const textNodeH = ((_a = textNode.height) == null ? void 0 : _a.call(textNode)) ?? txt.height;
12914
+ const pos = computeBoundTextPosition(
12915
+ { x: newX, y: newY, width: shapeW, height: shapeH },
12916
+ { height: textNodeH, verticalAlign: txt.verticalAlign }
12917
+ );
12918
+ (_b = textNode.width) == null ? void 0 : _b.call(textNode, pos.width);
12919
+ textNode.x(pos.x);
12920
+ textNode.y(pos.y);
12921
+ }
12922
+ },
12923
+ []
12924
+ );
12433
12925
  const handleElementDragMove = useCallback(
12434
12926
  (id, updates) => {
12435
12927
  if (readOnly) return;
@@ -12440,6 +12932,12 @@ const FlowCanvas = forwardRef((props, ref) => {
12440
12932
  const { elements: elements2, selectedIds: selectedIds2 } = useCanvasStore.getState();
12441
12933
  if (selectedIds2.length > MULTI_DRAG_STORE_SKIP_THRESHOLD) {
12442
12934
  isMultiDragSkippingRef.current = true;
12935
+ const el2 = elements2.find((e) => e.id === id);
12936
+ if (el2) {
12937
+ const nx = updates.x ?? el2.x;
12938
+ const ny = updates.y ?? el2.y;
12939
+ syncBoundTextNodes(el2, nx, ny);
12940
+ }
12443
12941
  return;
12444
12942
  }
12445
12943
  isMultiDragSkippingRef.current = false;
@@ -12466,12 +12964,19 @@ const FlowCanvas = forwardRef((props, ref) => {
12466
12964
  }
12467
12965
  if (!dragBatchRef.current) dragBatchRef.current = /* @__PURE__ */ new Map();
12468
12966
  dragBatchRef.current.set(id, updates);
12967
+ if (el) {
12968
+ const nx = updates.x ?? el.x;
12969
+ const ny = updates.y ?? el.y;
12970
+ const nw = updates.width;
12971
+ const nh = updates.height;
12972
+ syncBoundTextNodes(el, nx, ny, nw, nh);
12973
+ }
12469
12974
  if (!dragFlushScheduledRef.current) {
12470
12975
  dragFlushScheduledRef.current = true;
12471
12976
  queueMicrotask(flushDragBatch);
12472
12977
  }
12473
12978
  },
12474
- [readOnly, flushDragBatch]
12979
+ [readOnly, flushDragBatch, syncBoundTextNodes]
12475
12980
  );
12476
12981
  const handleDragSnap = useCallback(
12477
12982
  (id, bounds) => {
@@ -12503,39 +13008,11 @@ const FlowCanvas = forwardRef((props, ref) => {
12503
13008
  store2.batchUpdateElements(entries.map(([id, upd]) => ({ id, updates: upd })));
12504
13009
  setAlignGuides([]);
12505
13010
  const freshElements = useCanvasStore.getState().elements;
12506
- const elMap = /* @__PURE__ */ new Map();
12507
- for (const el of freshElements) elMap.set(el.id, el);
12508
- const connectorUpdates = [];
12509
- const processedConnectors = /* @__PURE__ */ new Set();
12510
- for (const [id] of entries) {
12511
- unboundConnectorIdsRef.current.delete(id);
12512
- const connectors = findConnectorsForElement(id, freshElements);
12513
- for (const conn of connectors) {
12514
- if (processedConnectors.has(conn.id)) continue;
12515
- processedConnectors.add(conn.id);
12516
- const freshConn = elMap.get(conn.id);
12517
- if (!freshConn) continue;
12518
- const recomputed = recomputeBoundPoints(freshConn, freshElements);
12519
- if (recomputed) connectorUpdates.push({ id: freshConn.id, updates: recomputed });
12520
- }
12521
- const el = elMap.get(id);
12522
- if ((el == null ? void 0 : el.boundElements) && ["rectangle", "ellipse", "diamond", "image"].includes(el.type)) {
12523
- const PADDING2 = 4;
12524
- for (const be of el.boundElements) {
12525
- if (be.type !== "text") continue;
12526
- const txt = elMap.get(be.id);
12527
- if (!txt) continue;
12528
- const tw = Math.max(20, el.width - PADDING2 * 2);
12529
- let ty;
12530
- if (txt.verticalAlign === "top") ty = el.y + PADDING2;
12531
- else if (txt.verticalAlign === "bottom") ty = el.y + el.height - txt.height - PADDING2;
12532
- else ty = el.y + (el.height - txt.height) / 2;
12533
- connectorUpdates.push({ id: be.id, updates: { x: el.x + PADDING2, y: ty, width: tw } });
12534
- }
12535
- }
12536
- }
12537
- if (connectorUpdates.length > 0) {
12538
- useCanvasStore.getState().batchUpdateElements(connectorUpdates);
13011
+ const movedIds = entries.map(([id]) => id);
13012
+ for (const id of movedIds) unboundConnectorIdsRef.current.delete(id);
13013
+ const { updates: syncUpdates } = syncAfterDrag(movedIds, freshElements);
13014
+ if (syncUpdates.length > 0) {
13015
+ useCanvasStore.getState().batchUpdateElements(syncUpdates);
12539
13016
  }
12540
13017
  useCanvasStore.getState().pushHistory();
12541
13018
  if (savedPixelRatioRef.current !== null) {
@@ -12560,46 +13037,22 @@ const FlowCanvas = forwardRef((props, ref) => {
12560
13037
  setAlignGuides([]);
12561
13038
  unboundConnectorIdsRef.current.delete(id);
12562
13039
  const freshElements = useCanvasStore.getState().elements;
13040
+ const { updates: syncUpdates } = syncAfterDrag([id], freshElements);
13041
+ for (const su of syncUpdates) updateElement(su.id, su.updates);
12563
13042
  const elMap = /* @__PURE__ */ new Map();
12564
- for (const e of freshElements) elMap.set(e.id, e);
12565
- const connectors = findConnectorsForElement(id, freshElements);
12566
- for (const conn of connectors) {
12567
- const freshConn = elMap.get(conn.id);
12568
- if (!freshConn) continue;
12569
- const recomputed = recomputeBoundPoints(freshConn, freshElements);
12570
- if (recomputed) updateElement(freshConn.id, recomputed);
12571
- }
13043
+ for (const e of useCanvasStore.getState().elements) elMap.set(e.id, e);
12572
13044
  const el = elMap.get(id);
12573
- if ((el == null ? void 0 : el.boundElements) && ["rectangle", "ellipse", "diamond", "image"].includes(el.type)) {
12574
- const PADDING2 = 4;
12575
- for (const be of el.boundElements) {
12576
- if (be.type !== "text") continue;
12577
- const txt = elMap.get(be.id);
12578
- if (!txt) continue;
12579
- const tw = Math.max(20, el.width - PADDING2 * 2);
12580
- let ty;
12581
- if (txt.verticalAlign === "top") ty = el.y + PADDING2;
12582
- else if (txt.verticalAlign === "bottom") ty = el.y + el.height - txt.height - PADDING2;
12583
- else ty = el.y + (el.height - txt.height) / 2;
12584
- updateElement(be.id, { x: el.x + PADDING2, y: ty, width: tw });
12585
- }
12586
- }
12587
13045
  if ((el == null ? void 0 : el.type) === "text") {
12588
13046
  const txt = el;
12589
13047
  if (txt.containerId) {
12590
13048
  const ctr = elMap.get(txt.containerId);
12591
- if (ctr && ["rectangle", "ellipse", "diamond", "image"].includes(ctr.type)) {
12592
- const PADDING2 = 4;
12593
- const minH = txt.height + PADDING2 * 2;
13049
+ if (ctr && CONTAINER_TYPES.has(ctr.type)) {
13050
+ const minH = txt.height + BOUND_TEXT_PADDING * 2;
12594
13051
  if (ctr.height < minH) {
12595
13052
  updateElement(ctr.id, { height: minH });
12596
13053
  const updatedElements = useCanvasStore.getState().elements;
12597
- for (const c of findConnectorsForElement(ctr.id, updatedElements)) {
12598
- const fc = updatedElements.find((e) => e.id === c.id);
12599
- if (!fc) continue;
12600
- const r = recomputeBoundPoints(fc, updatedElements);
12601
- if (r) updateElement(fc.id, r);
12602
- }
13054
+ const { updates: resizeSync } = syncAfterDrag([ctr.id], updatedElements);
13055
+ for (const su of resizeSync) updateElement(su.id, su.updates);
12603
13056
  }
12604
13057
  }
12605
13058
  }
@@ -12630,41 +13083,15 @@ const FlowCanvas = forwardRef((props, ref) => {
12630
13083
  }));
12631
13084
  store2.batchUpdateElements(posUpdates);
12632
13085
  const freshElements = useCanvasStore.getState().elements;
12633
- const elMap = /* @__PURE__ */ new Map();
12634
- for (const el of freshElements) elMap.set(el.id, el);
12635
13086
  const memberIds = new Set(members.map((m) => m.id));
12636
- const connectorUpdates = [];
12637
- const processedConnectors = /* @__PURE__ */ new Set();
12638
- for (const member of members) {
12639
- const connectors = findConnectorsForElement(member.id, freshElements);
12640
- for (const conn of connectors) {
12641
- if (processedConnectors.has(conn.id)) continue;
12642
- processedConnectors.add(conn.id);
12643
- if (memberIds.has(conn.id)) continue;
12644
- const freshConn = elMap.get(conn.id);
12645
- if (!freshConn) continue;
12646
- const recomputed = recomputeBoundPoints(freshConn, freshElements);
12647
- if (recomputed) connectorUpdates.push({ id: freshConn.id, updates: recomputed });
12648
- }
12649
- const el = elMap.get(member.id);
12650
- if ((el == null ? void 0 : el.boundElements) && ["rectangle", "ellipse", "diamond", "image"].includes(el.type)) {
12651
- const PADDING2 = 4;
12652
- for (const be of el.boundElements) {
12653
- if (be.type !== "text") continue;
12654
- if (memberIds.has(be.id)) continue;
12655
- const txt = elMap.get(be.id);
12656
- if (!txt) continue;
12657
- const tw = Math.max(20, el.width - PADDING2 * 2);
12658
- let ty;
12659
- if (txt.verticalAlign === "top") ty = el.y + PADDING2;
12660
- else if (txt.verticalAlign === "bottom") ty = el.y + el.height - txt.height - PADDING2;
12661
- else ty = el.y + (el.height - txt.height) / 2;
12662
- connectorUpdates.push({ id: be.id, updates: { x: el.x + PADDING2, y: ty, width: tw } });
12663
- }
12664
- }
12665
- }
12666
- if (connectorUpdates.length > 0) {
12667
- useCanvasStore.getState().batchUpdateElements(connectorUpdates);
13087
+ const { updates: syncUpdates } = syncAfterDrag(
13088
+ memberIds,
13089
+ freshElements,
13090
+ memberIds
13091
+ // skip group-internal connectors & text (already moved)
13092
+ );
13093
+ if (syncUpdates.length > 0) {
13094
+ useCanvasStore.getState().batchUpdateElements(syncUpdates);
12668
13095
  }
12669
13096
  store2.pushHistory();
12670
13097
  },
@@ -12695,24 +13122,12 @@ const FlowCanvas = forwardRef((props, ref) => {
12695
13122
  }
12696
13123
  const textId = generateId();
12697
13124
  const conn = el;
12698
- const pts = conn.points;
12699
- const startPt = { x: pts[0], y: pts[1] };
12700
- const endPt = { x: pts[pts.length - 2], y: pts[pts.length - 1] };
12701
- let midX, midY;
12702
- if (conn.lineType === "curved") {
12703
- const cp = computeCurveControlPoint(startPt, endPt, conn.curvature ?? CURVE_RATIO);
12704
- const mid = quadBezierAt(startPt, cp, endPt, 0.5);
12705
- midX = conn.x + mid.x;
12706
- midY = conn.y + mid.y;
12707
- } else {
12708
- midX = conn.x + (startPt.x + endPt.x) / 2;
12709
- midY = conn.y + (startPt.y + endPt.y) / 2;
12710
- }
13125
+ const labelPos = computeConnectorLabelPosition(conn, 100, 30);
12711
13126
  const textEl = {
12712
13127
  id: textId,
12713
13128
  type: "text",
12714
- x: midX,
12715
- y: midY,
13129
+ x: labelPos.x,
13130
+ y: labelPos.y,
12716
13131
  width: 100,
12717
13132
  height: 30,
12718
13133
  rotation: 0,
@@ -12738,7 +13153,7 @@ const FlowCanvas = forwardRef((props, ref) => {
12738
13153
  if (el.type === "rectangle" || el.type === "ellipse" || el.type === "diamond" || el.type === "image") {
12739
13154
  const existingTextBinding = (_b = el.boundElements) == null ? void 0 : _b.find((be) => be.type === "text");
12740
13155
  if (existingTextBinding) {
12741
- setSel([existingTextBinding.id]);
13156
+ setSel([existingTextBinding.id, id]);
12742
13157
  setAutoEditTextId(existingTextBinding.id);
12743
13158
  return;
12744
13159
  }
@@ -12766,7 +13181,7 @@ const FlowCanvas = forwardRef((props, ref) => {
12766
13181
  update(id, {
12767
13182
  boundElements: [...currentBound, { id: textId, type: "text" }]
12768
13183
  });
12769
- setSel([textId]);
13184
+ setSel([textId, id]);
12770
13185
  setAutoEditTextId(textId);
12771
13186
  return;
12772
13187
  }
@@ -12817,11 +13232,17 @@ const FlowCanvas = forwardRef((props, ref) => {
12817
13232
  (id, isEmpty) => {
12818
13233
  setEditingTextId(null);
12819
13234
  setAutoEditTextId(null);
13235
+ const {
13236
+ elements: els,
13237
+ selectedIds: currentSel,
13238
+ setSelectedIds: setSel,
13239
+ updateElement: update,
13240
+ deleteElements: del
13241
+ } = useCanvasStore.getState();
13242
+ const textEl = els.find((e) => e.id === id);
13243
+ const containerId = (textEl == null ? void 0 : textEl.type) === "text" ? textEl.containerId : null;
12820
13244
  if (isEmpty) {
12821
- const { elements: els, updateElement: update, deleteElements: del, pushHistory: push } = useCanvasStore.getState();
12822
- const textEl = els.find((e) => e.id === id);
12823
- if ((textEl == null ? void 0 : textEl.type) === "text" && textEl.containerId) {
12824
- const containerId = textEl.containerId;
13245
+ if (containerId) {
12825
13246
  const container = els.find((e) => e.id === containerId);
12826
13247
  if (container == null ? void 0 : container.boundElements) {
12827
13248
  update(containerId, {
@@ -12834,6 +13255,11 @@ const FlowCanvas = forwardRef((props, ref) => {
12834
13255
  } else {
12835
13256
  useCanvasStore.getState().pushHistory();
12836
13257
  }
13258
+ if (containerId && currentSel.includes(containerId)) {
13259
+ setSel([containerId]);
13260
+ } else if (containerId) {
13261
+ setSel(currentSel.filter((sid) => sid !== id));
13262
+ }
12837
13263
  },
12838
13264
  [onElementDelete]
12839
13265
  );
@@ -12901,6 +13327,7 @@ const FlowCanvas = forwardRef((props, ref) => {
12901
13327
  );
12902
13328
  const contextMenuItems = useMemo(() => {
12903
13329
  var _a;
13330
+ if (!contextMenu) return [];
12904
13331
  const hasSelection = selectedIds.length > 0;
12905
13332
  const isMac = navigator.platform.includes("Mac");
12906
13333
  const mod = isMac ? "⌘" : "Ctrl+";
@@ -13202,6 +13629,7 @@ const FlowCanvas = forwardRef((props, ref) => {
13202
13629
  onMouseMove: handleMouseMove,
13203
13630
  onMouseUp: handleMouseUp,
13204
13631
  onWheel: handleWheel,
13632
+ onDragMove: handleStageDragMove,
13205
13633
  onDragEnd: handleStageDragEnd,
13206
13634
  onTouchStart: handleMouseDown,
13207
13635
  onTouchMove: handleMouseMove,
@@ -13263,6 +13691,10 @@ const FlowCanvas = forwardRef((props, ref) => {
13263
13691
  if (el.type === "line" || el.type === "arrow") return false;
13264
13692
  if (el.type === "text" && el.containerId) return false;
13265
13693
  if (sid === editingTextId) return false;
13694
+ if (editingTextId) {
13695
+ const editingEl = resolvedElementMap.get(editingTextId);
13696
+ if ((editingEl == null ? void 0 : editingEl.type) === "text" && editingEl.containerId === sid) return false;
13697
+ }
13266
13698
  return true;
13267
13699
  });
13268
13700
  if (transformableIds.length === 0) return null;
@@ -13378,6 +13810,16 @@ const FlowCanvas = forwardRef((props, ref) => {
13378
13810
  theme
13379
13811
  }
13380
13812
  )),
13813
+ renderAnnotation && /* @__PURE__ */ jsx(
13814
+ AnnotationsOverlay,
13815
+ {
13816
+ elements: resolvedElements,
13817
+ viewport,
13818
+ containerWidth: dimensions.width,
13819
+ containerHeight: dimensions.height,
13820
+ renderAnnotation
13821
+ }
13822
+ ),
13381
13823
  showStatusBar && /* @__PURE__ */ jsx(StatusBar, { theme })
13382
13824
  ]
13383
13825
  }
@@ -13385,9 +13827,17 @@ const FlowCanvas = forwardRef((props, ref) => {
13385
13827
  });
13386
13828
  FlowCanvas.displayName = "FlowCanvas";
13387
13829
  const StatusBar = React.memo(({ theme }) => {
13388
- const elementCount = useCanvasStore((s) => s.elements.length);
13830
+ const elementCount = useCanvasStore(
13831
+ (s) => s.elements.filter((el) => !(el.type === "text" && el.containerId)).length
13832
+ );
13389
13833
  const activeTool = useCanvasStore((s) => s.activeTool);
13390
- const selectedCount = useCanvasStore((s) => s.selectedIds.length);
13834
+ const selectedCount = useCanvasStore((s) => {
13835
+ const els = s.elements;
13836
+ return s.selectedIds.filter((id) => {
13837
+ const el = els.find((e) => e.id === id);
13838
+ return el && !(el.type === "text" && el.containerId);
13839
+ }).length;
13840
+ });
13391
13841
  return /* @__PURE__ */ jsxs(
13392
13842
  "div",
13393
13843
  {
@@ -13592,6 +14042,11 @@ function getSharedSpatialSoA() {
13592
14042
  }
13593
14043
  const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
13594
14044
  const BASE = DIGITS.length;
14045
+ function trimTrailingZeros(s) {
14046
+ let end = s.length;
14047
+ while (end > 0 && s[end - 1] === "0") end--;
14048
+ return end === s.length ? s : s.slice(0, end);
14049
+ }
13595
14050
  const SMALLEST_CHAR = DIGITS[0];
13596
14051
  const LARGEST_CHAR = DIGITS[BASE - 1];
13597
14052
  function midChar(a, b) {
@@ -13664,7 +14119,7 @@ function midpoint(a, b) {
13664
14119
  if (mid !== null) {
13665
14120
  return commonPrefix + mid;
13666
14121
  }
13667
- return commonPrefix + aPad[i] + incrementKey(aPad.slice(i + 1).replace(/0+$/, "") || SMALLEST_CHAR);
14122
+ return commonPrefix + aPad[i] + incrementKey(trimTrailingZeros(aPad.slice(i + 1)) || SMALLEST_CHAR);
13668
14123
  }
13669
14124
  return a + DIGITS[Math.floor(BASE / 2)];
13670
14125
  }
@@ -15298,6 +15753,11 @@ export {
15298
15753
  ExportWorkerManager,
15299
15754
  FILL_COLORS,
15300
15755
  FlowCanvas,
15756
+ LABEL_CORNER,
15757
+ LABEL_LINE_HEIGHT,
15758
+ LABEL_MIN_WIDTH,
15759
+ LABEL_PADDING_H,
15760
+ LABEL_PADDING_V,
15301
15761
  LINE_TYPES,
15302
15762
  OperationLog,
15303
15763
  ROUGHNESS_CONFIGS,
@@ -15335,6 +15795,7 @@ export {
15335
15795
  computeElbowRoute,
15336
15796
  computeFixedPoint,
15337
15797
  computeImageElementDimensions,
15798
+ computePillSize,
15338
15799
  computeZoomToFit,
15339
15800
  createCollaborationProvider,
15340
15801
  createImageElement,
@@ -15399,6 +15860,7 @@ export {
15399
15860
  isValidFractionalIndex,
15400
15861
  isWorkerSupported,
15401
15862
  loadImage,
15863
+ measureLabelText,
15402
15864
  normalizeRect,
15403
15865
  onStatusChange,
15404
15866
  opAdd,