@zuilib/text-editor 0.7.1 → 0.7.2

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/index.js CHANGED
@@ -1543,2152 +1543,2159 @@ function moveGroup(shapes, selected, clickedId) {
1543
1543
  return new Map(shapes.filter((s) => ids.has(s.id)).map((s) => [s.id, s]));
1544
1544
  }
1545
1545
 
1546
- // src/drawing/canvas/PropertyBar.tsx
1547
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1548
- function Swatch({
1549
- color,
1550
- kind,
1551
- active,
1552
- onClick
1553
- }) {
1554
- const name = color === "transparent" ? "none" : color;
1555
- return /* @__PURE__ */ jsx2(
1556
- "button",
1557
- {
1558
- type: "button",
1559
- title: `${kind} ${name}`,
1560
- "aria-label": `${kind} color ${name}`,
1561
- "aria-pressed": active,
1562
- className: `zui-drawing-swatch ${color === "transparent" ? "is-transparent" : ""} ${active ? "is-active" : ""}`,
1563
- style: { backgroundColor: color },
1564
- onClick
1565
- }
1566
- );
1546
+ // src/drawing/skeleton.ts
1547
+ var COLOR_PRESETS = {
1548
+ /** No outline, light surface; text stays dark */
1549
+ plain: { stroke: "transparent", fill: "#f1f3f5" },
1550
+ /** Solid dark card; text renders light */
1551
+ black: { stroke: "#1e1e1e", fill: "#1e1e1e" },
1552
+ gray: { stroke: "#1e1e1e", fill: "transparent" },
1553
+ red: { stroke: "#e03131", fill: "#ffc9c9" },
1554
+ green: { stroke: "#2f9e44", fill: "#b2f2bb" },
1555
+ blue: { stroke: "#1971c2", fill: "#a5d8ff" },
1556
+ orange: { stroke: "#f08c00", fill: "#ffec99" },
1557
+ purple: { stroke: "#7048e8", fill: "#d0bfff" }
1558
+ };
1559
+ var COLOR_NAMES = Object.keys(COLOR_PRESETS);
1560
+ var TEXT_WRAP_WIDTH = 260;
1561
+ var SLOT_GAP = 6;
1562
+ var RANK_GAP = 90;
1563
+ var STACK_GAP = 40;
1564
+ var LAYOUT_ORIGIN = 32;
1565
+ var CANVAS_MARGIN = 32;
1566
+ var MIN_AUTO_CANVAS_HEIGHT = 120;
1567
+ var DEFAULT_STROKE2 = "#1e1e1e";
1568
+ var STROKE_WIDTH = 2;
1569
+ function isDrawingSkeleton(value) {
1570
+ return isRecord2(value) && Array.isArray(value.boxes);
1567
1571
  }
1568
- function OptionButton({
1569
- label,
1570
- active,
1571
- onClick,
1572
- children,
1573
- className
1574
- }) {
1575
- return /* @__PURE__ */ jsx2(
1576
- "button",
1577
- {
1578
- type: "button",
1579
- title: label,
1580
- "aria-label": label,
1581
- "aria-pressed": active,
1582
- className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
1583
- onClick,
1584
- children: /* @__PURE__ */ jsx2(Icon, { children })
1585
- }
1572
+ function parseDrawingSkeleton(json) {
1573
+ try {
1574
+ const parsed = JSON.parse(json);
1575
+ if (!isDrawingSkeleton(parsed)) return EMPTY_DRAWING;
1576
+ return expandSkeleton(normalizeSkeleton(parsed));
1577
+ } catch {
1578
+ return EMPTY_DRAWING;
1579
+ }
1580
+ }
1581
+ function expandSkeleton(skeleton) {
1582
+ const boxes = dedupeById(skeleton.boxes);
1583
+ const boxTypes = new Map(boxes.map((box) => [box.id, box.type ?? "rect"]));
1584
+ const sizes = new Map(boxes.map((box) => [box.id, measureBox(box, boxTypes.get(box.id))]));
1585
+ const boxIds = new Set(boxTypes.keys());
1586
+ const connectors = (skeleton.connectors ?? []).filter(
1587
+ (c) => boxIds.has(endId(c.from)) && boxIds.has(endId(c.to)) && endId(c.from) !== endId(c.to)
1588
+ );
1589
+ const positions = layoutBoxes(boxes, sizes, connectors, skeleton.direction ?? "right");
1590
+ const boxShapes = boxes.map(
1591
+ (box) => expandBox(box, boxTypes.get(box.id), sizes.get(box.id), positions.get(box.id))
1586
1592
  );
1593
+ const centers = new Map(boxShapes.map((shape) => [shape.id, shapeCenter(shape)]));
1594
+ const used = new Set(boxIds);
1595
+ const nextId = (prefix) => {
1596
+ let i = used.size + 1;
1597
+ while (used.has(`${prefix}${i}`)) i += 1;
1598
+ const id = `${prefix}${i}`;
1599
+ used.add(id);
1600
+ return id;
1601
+ };
1602
+ const connectorShapes = connectors.map((c) => expandConnector(c, centers, nextId("c")));
1603
+ const textShapes = (skeleton.texts ?? []).map((t) => expandText(t, nextId("t")));
1604
+ const shapes = resolveBindings([...boxShapes, ...connectorShapes, ...textShapes]);
1605
+ const bottom = shapes.reduce((max, shape) => {
1606
+ const b = bbox(shape);
1607
+ return Math.max(max, b.y + b.h);
1608
+ }, 0);
1609
+ return normalizeDrawingData({
1610
+ version: 2,
1611
+ ...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
1612
+ canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
1613
+ ...skeleton.width !== void 0 ? { width: skeleton.width } : {},
1614
+ shapes
1615
+ });
1587
1616
  }
1588
- function PropertyBar({
1589
- selection,
1590
- showFill,
1591
- stroke,
1592
- fill,
1593
- onStroke,
1594
- onFill,
1595
- onRouting,
1596
- onDirection,
1597
- onDelete
1598
- }) {
1599
- const connectors = selection.filter((s) => isConnectorType(s.type));
1600
- const arrows = selection.filter((s) => s.type === "arrow");
1601
- const allElbow = connectors.length > 0 && connectors.every((s) => s.routing === "elbow");
1602
- const allStraight = connectors.length > 0 && connectors.every((s) => s.routing !== "elbow");
1603
- const allTwoWay = arrows.length > 0 && arrows.every((s) => s.bidirectional);
1604
- const allOneWay = arrows.length > 0 && arrows.every((s) => !s.bidirectional);
1605
- const showFillSwatches = showFill || selection.some((s) => isBoxType(s.type));
1606
- return /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props", onPointerDown: (e) => e.stopPropagation(), children: [
1607
- /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Stroke color", children: [
1608
- /* @__PURE__ */ jsx2("span", { className: "zui-drawing-swatch-label", children: "Stroke" }),
1609
- STROKE_COLORS.map((color) => /* @__PURE__ */ jsx2(
1610
- Swatch,
1611
- {
1612
- color,
1613
- kind: "Stroke",
1614
- active: stroke === color,
1615
- onClick: () => onStroke(color)
1616
- },
1617
- color
1618
- ))
1619
- ] }),
1620
- showFillSwatches && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Fill color", children: [
1621
- /* @__PURE__ */ jsx2("span", { className: "zui-drawing-swatch-label", children: "Fill" }),
1622
- FILL_COLORS.map((color) => /* @__PURE__ */ jsx2(
1623
- Swatch,
1624
- {
1625
- color,
1626
- kind: "Fill",
1627
- active: fill === color,
1628
- onClick: () => onFill(color)
1629
- },
1630
- color
1631
- ))
1632
- ] }),
1633
- connectors.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Connector routing", children: [
1634
- /* @__PURE__ */ jsx2(OptionButton, { label: "Straight connector", active: allStraight, onClick: () => onRouting(false), children: UI_ICONS.straight }),
1635
- /* @__PURE__ */ jsx2(OptionButton, { label: "Elbow connector (auto-routed)", active: allElbow, onClick: () => onRouting(true), children: UI_ICONS.elbow })
1636
- ] }),
1637
- arrows.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Arrow direction", children: [
1638
- /* @__PURE__ */ jsx2(OptionButton, { label: "One-way arrow", active: allOneWay, onClick: () => onDirection(false), children: UI_ICONS.oneWay }),
1639
- /* @__PURE__ */ jsx2(OptionButton, { label: "Two-way arrow", active: allTwoWay, onClick: () => onDirection(true), children: UI_ICONS.twoWay })
1640
- ] }),
1641
- selection.length > 0 && /* @__PURE__ */ jsx2("div", { className: "zui-drawing-props-group", children: /* @__PURE__ */ jsx2(
1642
- OptionButton,
1643
- {
1644
- label: selection.length > 1 ? `Delete ${selection.length} shapes` : "Delete shape",
1645
- className: "zui-drawing-tool-danger",
1646
- onClick: onDelete,
1647
- children: UI_ICONS.trash
1648
- }
1649
- ) })
1650
- ] });
1617
+ function measureBox(box, type) {
1618
+ const def = BOX_DEFINITIONS[type];
1619
+ const probe = {
1620
+ id: "",
1621
+ type,
1622
+ x: 0,
1623
+ y: 0,
1624
+ w: def.defaultSize.w,
1625
+ h: def.defaultSize.h,
1626
+ stroke: "",
1627
+ fill: "",
1628
+ strokeWidth: 0
1629
+ };
1630
+ const area = def.textArea(probe);
1631
+ const ratioW = area.w / def.defaultSize.w;
1632
+ const ratioH = area.h / def.defaultSize.h;
1633
+ const wrapWidth = box.w !== void 0 ? Math.max(20, box.w * ratioW) : TEXT_WRAP_WIDTH;
1634
+ const slots = def.textSlots.flatMap((slot) => {
1635
+ const size = slotSize(box, slot, wrapWidth);
1636
+ return size ? [size] : [];
1637
+ });
1638
+ const innerW = slots.reduce((max, s) => Math.max(max, s.w), 0);
1639
+ const innerH = slots.reduce((sum, s) => sum + s.h, 0) + Math.max(0, slots.length - 1) * SLOT_GAP;
1640
+ return {
1641
+ w: box.w ?? Math.max(def.defaultSize.w, Math.ceil(innerW / ratioW)),
1642
+ h: box.h ?? Math.max(def.defaultSize.h, Math.ceil(innerH / ratioH))
1643
+ };
1651
1644
  }
1652
-
1653
- // src/drawing/canvas/SelectionOverlay.tsx
1654
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1655
- var HANDLE = 7;
1656
- function Handle({
1657
- x,
1658
- y,
1659
- cursor,
1660
- square,
1661
- onPointerDown,
1662
- title
1663
- }) {
1664
- const props = {
1665
- className: "zui-drawing-handle",
1666
- strokeWidth: 1.5,
1667
- style: { cursor },
1668
- onPointerDown
1645
+ function slotSize(box, slot, wrapWidth) {
1646
+ const value = box[slot];
1647
+ if (!value) return null;
1648
+ const fontSize = slot === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
1649
+ return textBoxSize(wrapText(value, wrapWidth, fontSize).join("\n"), fontSize);
1650
+ }
1651
+ function layoutBoxes(boxes, sizes, connectors, direction) {
1652
+ const positions = /* @__PURE__ */ new Map();
1653
+ const auto = boxes.filter((box) => box.x === void 0 || box.y === void 0);
1654
+ const autoIds = new Set(auto.map((box) => box.id));
1655
+ const edges = connectors.map((c) => ({ from: endId(c.from), to: endId(c.to) })).filter((e) => e.from !== e.to && autoIds.has(e.from) && autoIds.has(e.to));
1656
+ const ranks = rankNodes(
1657
+ auto.map((box) => box.id),
1658
+ acyclicEdges(
1659
+ auto.map((box) => box.id),
1660
+ edges
1661
+ )
1662
+ );
1663
+ const rows = [];
1664
+ for (const box of auto) {
1665
+ const rank = ranks.get(box.id) ?? 0;
1666
+ (rows[rank] ?? (rows[rank] = [])).push(box.id);
1667
+ }
1668
+ const main = direction === "right" ? "w" : "h";
1669
+ const cross = direction === "right" ? "h" : "w";
1670
+ const rowMain = rows.map((row) => row.reduce((max, id) => Math.max(max, sizes.get(id)[main]), 0));
1671
+ const rowCross = rows.map(
1672
+ (row) => row.reduce((sum, id) => sum + sizes.get(id)[cross], 0) + Math.max(0, row.length - 1) * STACK_GAP
1673
+ );
1674
+ const maxCross = rowCross.reduce((max, c) => Math.max(max, c), 0);
1675
+ let mainOffset = LAYOUT_ORIGIN;
1676
+ rows.forEach((row, rank) => {
1677
+ let crossOffset = LAYOUT_ORIGIN + (maxCross - rowCross[rank]) / 2;
1678
+ for (const id of row) {
1679
+ const size = sizes.get(id);
1680
+ const mainPos = Math.round(mainOffset + (rowMain[rank] - size[main]) / 2);
1681
+ const crossPos = Math.round(crossOffset);
1682
+ positions.set(
1683
+ id,
1684
+ direction === "right" ? { x: mainPos, y: crossPos } : { x: crossPos, y: mainPos }
1685
+ );
1686
+ crossOffset += size[cross] + STACK_GAP;
1687
+ }
1688
+ mainOffset += rowMain[rank] + RANK_GAP;
1689
+ });
1690
+ for (const box of boxes) {
1691
+ const computed = positions.get(box.id) ?? { x: LAYOUT_ORIGIN, y: LAYOUT_ORIGIN };
1692
+ positions.set(box.id, { x: box.x ?? computed.x, y: box.y ?? computed.y });
1693
+ }
1694
+ return positions;
1695
+ }
1696
+ function acyclicEdges(ids, edges) {
1697
+ const outgoing = /* @__PURE__ */ new Map();
1698
+ for (const edge of edges) (outgoing.get(edge.from) ?? outgoing.set(edge.from, []).get(edge.from)).push(edge);
1699
+ const state = /* @__PURE__ */ new Map();
1700
+ const kept = [];
1701
+ const visit = (id) => {
1702
+ state.set(id, "visiting");
1703
+ for (const edge of outgoing.get(id) ?? []) {
1704
+ const target = state.get(edge.to);
1705
+ if (target === "visiting") continue;
1706
+ kept.push(edge);
1707
+ if (target === void 0) visit(edge.to);
1708
+ }
1709
+ state.set(id, "done");
1669
1710
  };
1670
- return square ? /* @__PURE__ */ jsx3("rect", { ...props, x: x - HANDLE / 2, y: y - HANDLE / 2, width: HANDLE, height: HANDLE, rx: 2, children: title && /* @__PURE__ */ jsx3("title", { children: title }) }) : /* @__PURE__ */ jsx3("circle", { ...props, cx: x, cy: y, r: HANDLE / 2 + 0.5, children: title && /* @__PURE__ */ jsx3("title", { children: title }) });
1711
+ for (const id of ids) if (!state.has(id)) visit(id);
1712
+ return kept;
1671
1713
  }
1672
- function SelectionOverlay({
1673
- shape,
1674
- points,
1675
- onHandlePointerDown,
1676
- onWaypointAdd,
1677
- onWaypointRemove
1678
- }) {
1679
- if (isConnectorType(shape.type)) {
1680
- const waypoints = shape.waypoints ?? [];
1681
- const isZ = shape.routing === "elbow" && !waypoints.length && points.length === 4;
1682
- const elbowMid = isZ ? { x: (points[1].x + points[2].x) / 2, y: (points[1].y + points[2].y) / 2 } : null;
1683
- const elbowVertical = isZ && Math.abs(points[1].x - points[2].x) < 0.01;
1684
- const ghosts = points.slice(0, -1).flatMap((p, i) => {
1685
- const q = points[i + 1];
1686
- if (Math.hypot(q.x - p.x, q.y - p.y) < 28) return [];
1687
- if (isZ && i === 1) return [];
1688
- return [{ index: i, x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }];
1689
- });
1690
- return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
1691
- ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx3(
1692
- "circle",
1693
- {
1694
- className: "zui-drawing-handle zui-drawing-ghost",
1695
- cx: x,
1696
- cy: y,
1697
- r: 4.5,
1698
- strokeWidth: 1.5,
1699
- strokeDasharray: "2 2",
1700
- style: { cursor: "copy" },
1701
- onPointerDown: (e) => onWaypointAdd(e, shape.id, index, { x, y }),
1702
- children: /* @__PURE__ */ jsx3("title", { children: "Drag to add a bend" })
1703
- },
1704
- `ghost-${index}`
1705
- )),
1706
- waypoints.map((p, i) => /* @__PURE__ */ jsx3(
1707
- "g",
1708
- {
1709
- onDoubleClick: (e) => {
1710
- e.stopPropagation();
1711
- onWaypointRemove(shape.id, i);
1712
- },
1713
- children: /* @__PURE__ */ jsx3(
1714
- Handle,
1715
- {
1716
- x: p.x,
1717
- y: p.y,
1718
- square: true,
1719
- cursor: "move",
1720
- title: "Drag to move, double-click to remove",
1721
- onPointerDown: (e) => onHandlePointerDown(e, { mode: "waypoint", id: shape.id, index: i })
1722
- }
1723
- )
1724
- },
1725
- `wp-${i}`
1726
- )),
1727
- elbowMid && /* @__PURE__ */ jsx3(
1728
- Handle,
1729
- {
1730
- x: elbowMid.x,
1731
- y: elbowMid.y,
1732
- square: true,
1733
- cursor: elbowVertical ? "ew-resize" : "ns-resize",
1734
- title: "Slide the middle segment",
1735
- onPointerDown: (e) => onHandlePointerDown(e, {
1736
- mode: "elbow",
1737
- id: shape.id,
1738
- a: points[0],
1739
- d: points[3],
1740
- vertical: elbowVertical
1741
- })
1742
- }
1743
- ),
1744
- ["start", "end"].map((end) => {
1745
- const p = end === "start" ? points[0] : points[points.length - 1];
1746
- return /* @__PURE__ */ jsx3(
1747
- Handle,
1748
- {
1749
- x: p.x,
1750
- y: p.y,
1751
- cursor: "crosshair",
1752
- onPointerDown: (e) => onHandlePointerDown(e, { mode: "endpoint", id: shape.id, end, orig: shape })
1753
- },
1754
- end
1755
- );
1756
- })
1757
- ] });
1758
- }
1759
- const b = bbox(shape);
1760
- const pad = 5;
1761
- const corners = [
1762
- { corner: "nw", x: b.x - pad, y: b.y - pad },
1763
- { corner: "ne", x: b.x + b.w + pad, y: b.y - pad },
1764
- { corner: "sw", x: b.x - pad, y: b.y + b.h + pad },
1765
- { corner: "se", x: b.x + b.w + pad, y: b.y + b.h + pad }
1766
- ];
1767
- const resizable = shape.type !== "text";
1768
- return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
1769
- /* @__PURE__ */ jsx3(SelectionFrame, { rect: b, pad }),
1770
- resizable && corners.map(({ corner, x, y }) => /* @__PURE__ */ jsx3(
1771
- Handle,
1772
- {
1773
- x,
1774
- y,
1775
- square: true,
1776
- cursor: corner === "nw" || corner === "se" ? "nwse-resize" : "nesw-resize",
1777
- onPointerDown: (e) => onHandlePointerDown(e, { mode: "resize", id: shape.id, corner, orig: shape })
1778
- },
1779
- corner
1780
- ))
1781
- ] });
1714
+ function rankNodes(ids, edges) {
1715
+ const incoming = /* @__PURE__ */ new Map();
1716
+ for (const edge of edges) (incoming.get(edge.to) ?? incoming.set(edge.to, []).get(edge.to)).push(edge.from);
1717
+ const ranks = /* @__PURE__ */ new Map();
1718
+ const rankOf = (id) => {
1719
+ const known = ranks.get(id);
1720
+ if (known !== void 0) return known;
1721
+ const rank = (incoming.get(id) ?? []).reduce((max, from) => Math.max(max, rankOf(from) + 1), 0);
1722
+ ranks.set(id, rank);
1723
+ return rank;
1724
+ };
1725
+ for (const id of ids) rankOf(id);
1726
+ return ranks;
1782
1727
  }
1783
- function SelectionFrame({ rect, pad }) {
1784
- return /* @__PURE__ */ jsx3(
1785
- "rect",
1786
- {
1787
- x: rect.x - pad,
1788
- y: rect.y - pad,
1789
- width: rect.w + pad * 2,
1790
- height: rect.h + pad * 2,
1791
- rx: 6,
1792
- fill: "none",
1793
- stroke: "currentColor",
1794
- strokeWidth: 1,
1795
- strokeDasharray: "5 4",
1796
- pointerEvents: "none"
1797
- }
1798
- );
1728
+ function expandBox(box, type, size, position) {
1729
+ const preset = box.color ? COLOR_PRESETS[box.color] : void 0;
1730
+ const fill = box.fill ?? preset?.fill ?? BOX_DEFINITIONS[type].defaultFill ?? "transparent";
1731
+ return {
1732
+ id: box.id,
1733
+ type,
1734
+ x: position.x,
1735
+ y: position.y,
1736
+ w: size.w,
1737
+ h: size.h,
1738
+ stroke: box.stroke ?? preset?.stroke ?? DEFAULT_STROKE2,
1739
+ fill,
1740
+ strokeWidth: STROKE_WIDTH,
1741
+ ...box.text ? { text: box.text } : {},
1742
+ ...box.label ? { label: box.label } : {},
1743
+ ...box.footer ? { footer: box.footer } : {}
1744
+ };
1799
1745
  }
1800
- function GroupSelectionOverlay({
1801
- shapes,
1802
- paths
1803
- }) {
1804
- if (!shapes.length) return null;
1805
- const rects = shapes.map((s) => {
1806
- if (!isConnectorType(s.type)) return bbox(s);
1807
- const pts = paths.get(s.id) ?? [];
1808
- const xs = pts.map((p) => p.x);
1809
- const ys = pts.map((p) => p.y);
1810
- const x = Math.min(...xs);
1811
- const y = Math.min(...ys);
1812
- return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
1813
- });
1814
- return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", pointerEvents: "none", children: [
1815
- rects.map((r, i) => /* @__PURE__ */ jsx3(
1816
- "rect",
1817
- {
1818
- x: r.x - 3,
1819
- y: r.y - 3,
1820
- width: r.w + 6,
1821
- height: r.h + 6,
1822
- rx: 4,
1823
- fill: "none",
1824
- stroke: "currentColor",
1825
- strokeWidth: 1,
1826
- opacity: 0.45
1827
- },
1828
- shapes[i].id
1829
- )),
1830
- /* @__PURE__ */ jsx3(SelectionFrame, { rect: unionRects(rects), pad: 8 })
1831
- ] });
1746
+ function expandConnector(connector, centers, fallbackId) {
1747
+ const type = connector.type ?? "arrow";
1748
+ const from = centers.get(endId(connector.from));
1749
+ const to = centers.get(endId(connector.to));
1750
+ return {
1751
+ id: connector.id ?? fallbackId,
1752
+ type,
1753
+ x: from.x,
1754
+ y: from.y,
1755
+ w: to.x - from.x,
1756
+ h: to.y - from.y,
1757
+ stroke: connector.stroke ?? (connector.color ? COLOR_PRESETS[connector.color].stroke : DEFAULT_STROKE2),
1758
+ fill: "transparent",
1759
+ strokeWidth: STROKE_WIDTH,
1760
+ startBinding: toBinding(connector.from),
1761
+ endBinding: toBinding(connector.to),
1762
+ ...connector.text ? { text: connector.text } : {},
1763
+ ...type === "arrow" && connector.bidirectional ? { bidirectional: true } : {},
1764
+ ...connector.routing === "elbow" ? { routing: "elbow" } : {}
1765
+ };
1832
1766
  }
1833
- function MarqueeOverlay({ rect }) {
1834
- return /* @__PURE__ */ jsx3(
1835
- "rect",
1836
- {
1837
- className: "zui-drawing-selection",
1838
- x: rect.x,
1839
- y: rect.y,
1840
- width: rect.w,
1841
- height: rect.h,
1842
- rx: 3,
1843
- fill: "currentColor",
1844
- fillOpacity: 0.08,
1845
- stroke: "currentColor",
1846
- strokeWidth: 1,
1847
- pointerEvents: "none"
1848
- }
1849
- );
1767
+ function expandText(text, fallbackId) {
1768
+ const size = textBoxSize(text.text);
1769
+ return {
1770
+ id: text.id ?? fallbackId,
1771
+ type: "text",
1772
+ x: text.x,
1773
+ y: text.y,
1774
+ w: size.w,
1775
+ h: size.h,
1776
+ stroke: text.stroke ?? (text.color ? COLOR_PRESETS[text.color].stroke : DEFAULT_STROKE2),
1777
+ fill: "transparent",
1778
+ strokeWidth: STROKE_WIDTH,
1779
+ text: text.text
1780
+ };
1850
1781
  }
1851
-
1852
- // src/drawing/shapes/render.tsx
1853
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1854
- var n = (v) => Number.isInteger(v) ? String(v) : v.toFixed(2);
1855
- function BoxGeometry({
1856
- shape,
1857
- stroke
1858
- }) {
1782
+ function endId(end) {
1783
+ return typeof end === "string" ? end : end.id;
1784
+ }
1785
+ function toBinding(end) {
1786
+ if (typeof end === "string" || !end.side) return { id: endId(end) };
1787
+ return { id: end.id, fixedPoint: SIDE_FIXED_POINTS[end.side] };
1788
+ }
1789
+ function shapeCenter(shape) {
1859
1790
  const b = bbox(shape);
1860
- const fill = shape.fill;
1861
- switch (shape.type) {
1862
- case "rect":
1863
- return /* @__PURE__ */ jsx4(
1864
- "rect",
1865
- {
1866
- ...stroke,
1867
- x: b.x,
1868
- y: b.y,
1869
- width: b.w,
1870
- height: b.h,
1871
- rx: Math.min(8, b.w / 4, b.h / 4),
1872
- fill
1873
- }
1874
- );
1875
- case "ellipse":
1876
- return /* @__PURE__ */ jsx4(
1877
- "ellipse",
1878
- {
1879
- ...stroke,
1880
- cx: b.x + b.w / 2,
1881
- cy: b.y + b.h / 2,
1882
- rx: b.w / 2,
1883
- ry: b.h / 2,
1884
- fill
1885
- }
1886
- );
1887
- case "diamond":
1888
- return /* @__PURE__ */ jsx4(
1889
- "polygon",
1890
- {
1891
- ...stroke,
1892
- points: `${n(b.x + b.w / 2)},${n(b.y)} ${n(b.x + b.w)},${n(b.y + b.h / 2)} ${n(b.x + b.w / 2)},${n(b.y + b.h)} ${n(b.x)},${n(b.y + b.h / 2)}`,
1893
- fill
1894
- }
1895
- );
1896
- case "note": {
1897
- const f = NOTE_FOLD(b);
1898
- const r = b.x + b.w;
1899
- const btm = b.y + b.h;
1900
- return /* @__PURE__ */ jsxs4("g", { children: [
1901
- /* @__PURE__ */ jsx4(
1902
- "path",
1903
- {
1904
- ...stroke,
1905
- d: `M ${n(b.x)} ${n(b.y)} H ${n(r)} V ${n(btm - f)} L ${n(r - f)} ${n(btm)} H ${n(b.x)} Z`,
1906
- fill
1907
- }
1908
- ),
1909
- /* @__PURE__ */ jsx4(
1910
- "path",
1911
- {
1912
- ...stroke,
1913
- d: `M ${n(r - f)} ${n(btm)} V ${n(btm - f)} H ${n(r)}`,
1914
- fill: "rgba(0,0,0,0.08)"
1915
- }
1916
- )
1917
- ] });
1918
- }
1919
- case "cylinder": {
1920
- const ry = CYLINDER_RY(b);
1921
- const rx = b.w / 2;
1922
- const cx = b.x + rx;
1923
- return /* @__PURE__ */ jsxs4("g", { children: [
1924
- /* @__PURE__ */ jsx4(
1925
- "path",
1926
- {
1927
- ...stroke,
1928
- d: `M ${n(b.x)} ${n(b.y + ry)} V ${n(b.y + b.h - ry)} A ${n(rx)} ${n(ry)} 0 0 0 ${n(b.x + b.w)} ${n(b.y + b.h - ry)} V ${n(b.y + ry)}`,
1929
- fill
1930
- }
1931
- ),
1932
- /* @__PURE__ */ jsx4("ellipse", { ...stroke, cx, cy: b.y + ry, rx, ry, fill })
1933
- ] });
1934
- }
1935
- case "cloud": {
1936
- const p = (u, v) => `${n(b.x + u * b.w)} ${n(b.y + v * b.h)}`;
1937
- return /* @__PURE__ */ jsx4(
1938
- "path",
1939
- {
1940
- ...stroke,
1941
- d: `M ${p(0.22, 0.86)} C ${p(0.04, 0.88)} ${p(0, 0.58)} ${p(0.16, 0.5)} C ${p(0.08, 0.26)} ${p(0.3, 0.12)} ${p(0.42, 0.28)} C ${p(0.5, 0.02)} ${p(0.76, 0.04)} ${p(0.78, 0.3)} C ${p(0.98, 0.26)} ${p(1.04, 0.56)} ${p(0.88, 0.64)} C ${p(1, 0.8)} ${p(0.9, 0.9)} ${p(0.76, 0.86)} Z`,
1942
- fill
1943
- }
1944
- );
1945
- }
1946
- case "queue": {
1947
- const rx = QUEUE_RX(b);
1948
- const ry = b.h / 2;
1949
- return /* @__PURE__ */ jsxs4("g", { children: [
1950
- /* @__PURE__ */ jsx4(
1951
- "path",
1952
- {
1953
- ...stroke,
1954
- d: `M ${n(b.x + rx)} ${n(b.y)} H ${n(b.x + b.w - rx)} V ${n(b.y + b.h)} H ${n(b.x + rx)} A ${n(rx)} ${n(ry)} 0 0 1 ${n(b.x + rx)} ${n(b.y)} Z`,
1955
- fill
1956
- }
1957
- ),
1958
- /* @__PURE__ */ jsx4(
1959
- "ellipse",
1960
- {
1961
- ...stroke,
1962
- cx: b.x + b.w - rx,
1963
- cy: b.y + ry,
1964
- rx,
1965
- ry,
1966
- fill
1967
- }
1968
- )
1969
- ] });
1970
- }
1971
- case "actor": {
1972
- const figH = b.h * ACTOR_FIGURE_RATIO;
1973
- const cx = b.x + b.w / 2;
1974
- const r = Math.max(4, Math.min(figH * 0.16, b.w * 0.2));
1975
- const neck = b.y + r * 2;
1976
- const hip = b.y + figH * 0.62;
1977
- const armY = neck + figH * 0.12;
1978
- const reach = Math.min(b.w * 0.32, figH * 0.3);
1979
- return /* @__PURE__ */ jsxs4("g", { children: [
1980
- /* @__PURE__ */ jsx4("circle", { ...stroke, cx, cy: b.y + r, r, fill }),
1981
- /* @__PURE__ */ jsx4(
1982
- "path",
1983
- {
1984
- ...stroke,
1985
- fill: "none",
1986
- d: `M ${n(cx)} ${n(neck)} V ${n(hip)} M ${n(cx - reach)} ${n(armY)} H ${n(cx + reach)} M ${n(cx)} ${n(hip)} L ${n(cx - reach)} ${n(b.y + figH)} M ${n(cx)} ${n(hip)} L ${n(cx + reach)} ${n(b.y + figH)}`
1987
- }
1988
- )
1989
- ] });
1990
- }
1991
- default:
1992
- return null;
1993
- }
1791
+ return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
1994
1792
  }
1995
-
1996
- // src/drawing/canvas/ShapeView.tsx
1997
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1998
- import { createElement } from "react";
1999
- var CONNECTOR_FONT_SIZE = 12;
2000
- var CONNECTOR_LABEL_MAX_WIDTH = 160;
2001
- function slotLayout(shape) {
2002
- const def = boxDefinition(shape);
1793
+ function dedupeById(items) {
1794
+ const seen = /* @__PURE__ */ new Set();
1795
+ return items.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
1796
+ }
1797
+ function normalizeSkeleton(raw) {
1798
+ const value = raw;
1799
+ const boxes = value.boxes.flatMap((b) => {
1800
+ const box = normalizeBox(b);
1801
+ return box ? [box] : [];
1802
+ });
1803
+ const connectors = Array.isArray(value.connectors) ? value.connectors.flatMap((c) => {
1804
+ const connector = normalizeConnector(c);
1805
+ return connector ? [connector] : [];
1806
+ }) : [];
1807
+ const texts = Array.isArray(value.texts) ? value.texts.flatMap((t) => {
1808
+ const text = normalizeText(t);
1809
+ return text ? [text] : [];
1810
+ }) : [];
1811
+ const canvasWidth = finiteNumber(value.canvasWidth);
1812
+ const canvasHeight = finiteNumber(value.canvasHeight);
2003
1813
  return {
2004
- area: def ? def.textArea(shape) : bbox(shape),
2005
- slots: def ? def.textSlots : ["label", "text", "footer"]
1814
+ direction: value.direction === "down" ? "down" : "right",
1815
+ ...canvasWidth !== void 0 ? { canvasWidth } : {},
1816
+ ...canvasHeight !== void 0 ? { canvasHeight } : {},
1817
+ ...isBlockWidth(value.width) ? { width: value.width } : {},
1818
+ boxes,
1819
+ connectors,
1820
+ texts
2006
1821
  };
2007
1822
  }
2008
- function slotAt(shape, y) {
2009
- const { area, slots } = slotLayout(shape);
2010
- const rel = (y - area.y) / Math.max(area.h, 1);
2011
- if (rel < 0.3 && slots.includes("label")) return "label";
2012
- if (rel > 0.7 && slots.includes("footer")) return "footer";
2013
- return slots.includes("text") ? "text" : slots[0];
1823
+ function normalizeBox(value) {
1824
+ if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
1825
+ if (value.type !== void 0 && !(typeof value.type === "string" && isBoxType(value.type))) {
1826
+ return null;
1827
+ }
1828
+ return compact({
1829
+ id: value.id,
1830
+ type: value.type,
1831
+ label: nonEmptyString(value.label) ? value.label : void 0,
1832
+ text: nonEmptyString(value.text) ? value.text : void 0,
1833
+ footer: nonEmptyString(value.footer) ? value.footer : void 0,
1834
+ x: finiteNumber(value.x),
1835
+ y: finiteNumber(value.y),
1836
+ w: positiveNumber(value.w),
1837
+ h: positiveNumber(value.h),
1838
+ color: colorName(value.color),
1839
+ stroke: nonEmptyString(value.stroke) ? value.stroke : void 0,
1840
+ fill: nonEmptyString(value.fill) ? value.fill : void 0
1841
+ });
2014
1842
  }
2015
- var textStyle = { userSelect: "none" };
2016
- var hintStyle = { userSelect: "none", pointerEvents: "none" };
2017
- function BoxTexts({
2018
- shape,
2019
- hideField,
2020
- showHints
2021
- }) {
2022
- const { area, slots } = slotLayout(shape);
2023
- const cx = area.x + area.w / 2;
2024
- const wrapW = Math.max(area.w, 20);
2025
- const hints = showHints && area.h >= 56 && area.w >= 44;
2026
- const mainLines = wrapText(shape.text ?? "", wrapW, FONT_SIZE);
2027
- const labelLines = wrapText(shape.label ?? "", wrapW, SMALL_FONT_SIZE);
2028
- const footerLines = wrapText(shape.footer ?? "", wrapW, SMALL_FONT_SIZE);
2029
- const mainLineH = FONT_SIZE * LINE_HEIGHT;
2030
- const smallLineH = SMALL_FONT_SIZE * LINE_HEIGHT;
2031
- const mainStart = area.y + area.h / 2 - (mainLines.length - 1) * mainLineH / 2 + FONT_SIZE * 0.35;
2032
- const common = {
2033
- fill: shape.stroke,
2034
- textAnchor: "middle"
2035
- };
2036
- return /* @__PURE__ */ jsxs5("g", { children: [
2037
- slots.includes("label") && hideField !== "label" && (shape.label ? labelLines.map((line, i) => /* @__PURE__ */ createElement(
2038
- "text",
2039
- {
2040
- ...common,
2041
- key: i,
2042
- x: cx,
2043
- y: area.y + SMALL_FONT_SIZE + i * smallLineH,
2044
- fontSize: SMALL_FONT_SIZE,
2045
- fontWeight: 600,
2046
- letterSpacing: 0.3,
2047
- opacity: 0.85,
2048
- style: textStyle
2049
- },
2050
- line
2051
- )) : hints && /* @__PURE__ */ jsx5(
2052
- "text",
2053
- {
2054
- ...common,
2055
- x: cx,
2056
- y: area.y + SMALL_FONT_SIZE,
2057
- fontSize: SMALL_FONT_SIZE,
2058
- opacity: 0.3,
2059
- style: hintStyle,
2060
- children: "Label"
2061
- }
2062
- )),
2063
- slots.includes("text") && hideField !== "text" && (shape.text ? mainLines.map((line, i) => /* @__PURE__ */ createElement(
2064
- "text",
2065
- {
2066
- ...common,
2067
- key: i,
2068
- x: cx,
2069
- y: mainStart + i * mainLineH,
2070
- fontSize: FONT_SIZE,
2071
- style: textStyle
2072
- },
2073
- line
2074
- )) : hints && /* @__PURE__ */ jsx5(
2075
- "text",
2076
- {
2077
- ...common,
2078
- x: cx,
2079
- y: mainStart,
2080
- fontSize: FONT_SIZE,
2081
- opacity: 0.3,
2082
- style: hintStyle,
2083
- children: "Text"
2084
- }
2085
- )),
2086
- slots.includes("footer") && hideField !== "footer" && (shape.footer ? footerLines.map((line, i) => /* @__PURE__ */ createElement(
2087
- "text",
2088
- {
2089
- ...common,
2090
- key: i,
2091
- x: cx,
2092
- y: area.y + area.h - 3 - (footerLines.length - 1 - i) * smallLineH,
2093
- fontSize: SMALL_FONT_SIZE,
2094
- opacity: 0.65,
2095
- style: textStyle
2096
- },
2097
- line
2098
- )) : hints && /* @__PURE__ */ jsx5(
2099
- "text",
2100
- {
2101
- ...common,
2102
- x: cx,
2103
- y: area.y + area.h - 3,
2104
- fontSize: SMALL_FONT_SIZE,
2105
- opacity: 0.3,
2106
- style: hintStyle,
2107
- children: "Footer"
2108
- }
2109
- ))
2110
- ] });
1843
+ function normalizeConnector(value) {
1844
+ if (!isRecord2(value)) return null;
1845
+ const from = normalizeEnd(value.from);
1846
+ const to = normalizeEnd(value.to);
1847
+ if (!from || !to) return null;
1848
+ if (value.type !== void 0 && !(typeof value.type === "string" && isConnectorType(value.type))) {
1849
+ return null;
1850
+ }
1851
+ return compact({
1852
+ id: nonEmptyString(value.id) ? value.id : void 0,
1853
+ type: value.type,
1854
+ from,
1855
+ to,
1856
+ text: nonEmptyString(value.text) ? value.text : void 0,
1857
+ bidirectional: value.bidirectional === true ? true : void 0,
1858
+ routing: value.routing === "elbow" ? "elbow" : void 0,
1859
+ color: colorName(value.color),
1860
+ stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
1861
+ });
2111
1862
  }
2112
- function ConnectorLabel({
2113
- shape,
2114
- points
2115
- }) {
2116
- const text = shape.text ?? "";
2117
- const lines = wrapText(text, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
2118
- const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
2119
- const { x: midX, y: midY } = connectorMidpoint(points);
2120
- const lineH = CONNECTOR_FONT_SIZE * LINE_HEIGHT;
2121
- const startY = midY - (lines.length - 1) * lineH / 2 + CONNECTOR_FONT_SIZE * 0.35;
2122
- return /* @__PURE__ */ jsxs5("g", { children: [
2123
- /* @__PURE__ */ jsx5(
2124
- "rect",
2125
- {
2126
- x: midX - size.w / 2 - 5,
2127
- y: midY - size.h / 2 - 3,
2128
- width: size.w + 10,
2129
- height: size.h + 6,
2130
- rx: 5,
2131
- className: "zui-drawing-label-plate",
2132
- opacity: 0.94
2133
- }
2134
- ),
2135
- lines.map((line, i) => /* @__PURE__ */ jsx5(
2136
- "text",
2137
- {
2138
- x: midX,
2139
- y: startY + i * lineH,
2140
- fill: shape.stroke,
2141
- fontSize: CONNECTOR_FONT_SIZE,
2142
- textAnchor: "middle",
2143
- style: textStyle,
2144
- children: line
2145
- },
2146
- i
2147
- ))
2148
- ] });
1863
+ function normalizeEnd(value) {
1864
+ if (nonEmptyString(value)) return value;
1865
+ if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
1866
+ const side = value.side;
1867
+ return typeof side === "string" && side in SIDE_FIXED_POINTS ? { id: value.id, side } : { id: value.id };
2149
1868
  }
2150
- function ShapeView({
2151
- shape,
2152
- points,
2153
- hideField,
2154
- showHints
2155
- }) {
2156
- const stroke = {
2157
- stroke: shape.stroke,
2158
- strokeWidth: shape.strokeWidth,
2159
- strokeLinecap: "round",
2160
- strokeLinejoin: "round"
2161
- };
2162
- if (isBoxType(shape.type)) {
2163
- return /* @__PURE__ */ jsxs5("g", { children: [
2164
- /* @__PURE__ */ jsx5(BoxGeometry, { shape, stroke }),
2165
- /* @__PURE__ */ jsx5(BoxTexts, { shape, hideField, showHints })
2166
- ] });
2167
- }
2168
- if (isConnectorType(shape.type)) {
2169
- const pts = points ?? [
2170
- { x: shape.x, y: shape.y },
2171
- { x: shape.x + shape.w, y: shape.y + shape.h }
2172
- ];
2173
- return /* @__PURE__ */ jsxs5("g", { children: [
2174
- /* @__PURE__ */ jsx5("path", { ...stroke, d: polylinePath(pts), fill: "none" }),
2175
- arrowHeads(shape, pts).map((d, i) => /* @__PURE__ */ jsx5("path", { ...stroke, d, fill: "none" }, i)),
2176
- shape.text && hideField !== "text" && /* @__PURE__ */ jsx5(ConnectorLabel, { shape, points: pts })
2177
- ] });
2178
- }
2179
- return /* @__PURE__ */ jsx5(
2180
- "text",
2181
- {
2182
- x: shape.x,
2183
- y: shape.y + FONT_SIZE,
2184
- fill: shape.stroke,
2185
- fontSize: FONT_SIZE,
2186
- style: { userSelect: "none", whiteSpace: "pre" },
2187
- children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx5("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * LINE_HEIGHT, children: line }, i))
2188
- }
2189
- );
1869
+ function normalizeText(value) {
1870
+ if (!isRecord2(value) || !nonEmptyString(value.text)) return null;
1871
+ const x = finiteNumber(value.x);
1872
+ const y = finiteNumber(value.y);
1873
+ if (x === void 0 || y === void 0) return null;
1874
+ return compact({
1875
+ id: nonEmptyString(value.id) ? value.id : void 0,
1876
+ x,
1877
+ y,
1878
+ text: value.text,
1879
+ color: colorName(value.color),
1880
+ stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
1881
+ });
2190
1882
  }
2191
- function HitArea({
2192
- shape,
2193
- points
2194
- }) {
2195
- if (isConnectorType(shape.type) && points) {
2196
- return /* @__PURE__ */ jsx5("path", { d: polylinePath(points), fill: "none", stroke: "transparent", strokeWidth: 16 });
2197
- }
2198
- const b = bbox(shape);
2199
- return /* @__PURE__ */ jsx5(
2200
- "rect",
2201
- {
2202
- x: b.x - 2,
2203
- y: b.y - 2,
2204
- width: Math.max(b.w, 8) + 4,
2205
- height: Math.max(b.h, 8) + 4,
2206
- fill: "transparent",
2207
- stroke: "none"
2208
- }
2209
- );
1883
+ function isRecord2(value) {
1884
+ return typeof value === "object" && value !== null;
2210
1885
  }
2211
-
2212
- // src/drawing/canvas/TextEditOverlay.tsx
2213
- import {
2214
- useEffect as useEffect5,
2215
- useRef as useRef2,
2216
- useState
2217
- } from "react";
2218
- import { jsx as jsx6 } from "react/jsx-runtime";
2219
- function TextEditOverlay({
2220
- shape,
2221
- field,
2222
- points,
2223
- onCommit
2224
- }) {
2225
- const original = shape[field] ?? "";
2226
- const [value, setValue] = useState(original);
2227
- const ref = useRef2(null);
2228
- const valueRef = useRef2(value);
2229
- valueRef.current = value;
2230
- useEffect5(() => {
2231
- const el = ref.current;
2232
- if (!el) return;
2233
- el.focus();
2234
- el.select();
2235
- const onKeyDown = (e) => {
2236
- e.stopPropagation();
2237
- if ((e.key === "Home" || e.key === "End") && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
2238
- e.preventDefault();
2239
- const text = el.value;
2240
- const caret = e.key === "Home" ? el.selectionStart : el.selectionEnd;
2241
- const pos = e.key === "Home" ? text.lastIndexOf("\n", caret - 1) + 1 : (text.indexOf("\n", caret) + 1 || text.length + 1) - 1;
2242
- el.setSelectionRange(pos, pos);
2243
- return;
2244
- }
2245
- if (e.key === "Enter" && !e.shiftKey) {
2246
- e.preventDefault();
2247
- onCommit(shape.id, field, valueRef.current);
2248
- } else if (e.key === "Escape") {
2249
- e.preventDefault();
2250
- onCommit(shape.id, field, original);
1886
+ function nonEmptyString(value) {
1887
+ return typeof value === "string" && value !== "";
1888
+ }
1889
+ function finiteNumber(value) {
1890
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1891
+ }
1892
+ function positiveNumber(value) {
1893
+ const n2 = finiteNumber(value);
1894
+ return n2 !== void 0 && n2 > 0 ? n2 : void 0;
1895
+ }
1896
+ function colorName(value) {
1897
+ return typeof value === "string" && COLOR_NAMES.includes(value) ? value : void 0;
1898
+ }
1899
+ function compact(value) {
1900
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1901
+ }
1902
+ var COLOR_SCHEMA = {
1903
+ enum: COLOR_NAMES,
1904
+ description: "Named color preset: outline plus, for boxes, a matching fill. `plain` has no outline and a light fill, `black` is a dark card with light text, `gray` is a dark outline on transparent (the default); the rest pair a colored outline with its pastel fill."
1905
+ };
1906
+ var END_SCHEMA = {
1907
+ oneOf: [
1908
+ {
1909
+ type: "string",
1910
+ description: "Id of a box. The connector attaches to its outline, aimed at the other end"
1911
+ },
1912
+ {
1913
+ type: "object",
1914
+ required: ["id"],
1915
+ additionalProperties: false,
1916
+ properties: {
1917
+ id: { type: "string", description: "Id of a box" },
1918
+ side: {
1919
+ enum: ["top", "right", "bottom", "left"],
1920
+ description: "Pin the endpoint to the middle of this side of the box instead of auto-aiming"
1921
+ }
2251
1922
  }
2252
- };
2253
- const stop = (e) => e.stopPropagation();
2254
- el.addEventListener("keydown", onKeyDown);
2255
- el.addEventListener("keyup", stop);
2256
- el.addEventListener("keypress", stop);
2257
- return () => {
2258
- el.removeEventListener("keydown", onKeyDown);
2259
- el.removeEventListener("keyup", stop);
2260
- el.removeEventListener("keypress", stop);
2261
- };
2262
- }, []);
2263
- const style = {
2264
- color: shape.stroke,
2265
- fontFamily: "var(--zui-drawing-font)",
2266
- lineHeight: LINE_HEIGHT
2267
- };
2268
- if (shape.type === "text") {
2269
- const size = textBoxSize(value || " ");
2270
- Object.assign(style, {
2271
- left: shape.x,
2272
- top: shape.y,
2273
- fontSize: FONT_SIZE,
2274
- width: Math.max(80, size.w + 20),
2275
- height: size.h + 2,
2276
- // `pre` would keep the lines, but Chrome ignores Home/End in a
2277
- // `white-space: pre` textarea; the width tracks the content anyway
2278
- whiteSpace: "pre-wrap"
2279
- });
2280
- } else if (isConnectorType(shape.type)) {
2281
- const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
2282
- const size = textBoxSize(lines.join("\n") || " ", CONNECTOR_FONT_SIZE);
2283
- const width = Math.max(70, size.w + 16);
2284
- const height = size.h + 4;
2285
- const mid = connectorMidpoint(
2286
- points ?? [
2287
- { x: shape.x, y: shape.y },
2288
- { x: shape.x + shape.w, y: shape.y + shape.h }
2289
- ]
2290
- );
2291
- Object.assign(style, {
2292
- left: mid.x - width / 2,
2293
- top: mid.y - height / 2,
2294
- width,
2295
- height,
2296
- fontSize: CONNECTOR_FONT_SIZE,
2297
- textAlign: "center",
2298
- whiteSpace: "pre-wrap"
2299
- });
2300
- } else {
2301
- const { area } = slotLayout(shape);
2302
- const fontSize = field === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
2303
- const width = Math.max(area.w, 40);
2304
- const lines = wrapText(value, width, fontSize).length;
2305
- const boxH = lines * fontSize * LINE_HEIGHT + 2;
2306
- const top = field === "label" ? area.y - 1 : field === "footer" ? area.y + area.h - boxH + 1 : area.y + area.h / 2 - boxH / 2;
2307
- Object.assign(style, {
2308
- left: area.x,
2309
- top,
2310
- width,
2311
- height: boxH,
2312
- fontSize,
2313
- fontWeight: field === "label" ? 600 : void 0,
2314
- letterSpacing: field === "label" ? 0.3 : void 0,
2315
- opacity: field === "text" ? 1 : field === "label" ? 0.85 : 0.65,
2316
- textAlign: "center",
2317
- whiteSpace: "pre-wrap"
2318
- });
1923
+ }
1924
+ ],
1925
+ description: "Box a connector end attaches to: a bare box id, or {id, side} to pick the attach edge"
1926
+ };
1927
+ var DRAWING_SKELETON_JSON_SCHEMA = {
1928
+ $schema: "http://json-schema.org/draft-07/schema#",
1929
+ title: "DrawingSkeleton",
1930
+ description: "Compact intent description of a diagram. Boxes are sized from their text and laid out automatically along the connector flow; connectors are drawn between box outlines. Only say what is on the canvas \u2014 geometry is derived",
1931
+ type: "object",
1932
+ required: ["boxes"],
1933
+ additionalProperties: false,
1934
+ properties: {
1935
+ direction: {
1936
+ enum: ["right", "down"],
1937
+ description: 'Auto-layout flow direction. Boxes connected a\u2192b are placed in successive ranks along this axis ("right" = columns left to right, "down" = rows top to bottom). Default "right"'
1938
+ },
1939
+ canvasWidth: {
1940
+ type: "number",
1941
+ minimum: 120,
1942
+ description: "Logical canvas width in px. Omit for a fluid canvas in CSS pixels"
1943
+ },
1944
+ canvasHeight: {
1945
+ type: "number",
1946
+ minimum: 80,
1947
+ description: "Canvas height in px. Omit to fit the content"
1948
+ },
1949
+ width: {
1950
+ enum: ["full", "text", "content"],
1951
+ description: 'Block width: "text" aligns the drawing with the text column (same as "full" unless the app sets a text measure); "content" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor'
1952
+ },
1953
+ boxes: {
1954
+ type: "array",
1955
+ items: { $ref: "#/definitions/box" },
1956
+ description: "Card-like shapes carrying up to three text slots. Order is the render order and the stacking order within a layout rank"
1957
+ },
1958
+ connectors: {
1959
+ type: "array",
1960
+ items: { $ref: "#/definitions/connector" },
1961
+ description: "Arrows and lines between boxes. Connectors whose from/to id does not match a box are dropped"
1962
+ },
1963
+ texts: {
1964
+ type: "array",
1965
+ items: { $ref: "#/definitions/text" },
1966
+ description: "Free-floating annotations not attached to any box. Prefer box slots (label/text/footer) when the text belongs to a box"
1967
+ }
1968
+ },
1969
+ definitions: {
1970
+ box: {
1971
+ type: "object",
1972
+ required: ["id"],
1973
+ additionalProperties: false,
1974
+ properties: {
1975
+ id: { type: "string", description: "Unique within the drawing; connectors reference it" },
1976
+ type: {
1977
+ enum: BOX_TYPES,
1978
+ description: 'Shape: rect (default), ellipse, diamond (decision), note (sticky note, yellow by default), cylinder (database), cloud (external system), queue (message queue), actor (stick figure; only the "text" slot renders, below the figure)'
1979
+ },
1980
+ label: { type: "string", description: "Small bold heading at the top of the box" },
1981
+ text: {
1982
+ type: "string",
1983
+ description: 'Main content, centered. Wraps automatically; "\\n" forces a line break'
1984
+ },
1985
+ footer: { type: "string", description: "Small dim line at the bottom of the box" },
1986
+ x: { type: "number", description: "Top-left x in px. Omit (with y) to let the layout place the box" },
1987
+ y: { type: "number", description: "Top-left y in px. Omit (with x) to let the layout place the box" },
1988
+ w: {
1989
+ type: "number",
1990
+ exclusiveMinimum: 0,
1991
+ description: "Width in px. Omit to size from the text (never smaller than the type default)"
1992
+ },
1993
+ h: {
1994
+ type: "number",
1995
+ exclusiveMinimum: 0,
1996
+ description: "Height in px. Omit to size from the text (never smaller than the type default)"
1997
+ },
1998
+ color: COLOR_SCHEMA,
1999
+ stroke: { type: "string", description: "CSS color of the outline and text; overrides the preset" },
2000
+ fill: {
2001
+ type: "string",
2002
+ description: 'CSS color of the interior ("transparent" for none); overrides the preset'
2003
+ }
2004
+ }
2005
+ },
2006
+ connector: {
2007
+ type: "object",
2008
+ required: ["from", "to"],
2009
+ additionalProperties: false,
2010
+ properties: {
2011
+ id: { type: "string", description: "Optional; generated when omitted" },
2012
+ type: {
2013
+ enum: CONNECTOR_TYPES,
2014
+ description: 'arrow (default; arrowhead at "to") or line (no arrowheads)'
2015
+ },
2016
+ from: END_SCHEMA,
2017
+ to: END_SCHEMA,
2018
+ text: { type: "string", description: "Label rendered at the middle of the connector" },
2019
+ bidirectional: {
2020
+ type: "boolean",
2021
+ description: "Arrow only: arrowheads on both ends"
2022
+ },
2023
+ routing: {
2024
+ enum: ["straight", "elbow"],
2025
+ description: "straight (default) draws a direct line; elbow draws a right-angled path routed around the boxes"
2026
+ },
2027
+ color: COLOR_SCHEMA,
2028
+ stroke: { type: "string", description: "CSS color of the line and label; overrides the preset" }
2029
+ }
2030
+ },
2031
+ text: {
2032
+ type: "object",
2033
+ required: ["x", "y", "text"],
2034
+ additionalProperties: false,
2035
+ properties: {
2036
+ id: { type: "string", description: "Optional; generated when omitted" },
2037
+ x: { type: "number", description: "Top-left x of the first line, in px" },
2038
+ y: { type: "number", description: "Top-left y of the first line, in px" },
2039
+ text: { type: "string", description: 'Content; "\\n" forces a line break' },
2040
+ color: COLOR_SCHEMA,
2041
+ stroke: { type: "string", description: "CSS color of the text; overrides the preset" }
2042
+ }
2043
+ }
2319
2044
  }
2320
- const placeholder = field === "label" ? "Label" : field === "footer" ? "Footer" : "Text";
2321
- return /* @__PURE__ */ jsx6(
2322
- "textarea",
2045
+ };
2046
+
2047
+ // src/drawing/canvas/PropertyBar.tsx
2048
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2049
+ var COLOR_NAMES2 = Object.keys(COLOR_PRESETS);
2050
+ function Swatch({
2051
+ name,
2052
+ active,
2053
+ onClick
2054
+ }) {
2055
+ const preset = COLOR_PRESETS[name];
2056
+ return /* @__PURE__ */ jsx2(
2057
+ "button",
2323
2058
  {
2324
- ref,
2325
- className: "zui-drawing-text-input",
2326
- style,
2327
- value,
2328
- placeholder,
2329
- spellCheck: false,
2330
- onChange: (e) => setValue(e.target.value),
2331
- onBlur: () => onCommit(shape.id, field, value),
2332
- onPointerDown: (e) => e.stopPropagation()
2059
+ type: "button",
2060
+ title: `Color: ${name}`,
2061
+ "aria-label": `Color ${name}`,
2062
+ "aria-pressed": active,
2063
+ className: `zui-drawing-swatch ${active ? "is-active" : ""}`,
2064
+ style: { backgroundColor: preset.fill, borderColor: preset.stroke },
2065
+ onClick
2333
2066
  }
2334
2067
  );
2335
2068
  }
2336
-
2337
- // src/drawing/canvas/Toolbar.tsx
2338
- import { useEffect as useEffect6, useRef as useRef3, useState as useState2 } from "react";
2339
-
2340
- // src/components/blockWidthOptions.tsx
2341
- import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2342
- var BLOCK_WIDTH_OPTIONS = [
2343
- {
2344
- width: "full",
2345
- label: "Full width",
2346
- icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2347
- /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2348
- /* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
2349
- ] })
2350
- },
2351
- {
2352
- width: "text",
2353
- label: "Text width",
2354
- icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2355
- /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2356
- /* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
2357
- ] })
2358
- },
2359
- {
2360
- width: "content",
2361
- label: "Content width",
2362
- icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2363
- /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2364
- /* @__PURE__ */ jsx7("path", { d: "M5.5 10h3M11.5 10h3M6.5 7.5L9 10l-2.5 2.5M13.5 7.5L11 10l2.5 2.5" })
2365
- ] })
2366
- }
2367
- ];
2368
-
2369
- // src/drawing/canvas/Toolbar.tsx
2370
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2371
- var PRIMARY_TOOLS = [
2372
- { tool: "select", label: "Select" },
2373
- { tool: "rect", label: "Rectangle" },
2374
- { tool: "ellipse", label: "Ellipse" },
2375
- { tool: "diamond", label: "Diamond" },
2376
- { tool: "arrow", label: "Arrow" },
2377
- { tool: "line", label: "Line" },
2378
- { tool: "text", label: "Text" }
2379
- ];
2380
- var MORE_SHAPES = ["note", "cylinder", "cloud", "queue", "actor"];
2381
- function ToolButton({
2069
+ function OptionButton({
2382
2070
  label,
2383
2071
  active,
2384
2072
  onClick,
2385
2073
  children,
2386
- className,
2387
- pressed
2074
+ className
2388
2075
  }) {
2389
- return /* @__PURE__ */ jsx8(
2076
+ return /* @__PURE__ */ jsx2(
2390
2077
  "button",
2391
2078
  {
2392
2079
  type: "button",
2393
2080
  title: label,
2394
2081
  "aria-label": label,
2395
- "aria-pressed": pressed ?? active,
2082
+ "aria-pressed": active,
2396
2083
  className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
2397
2084
  onClick,
2398
- children: /* @__PURE__ */ jsx8(Icon, { children })
2085
+ children: /* @__PURE__ */ jsx2(Icon, { children })
2399
2086
  }
2400
2087
  );
2401
2088
  }
2402
- function DrawingToolbar({
2403
- tool,
2404
- onToolChange,
2405
- width,
2406
- onWidthChange,
2407
- onCopyMermaid
2408
- }) {
2409
- const [moreOpen, setMoreOpen] = useState2(false);
2410
- const [lastMore, setLastMore] = useState2("note");
2411
- const [copied, setCopied] = useState2(false);
2412
- const moreRef = useRef3(null);
2413
- useEffect6(() => {
2414
- if (!moreOpen) return;
2415
- const close = (e) => {
2416
- if (!moreRef.current?.contains(e.target)) setMoreOpen(false);
2417
- };
2418
- const key = (e) => {
2419
- if (e.key === "Escape") setMoreOpen(false);
2420
- };
2421
- document.addEventListener("pointerdown", close);
2422
- document.addEventListener("keydown", key);
2423
- return () => {
2424
- document.removeEventListener("pointerdown", close);
2425
- document.removeEventListener("keydown", key);
2426
- };
2427
- }, [moreOpen]);
2428
- const moreActive = MORE_SHAPES.includes(tool);
2429
- const moreIcon = moreActive ? SHAPE_ICONS[tool] : SHAPE_ICONS[lastMore];
2430
- return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2431
- /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group", role: "toolbar", "aria-label": "Drawing tools", children: [
2432
- PRIMARY_TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx8(ToolButton, { label, active: tool === t, onClick: () => onToolChange(t), children: SHAPE_ICONS[t] }, t)),
2433
- /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-more", ref: moreRef, children: [
2434
- /* @__PURE__ */ jsxs7(
2435
- "button",
2436
- {
2437
- type: "button",
2438
- title: "More shapes",
2439
- "aria-label": "More shapes",
2440
- "aria-haspopup": "menu",
2441
- "aria-expanded": moreOpen,
2442
- className: `zui-drawing-tool zui-drawing-tool-more ${moreActive ? "is-active" : ""}`,
2443
- onClick: () => setMoreOpen((open) => !open),
2444
- children: [
2445
- /* @__PURE__ */ jsx8(Icon, { children: moreIcon }),
2446
- /* @__PURE__ */ jsx8("span", { className: "zui-drawing-caret", "aria-hidden": "true" })
2447
- ]
2448
- }
2449
- ),
2450
- moreOpen && /* @__PURE__ */ jsx8("div", { className: "zui-drawing-popover", role: "menu", children: MORE_SHAPES.map((type) => /* @__PURE__ */ jsxs7(
2451
- "button",
2452
- {
2453
- type: "button",
2454
- role: "menuitemradio",
2455
- "aria-checked": tool === type,
2456
- className: `zui-drawing-popover-item ${tool === type ? "is-active" : ""}`,
2457
- onClick: () => {
2458
- setLastMore(type);
2459
- onToolChange(type);
2460
- setMoreOpen(false);
2461
- },
2462
- children: [
2463
- /* @__PURE__ */ jsx8(Icon, { size: 18, children: SHAPE_ICONS[type] }),
2464
- /* @__PURE__ */ jsx8("span", { children: BOX_DEFINITIONS[type].label })
2465
- ]
2466
- },
2467
- type
2468
- )) })
2469
- ] })
2089
+ function PropertyBar({
2090
+ selection,
2091
+ stroke,
2092
+ fill,
2093
+ onColor,
2094
+ onRouting,
2095
+ onDirection,
2096
+ onDelete
2097
+ }) {
2098
+ const connectors = selection.filter((s) => isConnectorType(s.type));
2099
+ const arrows = selection.filter((s) => s.type === "arrow");
2100
+ const allElbow = connectors.length > 0 && connectors.every((s) => s.routing === "elbow");
2101
+ const allStraight = connectors.length > 0 && connectors.every((s) => s.routing !== "elbow");
2102
+ const allTwoWay = arrows.length > 0 && arrows.every((s) => s.bidirectional);
2103
+ const allOneWay = arrows.length > 0 && arrows.every((s) => !s.bidirectional);
2104
+ return /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props", onPointerDown: (e) => e.stopPropagation(), children: [
2105
+ /* @__PURE__ */ jsx2("div", { className: "zui-drawing-props-group", role: "radiogroup", "aria-label": "Color", children: COLOR_NAMES2.map((name) => /* @__PURE__ */ jsx2(
2106
+ Swatch,
2107
+ {
2108
+ name,
2109
+ active: COLOR_PRESETS[name].stroke === stroke && COLOR_PRESETS[name].fill === fill,
2110
+ onClick: () => onColor(name)
2111
+ },
2112
+ name
2113
+ )) }),
2114
+ connectors.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Connector routing", children: [
2115
+ /* @__PURE__ */ jsx2(OptionButton, { label: "Straight connector", active: allStraight, onClick: () => onRouting(false), children: UI_ICONS.straight }),
2116
+ /* @__PURE__ */ jsx2(OptionButton, { label: "Elbow connector (auto-routed)", active: allElbow, onClick: () => onRouting(true), children: UI_ICONS.elbow })
2470
2117
  ] }),
2471
- /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group zui-drawing-toolbar-group-end", children: [
2472
- /* @__PURE__ */ jsx8(
2473
- ToolButton,
2118
+ arrows.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Arrow direction", children: [
2119
+ /* @__PURE__ */ jsx2(OptionButton, { label: "One-way arrow", active: allOneWay, onClick: () => onDirection(false), children: UI_ICONS.oneWay }),
2120
+ /* @__PURE__ */ jsx2(OptionButton, { label: "Two-way arrow", active: allTwoWay, onClick: () => onDirection(true), children: UI_ICONS.twoWay })
2121
+ ] }),
2122
+ selection.length > 0 && /* @__PURE__ */ jsx2("div", { className: "zui-drawing-props-group", children: /* @__PURE__ */ jsx2(
2123
+ OptionButton,
2124
+ {
2125
+ label: selection.length > 1 ? `Delete ${selection.length} shapes` : "Delete shape",
2126
+ className: "zui-drawing-tool-danger",
2127
+ onClick: onDelete,
2128
+ children: UI_ICONS.trash
2129
+ }
2130
+ ) })
2131
+ ] });
2132
+ }
2133
+
2134
+ // src/drawing/canvas/SelectionOverlay.tsx
2135
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2136
+ var HANDLE = 7;
2137
+ function Handle({
2138
+ x,
2139
+ y,
2140
+ cursor,
2141
+ square,
2142
+ onPointerDown,
2143
+ title
2144
+ }) {
2145
+ const props = {
2146
+ className: "zui-drawing-handle",
2147
+ strokeWidth: 1.5,
2148
+ style: { cursor },
2149
+ onPointerDown
2150
+ };
2151
+ return square ? /* @__PURE__ */ jsx3("rect", { ...props, x: x - HANDLE / 2, y: y - HANDLE / 2, width: HANDLE, height: HANDLE, rx: 2, children: title && /* @__PURE__ */ jsx3("title", { children: title }) }) : /* @__PURE__ */ jsx3("circle", { ...props, cx: x, cy: y, r: HANDLE / 2 + 0.5, children: title && /* @__PURE__ */ jsx3("title", { children: title }) });
2152
+ }
2153
+ function SelectionOverlay({
2154
+ shape,
2155
+ points,
2156
+ onHandlePointerDown,
2157
+ onWaypointAdd,
2158
+ onWaypointRemove
2159
+ }) {
2160
+ if (isConnectorType(shape.type)) {
2161
+ const waypoints = shape.waypoints ?? [];
2162
+ const isZ = shape.routing === "elbow" && !waypoints.length && points.length === 4;
2163
+ const elbowMid = isZ ? { x: (points[1].x + points[2].x) / 2, y: (points[1].y + points[2].y) / 2 } : null;
2164
+ const elbowVertical = isZ && Math.abs(points[1].x - points[2].x) < 0.01;
2165
+ const ghosts = points.slice(0, -1).flatMap((p, i) => {
2166
+ const q = points[i + 1];
2167
+ if (Math.hypot(q.x - p.x, q.y - p.y) < 28) return [];
2168
+ if (isZ && i === 1) return [];
2169
+ return [{ index: i, x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }];
2170
+ });
2171
+ return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
2172
+ ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx3(
2173
+ "circle",
2474
2174
  {
2475
- label: copied ? "Copied Mermaid to clipboard" : "Copy as Mermaid",
2476
- className: copied ? "is-success" : "",
2477
- onClick: () => {
2478
- void onCopyMermaid().then((ok) => {
2479
- if (!ok) return;
2480
- setCopied(true);
2481
- setTimeout(() => setCopied(false), 1600);
2482
- });
2175
+ className: "zui-drawing-handle zui-drawing-ghost",
2176
+ cx: x,
2177
+ cy: y,
2178
+ r: 4.5,
2179
+ strokeWidth: 1.5,
2180
+ strokeDasharray: "2 2",
2181
+ style: { cursor: "copy" },
2182
+ onPointerDown: (e) => onWaypointAdd(e, shape.id, index, { x, y }),
2183
+ children: /* @__PURE__ */ jsx3("title", { children: "Drag to add a bend" })
2184
+ },
2185
+ `ghost-${index}`
2186
+ )),
2187
+ waypoints.map((p, i) => /* @__PURE__ */ jsx3(
2188
+ "g",
2189
+ {
2190
+ onDoubleClick: (e) => {
2191
+ e.stopPropagation();
2192
+ onWaypointRemove(shape.id, i);
2483
2193
  },
2484
- children: copied ? UI_ICONS.check : UI_ICONS.mermaid
2194
+ children: /* @__PURE__ */ jsx3(
2195
+ Handle,
2196
+ {
2197
+ x: p.x,
2198
+ y: p.y,
2199
+ square: true,
2200
+ cursor: "move",
2201
+ title: "Drag to move, double-click to remove",
2202
+ onPointerDown: (e) => onHandlePointerDown(e, { mode: "waypoint", id: shape.id, index: i })
2203
+ }
2204
+ )
2205
+ },
2206
+ `wp-${i}`
2207
+ )),
2208
+ elbowMid && /* @__PURE__ */ jsx3(
2209
+ Handle,
2210
+ {
2211
+ x: elbowMid.x,
2212
+ y: elbowMid.y,
2213
+ square: true,
2214
+ cursor: elbowVertical ? "ew-resize" : "ns-resize",
2215
+ title: "Slide the middle segment",
2216
+ onPointerDown: (e) => onHandlePointerDown(e, {
2217
+ mode: "elbow",
2218
+ id: shape.id,
2219
+ a: points[0],
2220
+ d: points[3],
2221
+ vertical: elbowVertical
2222
+ })
2485
2223
  }
2486
2224
  ),
2487
- /* @__PURE__ */ jsx8("span", { className: "zui-drawing-toolbar-divider" }),
2488
- BLOCK_WIDTH_OPTIONS.map(({ width: option, label, icon }) => /* @__PURE__ */ jsx8(
2489
- ToolButton,
2490
- {
2491
- label,
2492
- active: width === option,
2493
- onClick: () => onWidthChange(option),
2494
- children: icon
2495
- },
2496
- option
2497
- ))
2498
- ] })
2225
+ ["start", "end"].map((end) => {
2226
+ const p = end === "start" ? points[0] : points[points.length - 1];
2227
+ return /* @__PURE__ */ jsx3(
2228
+ Handle,
2229
+ {
2230
+ x: p.x,
2231
+ y: p.y,
2232
+ cursor: "crosshair",
2233
+ onPointerDown: (e) => onHandlePointerDown(e, { mode: "endpoint", id: shape.id, end, orig: shape })
2234
+ },
2235
+ end
2236
+ );
2237
+ })
2238
+ ] });
2239
+ }
2240
+ const b = bbox(shape);
2241
+ const pad = 5;
2242
+ const corners = [
2243
+ { corner: "nw", x: b.x - pad, y: b.y - pad },
2244
+ { corner: "ne", x: b.x + b.w + pad, y: b.y - pad },
2245
+ { corner: "sw", x: b.x - pad, y: b.y + b.h + pad },
2246
+ { corner: "se", x: b.x + b.w + pad, y: b.y + b.h + pad }
2247
+ ];
2248
+ const resizable = shape.type !== "text";
2249
+ return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
2250
+ /* @__PURE__ */ jsx3(SelectionFrame, { rect: b, pad }),
2251
+ resizable && corners.map(({ corner, x, y }) => /* @__PURE__ */ jsx3(
2252
+ Handle,
2253
+ {
2254
+ x,
2255
+ y,
2256
+ square: true,
2257
+ cursor: corner === "nw" || corner === "se" ? "nwse-resize" : "nesw-resize",
2258
+ onPointerDown: (e) => onHandlePointerDown(e, { mode: "resize", id: shape.id, corner, orig: shape })
2259
+ },
2260
+ corner
2261
+ ))
2499
2262
  ] });
2500
2263
  }
2501
-
2502
- // src/drawing/canvas/DrawingCanvas.tsx
2503
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2504
- var MIN_HEIGHT = 120;
2505
- var MAX_HEIGHT = 1200;
2506
- var WIDTH_CLASS = {
2507
- full: "",
2508
- text: "is-text-width",
2509
- content: "is-content-width"
2510
- };
2511
- var MIN_CONTENT_WIDTH = 240;
2512
- var CONTENT_WIDTH_PADDING = 40;
2513
- var EMPTY_SET = /* @__PURE__ */ new Set();
2514
- function contentWidth(shapes, paths) {
2515
- let right = 0;
2516
- for (const shape of shapes) {
2517
- if (isConnectorType(shape.type)) {
2518
- for (const p of paths.get(shape.id) ?? []) right = Math.max(right, p.x);
2519
- } else {
2520
- const b = bbox(shape);
2521
- right = Math.max(right, b.x + b.w);
2264
+ function SelectionFrame({ rect, pad }) {
2265
+ return /* @__PURE__ */ jsx3(
2266
+ "rect",
2267
+ {
2268
+ x: rect.x - pad,
2269
+ y: rect.y - pad,
2270
+ width: rect.w + pad * 2,
2271
+ height: rect.h + pad * 2,
2272
+ rx: 6,
2273
+ fill: "none",
2274
+ stroke: "currentColor",
2275
+ strokeWidth: 1,
2276
+ strokeDasharray: "5 4",
2277
+ pointerEvents: "none"
2522
2278
  }
2523
- }
2524
- return Math.max(MIN_CONTENT_WIDTH, Math.ceil(right + CONTENT_WIDTH_PADDING));
2279
+ );
2525
2280
  }
2526
- function computePaths(shapes) {
2527
- const paths = /* @__PURE__ */ new Map();
2528
- for (const shape of shapes) {
2529
- if (isConnectorType(shape.type)) paths.set(shape.id, connectorPoints(shape, shapes));
2530
- }
2531
- return paths;
2281
+ function GroupSelectionOverlay({
2282
+ shapes,
2283
+ paths
2284
+ }) {
2285
+ if (!shapes.length) return null;
2286
+ const rects = shapes.map((s) => {
2287
+ if (!isConnectorType(s.type)) return bbox(s);
2288
+ const pts = paths.get(s.id) ?? [];
2289
+ const xs = pts.map((p) => p.x);
2290
+ const ys = pts.map((p) => p.y);
2291
+ const x = Math.min(...xs);
2292
+ const y = Math.min(...ys);
2293
+ return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
2294
+ });
2295
+ return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", pointerEvents: "none", children: [
2296
+ rects.map((r, i) => /* @__PURE__ */ jsx3(
2297
+ "rect",
2298
+ {
2299
+ x: r.x - 3,
2300
+ y: r.y - 3,
2301
+ width: r.w + 6,
2302
+ height: r.h + 6,
2303
+ rx: 4,
2304
+ fill: "none",
2305
+ stroke: "currentColor",
2306
+ strokeWidth: 1,
2307
+ opacity: 0.45
2308
+ },
2309
+ shapes[i].id
2310
+ )),
2311
+ /* @__PURE__ */ jsx3(SelectionFrame, { rect: unionRects(rects), pad: 8 })
2312
+ ] });
2532
2313
  }
2533
- function DrawingCanvas({ nodeKey, data }) {
2534
- const [editor] = useLexicalComposerContext5();
2535
- const isEditable = useLexicalEditable();
2536
- const [shapes, setShapes] = useState3(data.shapes);
2537
- const [canvasHeight, setCanvasHeight] = useState3(data.canvasHeight);
2538
- const [width, setWidth] = useState3(data.width ?? "full");
2539
- const [tool, setTool] = useState3("select");
2540
- const [selectedIds, setSelectedIds] = useState3(EMPTY_SET);
2541
- const [editingText, setEditingText] = useState3(null);
2542
- const [stroke, setStroke] = useState3(STROKE_COLORS[0]);
2543
- const [fill, setFill] = useState3(FILL_COLORS[0]);
2544
- const [hoverBoxId, setHoverBoxId] = useState3(null);
2545
- const [marquee, setMarquee] = useState3(null);
2546
- const [scale, setScale] = useState3(1);
2547
- const rootRef = useRef4(null);
2548
- const svgRef = useRef4(null);
2549
- const dragRef = useRef4(null);
2550
- const pendingTextEditRef = useRef4(null);
2551
- const pendingPointRef = useRef4(null);
2552
- const frameRef = useRef4(null);
2553
- const lastCommittedRef = useRef4(serializeDrawingData(data));
2554
- const shapesRef = useRef4(shapes);
2555
- shapesRef.current = shapes;
2556
- const heightRef = useRef4(canvasHeight);
2557
- heightRef.current = canvasHeight;
2558
- const widthRef = useRef4(width);
2559
- widthRef.current = width;
2560
- const selectedRef = useRef4(selectedIds);
2561
- selectedRef.current = selectedIds;
2562
- const paths = useMemo(() => computePaths(shapes), [shapes]);
2563
- const canvasWidth = data.canvasWidth;
2564
- const logicalWidth = canvasWidth ?? (width === "content" ? contentWidth(shapes, paths) : null);
2565
- useEffect7(() => {
2566
- const incoming = serializeDrawingData(data);
2567
- if (incoming !== lastCommittedRef.current) {
2568
- lastCommittedRef.current = incoming;
2569
- setShapes(data.shapes);
2570
- setCanvasHeight(data.canvasHeight);
2571
- setWidth(data.width ?? "full");
2572
- setSelectedIds(EMPTY_SET);
2573
- setEditingText(null);
2574
- }
2575
- }, [data]);
2576
- useEffect7(() => {
2577
- const svg = svgRef.current;
2578
- if (!logicalWidth || !svg || typeof ResizeObserver === "undefined") {
2579
- setScale(1);
2580
- return;
2581
- }
2582
- const update = () => {
2583
- const rect = svg.getBoundingClientRect();
2584
- setScale(rect.width > 0 ? rect.width / logicalWidth : 1);
2585
- };
2586
- update();
2587
- const observer = new ResizeObserver(update);
2588
- observer.observe(svg);
2589
- return () => observer.disconnect();
2590
- }, [logicalWidth]);
2591
- const commit = useCallback(
2592
- (nextShapes, options) => {
2593
- const nextWidth = options?.width ?? widthRef.current;
2594
- const explicitFull = options?.width === void 0 && data.width !== void 0;
2595
- const payload = {
2596
- version: 2,
2597
- ...canvasWidth ? { canvasWidth } : {},
2598
- canvasHeight: options?.height ?? heightRef.current,
2599
- ...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
2600
- shapes: nextShapes
2601
- };
2602
- const json = serializeDrawingData(payload);
2603
- if (json === lastCommittedRef.current) return;
2604
- lastCommittedRef.current = json;
2605
- editor.update(() => {
2606
- const node = $getNodeByKey2(nodeKey);
2607
- if ($isDrawingNode(node)) node.setData(payload);
2608
- });
2609
- },
2610
- [editor, nodeKey, canvasWidth, data.width]
2611
- );
2612
- const updateShapes = useCallback(
2613
- (updater, options) => {
2614
- setShapes((prev) => {
2615
- const next = resolveBindings(updater(prev));
2616
- if (options?.commit) commit(next);
2617
- return next;
2618
- });
2619
- },
2620
- [commit]
2621
- );
2622
- const getPoint = useCallback((e) => {
2623
- const svg = svgRef.current;
2624
- if (!svg) return { x: 0, y: 0 };
2625
- const ctm = svg.getScreenCTM();
2626
- if (!ctm) {
2627
- const rect = svg.getBoundingClientRect();
2628
- return { x: e.clientX - rect.left, y: e.clientY - rect.top };
2314
+ function MarqueeOverlay({ rect }) {
2315
+ return /* @__PURE__ */ jsx3(
2316
+ "rect",
2317
+ {
2318
+ className: "zui-drawing-selection",
2319
+ x: rect.x,
2320
+ y: rect.y,
2321
+ width: rect.w,
2322
+ height: rect.h,
2323
+ rx: 3,
2324
+ fill: "currentColor",
2325
+ fillOpacity: 0.08,
2326
+ stroke: "currentColor",
2327
+ strokeWidth: 1,
2328
+ pointerEvents: "none"
2629
2329
  }
2630
- const inverse = ctm.inverse();
2631
- return {
2632
- x: inverse.a * e.clientX + inverse.c * e.clientY + inverse.e,
2633
- y: inverse.b * e.clientX + inverse.d * e.clientY + inverse.f
2634
- };
2635
- }, []);
2636
- const selection = useMemo(
2637
- () => shapes.filter((s) => selectedIds.has(s.id)),
2638
- [shapes, selectedIds]
2639
2330
  );
2640
- const single = selection.length === 1 ? selection[0] : null;
2641
- const startTextEditing = useCallback((id, field) => {
2642
- setEditingText({ id, field });
2643
- setSelectedIds(/* @__PURE__ */ new Set([id]));
2644
- }, []);
2645
- const capture = (e) => {
2646
- svgRef.current?.setPointerCapture(e.pointerId);
2647
- };
2648
- const handleBackgroundPointerDown = useCallback(
2649
- (e) => {
2650
- if (!isEditable || editingText || e.button !== 0) return;
2651
- const point = getPoint(e);
2652
- capture(e);
2653
- if (tool === "select") {
2654
- if (!e.shiftKey) setSelectedIds(EMPTY_SET);
2655
- dragRef.current = { mode: "marquee", origin: point, current: point, additive: e.shiftKey };
2656
- return;
2657
- }
2658
- if (tool === "text") {
2659
- const id2 = createShapeId();
2660
- const size = textBoxSize("");
2661
- updateShapes((prev) => [
2662
- ...prev,
2331
+ }
2332
+
2333
+ // src/drawing/shapes/render.tsx
2334
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2335
+ var n = (v) => Number.isInteger(v) ? String(v) : v.toFixed(2);
2336
+ function BoxGeometry({
2337
+ shape,
2338
+ stroke
2339
+ }) {
2340
+ const b = bbox(shape);
2341
+ const fill = shape.fill;
2342
+ switch (shape.type) {
2343
+ case "rect":
2344
+ return /* @__PURE__ */ jsx4(
2345
+ "rect",
2346
+ {
2347
+ ...stroke,
2348
+ x: b.x,
2349
+ y: b.y,
2350
+ width: b.w,
2351
+ height: b.h,
2352
+ rx: Math.min(8, b.w / 4, b.h / 4),
2353
+ fill
2354
+ }
2355
+ );
2356
+ case "ellipse":
2357
+ return /* @__PURE__ */ jsx4(
2358
+ "ellipse",
2359
+ {
2360
+ ...stroke,
2361
+ cx: b.x + b.w / 2,
2362
+ cy: b.y + b.h / 2,
2363
+ rx: b.w / 2,
2364
+ ry: b.h / 2,
2365
+ fill
2366
+ }
2367
+ );
2368
+ case "diamond":
2369
+ return /* @__PURE__ */ jsx4(
2370
+ "polygon",
2371
+ {
2372
+ ...stroke,
2373
+ points: `${n(b.x + b.w / 2)},${n(b.y)} ${n(b.x + b.w)},${n(b.y + b.h / 2)} ${n(b.x + b.w / 2)},${n(b.y + b.h)} ${n(b.x)},${n(b.y + b.h / 2)}`,
2374
+ fill
2375
+ }
2376
+ );
2377
+ case "note": {
2378
+ const f = NOTE_FOLD(b);
2379
+ const r = b.x + b.w;
2380
+ const btm = b.y + b.h;
2381
+ return /* @__PURE__ */ jsxs4("g", { children: [
2382
+ /* @__PURE__ */ jsx4(
2383
+ "path",
2663
2384
  {
2664
- id: id2,
2665
- type: "text",
2666
- x: point.x,
2667
- y: point.y - FONT_SIZE / 2,
2668
- w: size.w,
2669
- h: size.h,
2670
- stroke,
2671
- fill: "transparent",
2672
- strokeWidth: 2,
2673
- text: ""
2385
+ ...stroke,
2386
+ d: `M ${n(b.x)} ${n(b.y)} H ${n(r)} V ${n(btm - f)} L ${n(r - f)} ${n(btm)} H ${n(b.x)} Z`,
2387
+ fill
2674
2388
  }
2675
- ]);
2676
- setTool("select");
2677
- setSelectedIds(/* @__PURE__ */ new Set([id2]));
2678
- pendingTextEditRef.current = id2;
2679
- return;
2680
- }
2681
- const id = createShapeId();
2682
- const def = isBoxType(tool) ? BOX_DEFINITIONS[tool] : null;
2683
- const shapeFill = def ? fill === "transparent" && def.defaultFill ? def.defaultFill : fill : "transparent";
2684
- dragRef.current = { mode: "draw", id, origin: point };
2685
- setSelectedIds(EMPTY_SET);
2686
- updateShapes((prev) => [
2687
- ...prev,
2688
- { id, type: tool, x: point.x, y: point.y, w: 0, h: 0, stroke, fill: shapeFill, strokeWidth: 2 }
2689
- ]);
2690
- },
2691
- [isEditable, editingText, getPoint, tool, stroke, fill, updateShapes, startTextEditing]
2692
- );
2693
- const handleShapePointerDown = useCallback(
2694
- (e, shape) => {
2695
- if (!isEditable || tool !== "select" || editingText || e.button !== 0) return;
2696
- e.stopPropagation();
2697
- const point = getPoint(e);
2698
- capture(e);
2699
- const selected = selectedRef.current;
2700
- if (e.shiftKey) {
2701
- if (selected.has(shape.id)) {
2702
- const next2 = new Set(selected);
2703
- next2.delete(shape.id);
2704
- setSelectedIds(next2);
2705
- return;
2706
- }
2707
- const next = new Set(selected).add(shape.id);
2708
- setSelectedIds(next);
2709
- dragRef.current = {
2710
- mode: "move",
2711
- clickedId: shape.id,
2712
- origin: point,
2713
- origs: moveGroup(shapesRef.current, next, shape.id),
2714
- wasSelected: false
2715
- };
2716
- return;
2717
- }
2718
- const wasSelected = selected.size === 1 && selected.has(shape.id);
2719
- const group = selected.has(shape.id) ? selected : /* @__PURE__ */ new Set([shape.id]);
2720
- if (!selected.has(shape.id)) setSelectedIds(group);
2721
- dragRef.current = {
2722
- mode: "move",
2723
- clickedId: shape.id,
2724
- origin: point,
2725
- origs: moveGroup(shapesRef.current, group, shape.id),
2726
- wasSelected
2727
- };
2728
- },
2729
- [isEditable, tool, editingText, getPoint]
2730
- );
2731
- const handleHandlePointerDown = useCallback(
2732
- (e, drag) => {
2733
- if (!isEditable || e.button !== 0) return;
2734
- e.stopPropagation();
2735
- capture(e);
2736
- dragRef.current = drag;
2737
- if (drag.mode === "endpoint") {
2738
- const key = drag.end === "start" ? "startBinding" : "endBinding";
2739
- updateShapes(
2740
- (prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
2741
- );
2742
- }
2743
- },
2744
- [isEditable, updateShapes]
2745
- );
2746
- const flushPointer = useCallback(() => {
2747
- frameRef.current = null;
2748
- const point = pendingPointRef.current;
2749
- const drag = dragRef.current;
2750
- if (!point || !drag) return;
2751
- if (drag.mode === "marquee") {
2752
- drag.current = point;
2753
- setMarquee({ origin: drag.origin, current: point });
2754
- return;
2755
- }
2756
- updateShapes((prev) => applyDrag(prev, drag, point));
2757
- if (drag.mode === "endpoint" || drag.mode === "draw" && isConnectorType(shapesRef.current.find((s) => s.id === drag.id)?.type ?? "")) {
2758
- const box = findBoxAt(shapesRef.current, point, drag.id);
2759
- setHoverBoxId((prev) => prev === (box?.id ?? null) ? prev : box?.id ?? null);
2389
+ ),
2390
+ /* @__PURE__ */ jsx4(
2391
+ "path",
2392
+ {
2393
+ ...stroke,
2394
+ d: `M ${n(r - f)} ${n(btm)} V ${n(btm - f)} H ${n(r)}`,
2395
+ fill: "rgba(0,0,0,0.08)"
2396
+ }
2397
+ )
2398
+ ] });
2760
2399
  }
2761
- }, [updateShapes]);
2762
- const handlePointerMove = useCallback(
2763
- (e) => {
2764
- if (!dragRef.current) return;
2765
- pendingPointRef.current = getPoint(e);
2766
- if (frameRef.current === null) {
2767
- frameRef.current = requestAnimationFrame(flushPointer);
2768
- }
2769
- },
2770
- [getPoint, flushPointer]
2771
- );
2772
- const handlePointerUp = useCallback(
2773
- (e) => {
2774
- const drag = dragRef.current;
2775
- dragRef.current = null;
2776
- if (frameRef.current !== null) {
2777
- cancelAnimationFrame(frameRef.current);
2778
- frameRef.current = null;
2779
- }
2780
- pendingPointRef.current = null;
2781
- setHoverBoxId(null);
2782
- if (pendingTextEditRef.current) {
2783
- const id = pendingTextEditRef.current;
2784
- pendingTextEditRef.current = null;
2785
- startTextEditing(id, "text");
2786
- return;
2787
- }
2788
- if (!drag) return;
2789
- const point = getPoint(e);
2790
- if (drag.mode === "marquee") {
2791
- setMarquee(null);
2792
- const rect = marqueeRect(drag.origin, point);
2793
- if (rect.w < CLICK_TOLERANCE && rect.h < CLICK_TOLERANCE) return;
2794
- const ids = shapesWithin(shapesRef.current, rect, paths);
2795
- setSelectedIds((prev) => drag.additive ? /* @__PURE__ */ new Set([...prev, ...ids]) : new Set(ids));
2796
- return;
2797
- }
2798
- if (drag.mode === "move" && drag.wasSelected) {
2799
- const moved = Math.abs(point.x - drag.origin.x) > CLICK_TOLERANCE || Math.abs(point.y - drag.origin.y) > CLICK_TOLERANCE;
2800
- const current = shapesRef.current.find((s) => s.id === drag.clickedId);
2801
- if (!moved && current) {
2802
- updateShapes((prev) => applyDrag(prev, drag, drag.origin));
2803
- if (current.type === "text" || isConnectorType(current.type)) {
2804
- startTextEditing(current.id, "text");
2805
- } else {
2806
- startTextEditing(current.id, slotAt(current, drag.origin.y));
2400
+ case "cylinder": {
2401
+ const ry = CYLINDER_RY(b);
2402
+ const rx = b.w / 2;
2403
+ const cx = b.x + rx;
2404
+ return /* @__PURE__ */ jsxs4("g", { children: [
2405
+ /* @__PURE__ */ jsx4(
2406
+ "path",
2407
+ {
2408
+ ...stroke,
2409
+ d: `M ${n(b.x)} ${n(b.y + ry)} V ${n(b.y + b.h - ry)} A ${n(rx)} ${n(ry)} 0 0 0 ${n(b.x + b.w)} ${n(b.y + b.h - ry)} V ${n(b.y + ry)}`,
2410
+ fill
2807
2411
  }
2808
- return;
2412
+ ),
2413
+ /* @__PURE__ */ jsx4("ellipse", { ...stroke, cx, cy: b.y + ry, rx, ry, fill })
2414
+ ] });
2415
+ }
2416
+ case "cloud": {
2417
+ const p = (u, v) => `${n(b.x + u * b.w)} ${n(b.y + v * b.h)}`;
2418
+ return /* @__PURE__ */ jsx4(
2419
+ "path",
2420
+ {
2421
+ ...stroke,
2422
+ d: `M ${p(0.22, 0.86)} C ${p(0.04, 0.88)} ${p(0, 0.58)} ${p(0.16, 0.5)} C ${p(0.08, 0.26)} ${p(0.3, 0.12)} ${p(0.42, 0.28)} C ${p(0.5, 0.02)} ${p(0.76, 0.04)} ${p(0.78, 0.3)} C ${p(0.98, 0.26)} ${p(1.04, 0.56)} ${p(0.88, 0.64)} C ${p(1, 0.8)} ${p(0.9, 0.9)} ${p(0.76, 0.86)} Z`,
2423
+ fill
2809
2424
  }
2810
- }
2811
- if (drag.mode === "draw") {
2812
- const drawn = shapesRef.current.find((s) => s.id === drag.id);
2813
- if (drawn && Math.abs(drawn.w) < 4 && Math.abs(drawn.h) < 4) {
2814
- if (drawn && isBoxType(drawn.type)) {
2815
- const size = BOX_DEFINITIONS[drawn.type].defaultSize;
2816
- updateShapes(
2817
- (prev) => prev.map(
2818
- (s) => s.id === drag.id ? { ...s, x: s.x - size.w / 2, y: s.y - size.h / 2, w: size.w, h: size.h } : s
2819
- ),
2820
- { commit: true }
2821
- );
2822
- setTool("select");
2823
- setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
2824
- } else {
2825
- updateShapes((prev) => prev.filter((s) => s.id !== drag.id));
2425
+ );
2426
+ }
2427
+ case "queue": {
2428
+ const rx = QUEUE_RX(b);
2429
+ const ry = b.h / 2;
2430
+ return /* @__PURE__ */ jsxs4("g", { children: [
2431
+ /* @__PURE__ */ jsx4(
2432
+ "path",
2433
+ {
2434
+ ...stroke,
2435
+ d: `M ${n(b.x + rx)} ${n(b.y)} H ${n(b.x + b.w - rx)} V ${n(b.y + b.h)} H ${n(b.x + rx)} A ${n(rx)} ${n(ry)} 0 0 1 ${n(b.x + rx)} ${n(b.y)} Z`,
2436
+ fill
2826
2437
  }
2827
- return;
2828
- }
2829
- if (drawn && isConnectorType(drawn.type)) {
2830
- updateShapes((prev) => prev.map((s) => s.id === drag.id ? bindEndpoints(s, prev) : s));
2831
- }
2832
- setTool("select");
2833
- setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
2834
- }
2835
- if (drag.mode === "endpoint") {
2836
- updateShapes(
2837
- (prev) => prev.map((s) => {
2838
- if (s.id !== drag.id) return s;
2839
- const end = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
2840
- const box = findBoxAt(prev, end, s.id);
2841
- const other = drag.end === "start" ? s.endBinding : s.startBinding;
2842
- if (box && box.id === other?.id) return s;
2843
- const key = drag.end === "start" ? "startBinding" : "endBinding";
2844
- return { ...s, [key]: box ? makeBinding(box, end) : void 0 };
2845
- })
2846
- );
2847
- }
2848
- updateShapes((prev) => prev.map(normalize), { commit: true });
2849
- },
2850
- [getPoint, paths, updateShapes, startTextEditing]
2851
- );
2852
- const deleteSelection = useCallback(() => {
2853
- const ids = selectedRef.current;
2854
- if (!ids.size) return;
2855
- setEditingText(null);
2856
- setSelectedIds(EMPTY_SET);
2857
- updateShapes(
2858
- (prev) => prev.filter(
2859
- (s) => !ids.has(s.id) && !(s.startBinding && ids.has(s.startBinding.id)) && !(s.endBinding && ids.has(s.endBinding.id))
2860
- ),
2861
- { commit: true }
2862
- );
2863
- }, [updateShapes]);
2864
- const editingRef = useRef4(editingText);
2865
- editingRef.current = editingText;
2866
- useEffect7(() => {
2867
- const root = rootRef.current;
2868
- if (!root || !isEditable) return;
2869
- const onKeyDown = (e) => {
2870
- if (editingRef.current) return;
2871
- const ids = selectedRef.current;
2872
- if ((e.key === "Delete" || e.key === "Backspace") && ids.size) {
2873
- e.preventDefault();
2874
- e.stopPropagation();
2875
- deleteSelection();
2876
- } else if (e.key === "Enter" && ids.size === 1) {
2877
- const [id] = ids;
2878
- const shape = shapesRef.current.find((s) => s.id === id);
2879
- if (shape) {
2880
- e.preventDefault();
2881
- e.stopPropagation();
2882
- startTextEditing(shape.id, "text");
2883
- }
2884
- } else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
2885
- e.preventDefault();
2886
- e.stopPropagation();
2887
- setSelectedIds(new Set(shapesRef.current.map((s) => s.id)));
2888
- } else if (e.key === "Escape") {
2889
- e.stopPropagation();
2890
- setSelectedIds(EMPTY_SET);
2891
- setTool("select");
2892
- }
2893
- };
2894
- root.addEventListener("keydown", onKeyDown);
2895
- return () => root.removeEventListener("keydown", onKeyDown);
2896
- }, [isEditable, deleteSelection, startTextEditing]);
2897
- const applyToSelection = useCallback(
2898
- (patch) => {
2899
- const ids = selectedRef.current;
2900
- if (!ids.size) return;
2901
- updateShapes((prev) => prev.map((s) => ids.has(s.id) ? patch(s) : s), { commit: true });
2902
- },
2903
- [updateShapes]
2904
- );
2905
- const applyStroke = useCallback(
2906
- (color) => {
2907
- setStroke(color);
2908
- applyToSelection((s) => ({ ...s, stroke: color }));
2909
- },
2910
- [applyToSelection]
2911
- );
2912
- const applyFill = useCallback(
2913
- (color) => {
2914
- setFill(color);
2915
- applyToSelection((s) => isBoxType(s.type) ? { ...s, fill: color } : s);
2916
- },
2917
- [applyToSelection]
2918
- );
2919
- const applyRouting = useCallback(
2920
- (elbow) => applyToSelection(
2921
- (s) => isConnectorType(s.type) ? { ...s, routing: elbow ? "elbow" : void 0, elbow: void 0, waypoints: void 0 } : s
2922
- ),
2923
- [applyToSelection]
2924
- );
2925
- const applyDirection = useCallback(
2926
- (bidirectional) => applyToSelection(
2927
- (s) => s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
2928
- ),
2929
- [applyToSelection]
2930
- );
2931
- const applyWidth = useCallback(
2932
- (next) => {
2933
- setWidth(next);
2934
- commit(shapesRef.current, { width: next });
2935
- },
2936
- [commit]
2937
- );
2938
- const addWaypoint = useCallback(
2939
- (e, shapeId, segmentIndex, point) => {
2940
- if (!isEditable || e.button !== 0) return;
2941
- e.stopPropagation();
2942
- capture(e);
2943
- updateShapes(
2944
- (prev) => prev.map((s) => {
2945
- if (s.id !== shapeId || !isConnectorType(s.type)) return s;
2946
- const interior = connectorPoints(s, prev).slice(1, -1);
2947
- const waypoints = [...interior.slice(0, segmentIndex), point, ...interior.slice(segmentIndex)];
2948
- return { ...s, waypoints, routing: void 0, elbow: void 0 };
2949
- })
2950
- );
2951
- dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
2952
- },
2953
- [isEditable, updateShapes]
2954
- );
2955
- const removeWaypoint = useCallback(
2956
- (shapeId, index) => {
2957
- updateShapes(
2958
- (prev) => prev.map((s) => {
2959
- if (s.id !== shapeId || !s.waypoints) return s;
2960
- const waypoints = s.waypoints.filter((_, i) => i !== index);
2961
- return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
2962
- }),
2963
- { commit: true }
2964
- );
2965
- },
2966
- [updateShapes]
2967
- );
2968
- const commitText = useCallback(
2969
- (id, field, value) => {
2970
- setEditingText(null);
2971
- rootRef.current?.focus({ preventScroll: true });
2972
- updateShapes(
2973
- (prev) => prev.flatMap((s) => {
2974
- if (s.id !== id) return [s];
2975
- if (s.type === "text") {
2976
- return value.trim() === "" ? [] : [{ ...s, text: value, ...textBoxSize(value) }];
2438
+ ),
2439
+ /* @__PURE__ */ jsx4(
2440
+ "ellipse",
2441
+ {
2442
+ ...stroke,
2443
+ cx: b.x + b.w - rx,
2444
+ cy: b.y + ry,
2445
+ rx,
2446
+ ry,
2447
+ fill
2977
2448
  }
2978
- return [{ ...s, [field]: value.trim() === "" ? void 0 : value }];
2979
- }),
2980
- { commit: true }
2981
- );
2982
- },
2983
- [updateShapes]
2984
- );
2985
- const copyMermaid = useCallback(async () => {
2986
- const payload = { version: 2, canvasHeight: heightRef.current, shapes: shapesRef.current };
2987
- try {
2988
- await navigator.clipboard.writeText(drawingToMermaid(payload));
2989
- return true;
2990
- } catch {
2991
- return false;
2449
+ )
2450
+ ] });
2992
2451
  }
2993
- }, []);
2994
- const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
2995
- const hoverBox = hoverBoxId ? shapes.find((s) => s.id === hoverBoxId) : null;
2996
- const propStroke = single?.stroke ?? selection[0]?.stroke ?? stroke;
2997
- const propFill = single?.fill ?? selection.find((s) => isBoxType(s.type))?.fill ?? fill;
2998
- const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
2999
- return /* @__PURE__ */ jsxs8(
3000
- "div",
3001
- {
3002
- ref: rootRef,
3003
- className: `zui-drawing-canvas ${WIDTH_CLASS[width]} ${isEditable ? "is-editable" : ""}`,
3004
- tabIndex: isEditable ? 0 : void 0,
3005
- onFocus: () => editor.dispatchCommand(DRAWING_FOCUS_COMMAND, nodeKey),
3006
- onBlur: (e) => {
3007
- if (rootRef.current?.contains(e.relatedTarget)) return;
3008
- editor.dispatchCommand(DRAWING_FOCUS_COMMAND, null);
2452
+ case "actor": {
2453
+ const figH = b.h * ACTOR_FIGURE_RATIO;
2454
+ const cx = b.x + b.w / 2;
2455
+ const r = Math.max(4, Math.min(figH * 0.16, b.w * 0.2));
2456
+ const neck = b.y + r * 2;
2457
+ const hip = b.y + figH * 0.62;
2458
+ const armY = neck + figH * 0.12;
2459
+ const reach = Math.min(b.w * 0.32, figH * 0.3);
2460
+ return /* @__PURE__ */ jsxs4("g", { children: [
2461
+ /* @__PURE__ */ jsx4("circle", { ...stroke, cx, cy: b.y + r, r, fill }),
2462
+ /* @__PURE__ */ jsx4(
2463
+ "path",
2464
+ {
2465
+ ...stroke,
2466
+ fill: "none",
2467
+ d: `M ${n(cx)} ${n(neck)} V ${n(hip)} M ${n(cx - reach)} ${n(armY)} H ${n(cx + reach)} M ${n(cx)} ${n(hip)} L ${n(cx - reach)} ${n(b.y + figH)} M ${n(cx)} ${n(hip)} L ${n(cx + reach)} ${n(b.y + figH)}`
2468
+ }
2469
+ )
2470
+ ] });
2471
+ }
2472
+ default:
2473
+ return null;
2474
+ }
2475
+ }
2476
+
2477
+ // src/drawing/canvas/ShapeView.tsx
2478
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2479
+ import { createElement } from "react";
2480
+ var CONNECTOR_FONT_SIZE = 12;
2481
+ var CONNECTOR_LABEL_MAX_WIDTH = 160;
2482
+ function slotLayout(shape) {
2483
+ const def = boxDefinition(shape);
2484
+ return {
2485
+ area: def ? def.textArea(shape) : bbox(shape),
2486
+ slots: def ? def.textSlots : ["label", "text", "footer"]
2487
+ };
2488
+ }
2489
+ function slotAt(shape, y) {
2490
+ const { area, slots } = slotLayout(shape);
2491
+ const rel = (y - area.y) / Math.max(area.h, 1);
2492
+ if (rel < 0.3 && slots.includes("label")) return "label";
2493
+ if (rel > 0.7 && slots.includes("footer")) return "footer";
2494
+ return slots.includes("text") ? "text" : slots[0];
2495
+ }
2496
+ var textStyle = { userSelect: "none" };
2497
+ function luminance(color) {
2498
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);
2499
+ if (!m) return 1;
2500
+ const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
2501
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
2502
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
2503
+ }
2504
+ function textColorFor(shape) {
2505
+ if (shape.stroke === "transparent" || shape.stroke === "none") return "#1e1e1e";
2506
+ if (luminance(shape.fill) < 0.35) return "#ffffff";
2507
+ return shape.stroke;
2508
+ }
2509
+ var hintStyle = { userSelect: "none", pointerEvents: "none" };
2510
+ function BoxTexts({
2511
+ shape,
2512
+ hideField,
2513
+ showHints
2514
+ }) {
2515
+ const { area, slots } = slotLayout(shape);
2516
+ const cx = area.x + area.w / 2;
2517
+ const wrapW = Math.max(area.w, 20);
2518
+ const hints = showHints && area.h >= 56 && area.w >= 44;
2519
+ const mainLines = wrapText(shape.text ?? "", wrapW, FONT_SIZE);
2520
+ const labelLines = wrapText(shape.label ?? "", wrapW, SMALL_FONT_SIZE);
2521
+ const footerLines = wrapText(shape.footer ?? "", wrapW, SMALL_FONT_SIZE);
2522
+ const mainLineH = FONT_SIZE * LINE_HEIGHT;
2523
+ const smallLineH = SMALL_FONT_SIZE * LINE_HEIGHT;
2524
+ const mainStart = area.y + area.h / 2 - (mainLines.length - 1) * mainLineH / 2 + FONT_SIZE * 0.35;
2525
+ const common = {
2526
+ fill: textColorFor(shape),
2527
+ textAnchor: "middle"
2528
+ };
2529
+ return /* @__PURE__ */ jsxs5("g", { children: [
2530
+ slots.includes("label") && hideField !== "label" && (shape.label ? labelLines.map((line, i) => /* @__PURE__ */ createElement(
2531
+ "text",
2532
+ {
2533
+ ...common,
2534
+ key: i,
2535
+ x: cx,
2536
+ y: area.y + SMALL_FONT_SIZE + i * smallLineH,
2537
+ fontSize: SMALL_FONT_SIZE,
2538
+ fontWeight: 600,
2539
+ letterSpacing: 0.3,
2540
+ opacity: 0.85,
2541
+ style: textStyle
3009
2542
  },
3010
- children: [
3011
- isEditable && /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: [
3012
- /* @__PURE__ */ jsx9(
3013
- DrawingToolbar,
3014
- {
3015
- tool,
3016
- onToolChange: (t) => {
3017
- setTool(t);
3018
- if (t !== "select") setSelectedIds(EMPTY_SET);
3019
- },
3020
- width,
3021
- onWidthChange: applyWidth,
3022
- onCopyMermaid: copyMermaid
3023
- }
3024
- ),
3025
- /* @__PURE__ */ jsx9("div", { className: "zui-drawing-props-host", children: /* @__PURE__ */ jsx9(
3026
- PropertyBar,
3027
- {
3028
- selection,
3029
- showFill: tool !== "select" && isBoxType(tool),
3030
- stroke: propStroke,
3031
- fill: propFill,
3032
- onStroke: applyStroke,
3033
- onFill: applyFill,
3034
- onRouting: applyRouting,
3035
- onDirection: applyDirection,
3036
- onDelete: deleteSelection
3037
- }
3038
- ) })
3039
- ] }),
3040
- /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
3041
- /* @__PURE__ */ jsxs8(
3042
- "svg",
3043
- {
3044
- ref: svgRef,
3045
- className: "zui-drawing-surface",
3046
- style: {
3047
- height: logicalWidth ? void 0 : canvasHeight,
3048
- width: logicalWidth ?? void 0,
3049
- maxWidth: "100%",
3050
- aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
3051
- cursor: canvasCursor
3052
- },
3053
- viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
3054
- preserveAspectRatio: "xMinYMin meet",
3055
- onPointerDown: handleBackgroundPointerDown,
3056
- onPointerMove: handlePointerMove,
3057
- onPointerUp: handlePointerUp,
3058
- onPointerCancel: handlePointerUp,
3059
- onDoubleClick: (e) => {
3060
- if (!isEditable) return;
3061
- const target = e.target.closest("[data-shape-id]");
3062
- const id = target?.getAttribute("data-shape-id");
3063
- const shape = id ? shapes.find((s) => s.id === id) : null;
3064
- if (!shape) return;
3065
- if (shape.type === "text" || isConnectorType(shape.type)) {
3066
- startTextEditing(shape.id, "text");
3067
- } else {
3068
- startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
3069
- }
3070
- },
3071
- children: [
3072
- shapes.map((shape) => /* @__PURE__ */ jsxs8(
3073
- "g",
3074
- {
3075
- "data-shape-id": shape.id,
3076
- className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
3077
- style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
3078
- onPointerDown: (e) => handleShapePointerDown(e, shape),
3079
- children: [
3080
- /* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
3081
- shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
3082
- ShapeView,
3083
- {
3084
- shape,
3085
- points: paths.get(shape.id),
3086
- hideField: shape.id === editingText?.id ? editingText.field : null,
3087
- showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
3088
- }
3089
- )
3090
- ]
3091
- },
3092
- shape.id
3093
- )),
3094
- hoverBox && /* @__PURE__ */ jsx9(
3095
- "polygon",
3096
- {
3097
- className: "zui-drawing-selection zui-drawing-bind-target",
3098
- points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
3099
- fill: "currentColor",
3100
- fillOpacity: 0.06,
3101
- stroke: "currentColor",
3102
- strokeWidth: 2,
3103
- strokeLinejoin: "round",
3104
- pointerEvents: "none"
3105
- }
3106
- ),
3107
- single && isEditable && !editingText && /* @__PURE__ */ jsx9(
3108
- SelectionOverlay,
3109
- {
3110
- shape: single,
3111
- points: paths.get(single.id) ?? [],
3112
- onHandlePointerDown: handleHandlePointerDown,
3113
- onWaypointAdd: addWaypoint,
3114
- onWaypointRemove: removeWaypoint
3115
- }
3116
- ),
3117
- selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
3118
- marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
3119
- ]
3120
- }
3121
- ),
3122
- isEditable && shapes.length === 0 && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-empty", "aria-hidden": "true", children: "Pick a shape above, then click or drag on the canvas" }),
3123
- editingShape && editingText && /* @__PURE__ */ jsx9(
3124
- "div",
3125
- {
3126
- className: "zui-drawing-overlay",
3127
- style: {
3128
- transform: `scale(${scale})`,
3129
- width: logicalWidth ?? void 0,
3130
- height: logicalWidth ? canvasHeight : void 0
3131
- },
3132
- children: /* @__PURE__ */ jsx9(
3133
- TextEditOverlay,
3134
- {
3135
- shape: editingShape,
3136
- field: editingText.field,
3137
- points: paths.get(editingShape.id),
3138
- onCommit: commitText
3139
- },
3140
- `${editingShape.id}:${editingText.field}`
3141
- )
3142
- }
3143
- ),
3144
- isEditable && /* @__PURE__ */ jsx9(
3145
- HeightHandle,
3146
- {
3147
- height: canvasHeight,
3148
- scale,
3149
- onChange: setCanvasHeight,
3150
- onCommit: (h) => commit(shapesRef.current, { height: h })
3151
- }
3152
- )
3153
- ] })
3154
- ]
3155
- }
3156
- );
3157
- }
3158
- function HeightHandle({
3159
- height,
3160
- scale,
3161
- onChange,
3162
- onCommit
3163
- }) {
3164
- const dragRef = useRef4(null);
3165
- const latestRef = useRef4(height);
3166
- latestRef.current = height;
3167
- const clamp = (h) => Math.round(Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, h)));
3168
- return /* @__PURE__ */ jsx9(
3169
- "div",
3170
- {
3171
- className: "zui-drawing-resize",
3172
- title: "Drag to resize the canvas",
3173
- role: "separator",
3174
- "aria-orientation": "horizontal",
3175
- onPointerDown: (e) => {
3176
- e.preventDefault();
3177
- e.stopPropagation();
3178
- e.currentTarget.setPointerCapture(e.pointerId);
3179
- dragRef.current = { startY: e.clientY, orig: height };
2543
+ line
2544
+ )) : hints && /* @__PURE__ */ jsx5(
2545
+ "text",
2546
+ {
2547
+ ...common,
2548
+ x: cx,
2549
+ y: area.y + SMALL_FONT_SIZE,
2550
+ fontSize: SMALL_FONT_SIZE,
2551
+ opacity: 0.3,
2552
+ style: hintStyle,
2553
+ children: "Label"
2554
+ }
2555
+ )),
2556
+ slots.includes("text") && hideField !== "text" && (shape.text ? mainLines.map((line, i) => /* @__PURE__ */ createElement(
2557
+ "text",
2558
+ {
2559
+ ...common,
2560
+ key: i,
2561
+ x: cx,
2562
+ y: mainStart + i * mainLineH,
2563
+ fontSize: FONT_SIZE,
2564
+ style: textStyle
3180
2565
  },
3181
- onPointerMove: (e) => {
3182
- const drag = dragRef.current;
3183
- if (!drag) return;
3184
- onChange(clamp(drag.orig + (e.clientY - drag.startY) / Math.max(scale, 0.05)));
2566
+ line
2567
+ )) : hints && /* @__PURE__ */ jsx5(
2568
+ "text",
2569
+ {
2570
+ ...common,
2571
+ x: cx,
2572
+ y: mainStart,
2573
+ fontSize: FONT_SIZE,
2574
+ opacity: 0.3,
2575
+ style: hintStyle,
2576
+ children: "Text"
2577
+ }
2578
+ )),
2579
+ slots.includes("footer") && hideField !== "footer" && (shape.footer ? footerLines.map((line, i) => /* @__PURE__ */ createElement(
2580
+ "text",
2581
+ {
2582
+ ...common,
2583
+ key: i,
2584
+ x: cx,
2585
+ y: area.y + area.h - 3 - (footerLines.length - 1 - i) * smallLineH,
2586
+ fontSize: SMALL_FONT_SIZE,
2587
+ opacity: 0.65,
2588
+ style: textStyle
3185
2589
  },
3186
- onPointerUp: () => {
3187
- if (!dragRef.current) return;
3188
- dragRef.current = null;
3189
- onCommit(latestRef.current);
2590
+ line
2591
+ )) : hints && /* @__PURE__ */ jsx5(
2592
+ "text",
2593
+ {
2594
+ ...common,
2595
+ x: cx,
2596
+ y: area.y + area.h - 3,
2597
+ fontSize: SMALL_FONT_SIZE,
2598
+ opacity: 0.3,
2599
+ style: hintStyle,
2600
+ children: "Footer"
2601
+ }
2602
+ ))
2603
+ ] });
2604
+ }
2605
+ function ConnectorLabel({
2606
+ shape,
2607
+ points
2608
+ }) {
2609
+ const text = shape.text ?? "";
2610
+ const lines = wrapText(text, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
2611
+ const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
2612
+ const { x: midX, y: midY } = connectorMidpoint(points);
2613
+ const lineH = CONNECTOR_FONT_SIZE * LINE_HEIGHT;
2614
+ const startY = midY - (lines.length - 1) * lineH / 2 + CONNECTOR_FONT_SIZE * 0.35;
2615
+ return /* @__PURE__ */ jsxs5("g", { children: [
2616
+ /* @__PURE__ */ jsx5(
2617
+ "rect",
2618
+ {
2619
+ x: midX - size.w / 2 - 5,
2620
+ y: midY - size.h / 2 - 3,
2621
+ width: size.w + 10,
2622
+ height: size.h + 6,
2623
+ rx: 5,
2624
+ className: "zui-drawing-label-plate",
2625
+ opacity: 0.94
2626
+ }
2627
+ ),
2628
+ lines.map((line, i) => /* @__PURE__ */ jsx5(
2629
+ "text",
2630
+ {
2631
+ x: midX,
2632
+ y: startY + i * lineH,
2633
+ fill: shape.stroke,
2634
+ fontSize: CONNECTOR_FONT_SIZE,
2635
+ textAnchor: "middle",
2636
+ style: textStyle,
2637
+ children: line
3190
2638
  },
3191
- children: /* @__PURE__ */ jsx9(Icon, { size: 14, children: UI_ICONS.grip })
2639
+ i
2640
+ ))
2641
+ ] });
2642
+ }
2643
+ function ShapeView({
2644
+ shape,
2645
+ points,
2646
+ hideField,
2647
+ showHints
2648
+ }) {
2649
+ const stroke = {
2650
+ stroke: shape.stroke,
2651
+ strokeWidth: shape.strokeWidth,
2652
+ strokeLinecap: "round",
2653
+ strokeLinejoin: "round"
2654
+ };
2655
+ if (isBoxType(shape.type)) {
2656
+ return /* @__PURE__ */ jsxs5("g", { children: [
2657
+ /* @__PURE__ */ jsx5(BoxGeometry, { shape, stroke }),
2658
+ /* @__PURE__ */ jsx5(BoxTexts, { shape, hideField, showHints })
2659
+ ] });
2660
+ }
2661
+ if (isConnectorType(shape.type)) {
2662
+ const pts = points ?? [
2663
+ { x: shape.x, y: shape.y },
2664
+ { x: shape.x + shape.w, y: shape.y + shape.h }
2665
+ ];
2666
+ return /* @__PURE__ */ jsxs5("g", { children: [
2667
+ /* @__PURE__ */ jsx5("path", { ...stroke, d: polylinePath(pts), fill: "none" }),
2668
+ arrowHeads(shape, pts).map((d, i) => /* @__PURE__ */ jsx5("path", { ...stroke, d, fill: "none" }, i)),
2669
+ shape.text && hideField !== "text" && /* @__PURE__ */ jsx5(ConnectorLabel, { shape, points: pts })
2670
+ ] });
2671
+ }
2672
+ return /* @__PURE__ */ jsx5(
2673
+ "text",
2674
+ {
2675
+ x: shape.x,
2676
+ y: shape.y + FONT_SIZE,
2677
+ fill: shape.stroke,
2678
+ fontSize: FONT_SIZE,
2679
+ style: { userSelect: "none", whiteSpace: "pre" },
2680
+ children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx5("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * LINE_HEIGHT, children: line }, i))
3192
2681
  }
3193
2682
  );
3194
2683
  }
3195
-
3196
- // src/drawing/skeleton.ts
3197
- var COLOR_PRESETS = {
3198
- gray: { stroke: "#1e1e1e", fill: "transparent" },
3199
- red: { stroke: "#e03131", fill: "#ffc9c9" },
3200
- green: { stroke: "#2f9e44", fill: "#b2f2bb" },
3201
- blue: { stroke: "#1971c2", fill: "#a5d8ff" },
3202
- orange: { stroke: "#f08c00", fill: "#ffec99" },
3203
- purple: { stroke: "#7048e8", fill: "#d0bfff" }
3204
- };
3205
- var COLOR_NAMES = Object.keys(COLOR_PRESETS);
3206
- var TEXT_WRAP_WIDTH = 260;
3207
- var SLOT_GAP = 6;
3208
- var RANK_GAP = 90;
3209
- var STACK_GAP = 40;
3210
- var LAYOUT_ORIGIN = 32;
3211
- var CANVAS_MARGIN = 32;
3212
- var MIN_AUTO_CANVAS_HEIGHT = 120;
3213
- var DEFAULT_STROKE2 = "#1e1e1e";
3214
- var STROKE_WIDTH = 2;
3215
- function isDrawingSkeleton(value) {
3216
- return isRecord2(value) && Array.isArray(value.boxes);
3217
- }
3218
- function parseDrawingSkeleton(json) {
3219
- try {
3220
- const parsed = JSON.parse(json);
3221
- if (!isDrawingSkeleton(parsed)) return EMPTY_DRAWING;
3222
- return expandSkeleton(normalizeSkeleton(parsed));
3223
- } catch {
3224
- return EMPTY_DRAWING;
2684
+ function HitArea({
2685
+ shape,
2686
+ points
2687
+ }) {
2688
+ if (isConnectorType(shape.type) && points) {
2689
+ return /* @__PURE__ */ jsx5("path", { d: polylinePath(points), fill: "none", stroke: "transparent", strokeWidth: 16 });
3225
2690
  }
3226
- }
3227
- function expandSkeleton(skeleton) {
3228
- const boxes = dedupeById(skeleton.boxes);
3229
- const boxTypes = new Map(boxes.map((box) => [box.id, box.type ?? "rect"]));
3230
- const sizes = new Map(boxes.map((box) => [box.id, measureBox(box, boxTypes.get(box.id))]));
3231
- const boxIds = new Set(boxTypes.keys());
3232
- const connectors = (skeleton.connectors ?? []).filter(
3233
- (c) => boxIds.has(endId(c.from)) && boxIds.has(endId(c.to)) && endId(c.from) !== endId(c.to)
3234
- );
3235
- const positions = layoutBoxes(boxes, sizes, connectors, skeleton.direction ?? "right");
3236
- const boxShapes = boxes.map(
3237
- (box) => expandBox(box, boxTypes.get(box.id), sizes.get(box.id), positions.get(box.id))
2691
+ const b = bbox(shape);
2692
+ return /* @__PURE__ */ jsx5(
2693
+ "rect",
2694
+ {
2695
+ x: b.x - 2,
2696
+ y: b.y - 2,
2697
+ width: Math.max(b.w, 8) + 4,
2698
+ height: Math.max(b.h, 8) + 4,
2699
+ fill: "transparent",
2700
+ stroke: "none"
2701
+ }
3238
2702
  );
3239
- const centers = new Map(boxShapes.map((shape) => [shape.id, shapeCenter(shape)]));
3240
- const used = new Set(boxIds);
3241
- const nextId = (prefix) => {
3242
- let i = used.size + 1;
3243
- while (used.has(`${prefix}${i}`)) i += 1;
3244
- const id = `${prefix}${i}`;
3245
- used.add(id);
3246
- return id;
3247
- };
3248
- const connectorShapes = connectors.map((c) => expandConnector(c, centers, nextId("c")));
3249
- const textShapes = (skeleton.texts ?? []).map((t) => expandText(t, nextId("t")));
3250
- const shapes = resolveBindings([...boxShapes, ...connectorShapes, ...textShapes]);
3251
- const bottom = shapes.reduce((max, shape) => {
3252
- const b = bbox(shape);
3253
- return Math.max(max, b.y + b.h);
3254
- }, 0);
3255
- return normalizeDrawingData({
3256
- version: 2,
3257
- ...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
3258
- canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
3259
- ...skeleton.width !== void 0 ? { width: skeleton.width } : {},
3260
- shapes
3261
- });
3262
2703
  }
3263
- function measureBox(box, type) {
3264
- const def = BOX_DEFINITIONS[type];
3265
- const probe = {
3266
- id: "",
3267
- type,
3268
- x: 0,
3269
- y: 0,
3270
- w: def.defaultSize.w,
3271
- h: def.defaultSize.h,
3272
- stroke: "",
3273
- fill: "",
3274
- strokeWidth: 0
3275
- };
3276
- const area = def.textArea(probe);
3277
- const ratioW = area.w / def.defaultSize.w;
3278
- const ratioH = area.h / def.defaultSize.h;
3279
- const wrapWidth = box.w !== void 0 ? Math.max(20, box.w * ratioW) : TEXT_WRAP_WIDTH;
3280
- const slots = def.textSlots.flatMap((slot) => {
3281
- const size = slotSize(box, slot, wrapWidth);
3282
- return size ? [size] : [];
3283
- });
3284
- const innerW = slots.reduce((max, s) => Math.max(max, s.w), 0);
3285
- const innerH = slots.reduce((sum, s) => sum + s.h, 0) + Math.max(0, slots.length - 1) * SLOT_GAP;
3286
- return {
3287
- w: box.w ?? Math.max(def.defaultSize.w, Math.ceil(innerW / ratioW)),
3288
- h: box.h ?? Math.max(def.defaultSize.h, Math.ceil(innerH / ratioH))
2704
+
2705
+ // src/drawing/canvas/TextEditOverlay.tsx
2706
+ import {
2707
+ useEffect as useEffect5,
2708
+ useRef as useRef2,
2709
+ useState
2710
+ } from "react";
2711
+ import { jsx as jsx6 } from "react/jsx-runtime";
2712
+ function TextEditOverlay({
2713
+ shape,
2714
+ field,
2715
+ points,
2716
+ onCommit
2717
+ }) {
2718
+ const original = shape[field] ?? "";
2719
+ const [value, setValue] = useState(original);
2720
+ const ref = useRef2(null);
2721
+ const valueRef = useRef2(value);
2722
+ valueRef.current = value;
2723
+ useEffect5(() => {
2724
+ const el = ref.current;
2725
+ if (!el) return;
2726
+ el.focus();
2727
+ el.select();
2728
+ const onKeyDown = (e) => {
2729
+ e.stopPropagation();
2730
+ if ((e.key === "Home" || e.key === "End") && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
2731
+ e.preventDefault();
2732
+ const text = el.value;
2733
+ const caret = e.key === "Home" ? el.selectionStart : el.selectionEnd;
2734
+ const pos = e.key === "Home" ? text.lastIndexOf("\n", caret - 1) + 1 : (text.indexOf("\n", caret) + 1 || text.length + 1) - 1;
2735
+ el.setSelectionRange(pos, pos);
2736
+ return;
2737
+ }
2738
+ if (e.key === "Enter" && !e.shiftKey) {
2739
+ e.preventDefault();
2740
+ onCommit(shape.id, field, valueRef.current);
2741
+ } else if (e.key === "Escape") {
2742
+ e.preventDefault();
2743
+ onCommit(shape.id, field, original);
2744
+ }
2745
+ };
2746
+ const stop = (e) => e.stopPropagation();
2747
+ el.addEventListener("keydown", onKeyDown);
2748
+ el.addEventListener("keyup", stop);
2749
+ el.addEventListener("keypress", stop);
2750
+ return () => {
2751
+ el.removeEventListener("keydown", onKeyDown);
2752
+ el.removeEventListener("keyup", stop);
2753
+ el.removeEventListener("keypress", stop);
2754
+ };
2755
+ }, []);
2756
+ const style = {
2757
+ color: textColorFor(shape),
2758
+ fontFamily: "var(--zui-drawing-font)",
2759
+ lineHeight: LINE_HEIGHT
3289
2760
  };
3290
- }
3291
- function slotSize(box, slot, wrapWidth) {
3292
- const value = box[slot];
3293
- if (!value) return null;
3294
- const fontSize = slot === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
3295
- return textBoxSize(wrapText(value, wrapWidth, fontSize).join("\n"), fontSize);
3296
- }
3297
- function layoutBoxes(boxes, sizes, connectors, direction) {
3298
- const positions = /* @__PURE__ */ new Map();
3299
- const auto = boxes.filter((box) => box.x === void 0 || box.y === void 0);
3300
- const autoIds = new Set(auto.map((box) => box.id));
3301
- const edges = connectors.map((c) => ({ from: endId(c.from), to: endId(c.to) })).filter((e) => e.from !== e.to && autoIds.has(e.from) && autoIds.has(e.to));
3302
- const ranks = rankNodes(
3303
- auto.map((box) => box.id),
3304
- acyclicEdges(
3305
- auto.map((box) => box.id),
3306
- edges
3307
- )
3308
- );
3309
- const rows = [];
3310
- for (const box of auto) {
3311
- const rank = ranks.get(box.id) ?? 0;
3312
- (rows[rank] ?? (rows[rank] = [])).push(box.id);
2761
+ if (shape.type === "text") {
2762
+ const size = textBoxSize(value || " ");
2763
+ Object.assign(style, {
2764
+ left: shape.x,
2765
+ top: shape.y,
2766
+ fontSize: FONT_SIZE,
2767
+ width: Math.max(80, size.w + 20),
2768
+ height: size.h + 2,
2769
+ // `pre` would keep the lines, but Chrome ignores Home/End in a
2770
+ // `white-space: pre` textarea; the width tracks the content anyway
2771
+ whiteSpace: "pre-wrap"
2772
+ });
2773
+ } else if (isConnectorType(shape.type)) {
2774
+ const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
2775
+ const size = textBoxSize(lines.join("\n") || " ", CONNECTOR_FONT_SIZE);
2776
+ const width = Math.max(70, size.w + 16);
2777
+ const height = size.h + 4;
2778
+ const mid = connectorMidpoint(
2779
+ points ?? [
2780
+ { x: shape.x, y: shape.y },
2781
+ { x: shape.x + shape.w, y: shape.y + shape.h }
2782
+ ]
2783
+ );
2784
+ Object.assign(style, {
2785
+ left: mid.x - width / 2,
2786
+ top: mid.y - height / 2,
2787
+ width,
2788
+ height,
2789
+ fontSize: CONNECTOR_FONT_SIZE,
2790
+ textAlign: "center",
2791
+ whiteSpace: "pre-wrap"
2792
+ });
2793
+ } else {
2794
+ const { area } = slotLayout(shape);
2795
+ const fontSize = field === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
2796
+ const width = Math.max(area.w, 40);
2797
+ const lines = wrapText(value, width, fontSize).length;
2798
+ const boxH = lines * fontSize * LINE_HEIGHT + 2;
2799
+ const top = field === "label" ? area.y - 1 : field === "footer" ? area.y + area.h - boxH + 1 : area.y + area.h / 2 - boxH / 2;
2800
+ Object.assign(style, {
2801
+ left: area.x,
2802
+ top,
2803
+ width,
2804
+ height: boxH,
2805
+ fontSize,
2806
+ fontWeight: field === "label" ? 600 : void 0,
2807
+ letterSpacing: field === "label" ? 0.3 : void 0,
2808
+ opacity: field === "text" ? 1 : field === "label" ? 0.85 : 0.65,
2809
+ textAlign: "center",
2810
+ whiteSpace: "pre-wrap"
2811
+ });
3313
2812
  }
3314
- const main = direction === "right" ? "w" : "h";
3315
- const cross = direction === "right" ? "h" : "w";
3316
- const rowMain = rows.map((row) => row.reduce((max, id) => Math.max(max, sizes.get(id)[main]), 0));
3317
- const rowCross = rows.map(
3318
- (row) => row.reduce((sum, id) => sum + sizes.get(id)[cross], 0) + Math.max(0, row.length - 1) * STACK_GAP
2813
+ const placeholder = field === "label" ? "Label" : field === "footer" ? "Footer" : "Text";
2814
+ return /* @__PURE__ */ jsx6(
2815
+ "textarea",
2816
+ {
2817
+ ref,
2818
+ className: "zui-drawing-text-input",
2819
+ style,
2820
+ value,
2821
+ placeholder,
2822
+ spellCheck: false,
2823
+ onChange: (e) => setValue(e.target.value),
2824
+ onBlur: () => onCommit(shape.id, field, value),
2825
+ onPointerDown: (e) => e.stopPropagation()
2826
+ }
3319
2827
  );
3320
- const maxCross = rowCross.reduce((max, c) => Math.max(max, c), 0);
3321
- let mainOffset = LAYOUT_ORIGIN;
3322
- rows.forEach((row, rank) => {
3323
- let crossOffset = LAYOUT_ORIGIN + (maxCross - rowCross[rank]) / 2;
3324
- for (const id of row) {
3325
- const size = sizes.get(id);
3326
- const mainPos = Math.round(mainOffset + (rowMain[rank] - size[main]) / 2);
3327
- const crossPos = Math.round(crossOffset);
3328
- positions.set(
3329
- id,
3330
- direction === "right" ? { x: mainPos, y: crossPos } : { x: crossPos, y: mainPos }
3331
- );
3332
- crossOffset += size[cross] + STACK_GAP;
3333
- }
3334
- mainOffset += rowMain[rank] + RANK_GAP;
3335
- });
3336
- for (const box of boxes) {
3337
- const computed = positions.get(box.id) ?? { x: LAYOUT_ORIGIN, y: LAYOUT_ORIGIN };
3338
- positions.set(box.id, { x: box.x ?? computed.x, y: box.y ?? computed.y });
2828
+ }
2829
+
2830
+ // src/drawing/canvas/Toolbar.tsx
2831
+ import { useEffect as useEffect6, useRef as useRef3, useState as useState2 } from "react";
2832
+
2833
+ // src/components/blockWidthOptions.tsx
2834
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2835
+ var LAYOUT_OPTIONS = [
2836
+ {
2837
+ preset: "compact",
2838
+ label: "Compact (shrink to content, tight rows)",
2839
+ width: "content",
2840
+ density: "compact",
2841
+ icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2842
+ /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2843
+ /* @__PURE__ */ jsx7("path", { d: "M5.5 10h3M11.5 10h3M6.5 7.5L9 10l-2.5 2.5M13.5 7.5L11 10l2.5 2.5" })
2844
+ ] })
2845
+ },
2846
+ {
2847
+ preset: "comfortable",
2848
+ label: "Comfortable (text width)",
2849
+ width: "text",
2850
+ density: "comfortable",
2851
+ icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2852
+ /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2853
+ /* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
2854
+ ] })
2855
+ },
2856
+ {
2857
+ preset: "full",
2858
+ label: "Full width",
2859
+ width: "full",
2860
+ density: "comfortable",
2861
+ icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2862
+ /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2863
+ /* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
2864
+ ] })
3339
2865
  }
3340
- return positions;
2866
+ ];
2867
+ function layoutPreset(width) {
2868
+ return width === "content" ? "compact" : width === "text" ? "comfortable" : "full";
3341
2869
  }
3342
- function acyclicEdges(ids, edges) {
3343
- const outgoing = /* @__PURE__ */ new Map();
3344
- for (const edge of edges) (outgoing.get(edge.from) ?? outgoing.set(edge.from, []).get(edge.from)).push(edge);
3345
- const state = /* @__PURE__ */ new Map();
3346
- const kept = [];
3347
- const visit = (id) => {
3348
- state.set(id, "visiting");
3349
- for (const edge of outgoing.get(id) ?? []) {
3350
- const target = state.get(edge.to);
3351
- if (target === "visiting") continue;
3352
- kept.push(edge);
3353
- if (target === void 0) visit(edge.to);
2870
+
2871
+ // src/drawing/canvas/Toolbar.tsx
2872
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2873
+ var PRIMARY_TOOLS = [
2874
+ { tool: "select", label: "Select" },
2875
+ { tool: "rect", label: "Rectangle" },
2876
+ { tool: "ellipse", label: "Ellipse" },
2877
+ { tool: "diamond", label: "Diamond" },
2878
+ { tool: "arrow", label: "Arrow" },
2879
+ { tool: "line", label: "Line" },
2880
+ { tool: "text", label: "Text" }
2881
+ ];
2882
+ var MORE_SHAPES = ["note", "cylinder", "cloud", "queue", "actor"];
2883
+ function ToolButton({
2884
+ label,
2885
+ active,
2886
+ onClick,
2887
+ children,
2888
+ className,
2889
+ pressed
2890
+ }) {
2891
+ return /* @__PURE__ */ jsx8(
2892
+ "button",
2893
+ {
2894
+ type: "button",
2895
+ title: label,
2896
+ "aria-label": label,
2897
+ "aria-pressed": pressed ?? active,
2898
+ className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
2899
+ onClick,
2900
+ children: /* @__PURE__ */ jsx8(Icon, { children })
3354
2901
  }
3355
- state.set(id, "done");
3356
- };
3357
- for (const id of ids) if (!state.has(id)) visit(id);
3358
- return kept;
3359
- }
3360
- function rankNodes(ids, edges) {
3361
- const incoming = /* @__PURE__ */ new Map();
3362
- for (const edge of edges) (incoming.get(edge.to) ?? incoming.set(edge.to, []).get(edge.to)).push(edge.from);
3363
- const ranks = /* @__PURE__ */ new Map();
3364
- const rankOf = (id) => {
3365
- const known = ranks.get(id);
3366
- if (known !== void 0) return known;
3367
- const rank = (incoming.get(id) ?? []).reduce((max, from) => Math.max(max, rankOf(from) + 1), 0);
3368
- ranks.set(id, rank);
3369
- return rank;
3370
- };
3371
- for (const id of ids) rankOf(id);
3372
- return ranks;
2902
+ );
3373
2903
  }
3374
- function expandBox(box, type, size, position) {
3375
- const preset = box.color ? COLOR_PRESETS[box.color] : void 0;
3376
- const fill = box.fill ?? preset?.fill ?? BOX_DEFINITIONS[type].defaultFill ?? "transparent";
3377
- return {
3378
- id: box.id,
3379
- type,
3380
- x: position.x,
3381
- y: position.y,
3382
- w: size.w,
3383
- h: size.h,
3384
- stroke: box.stroke ?? preset?.stroke ?? DEFAULT_STROKE2,
3385
- fill,
3386
- strokeWidth: STROKE_WIDTH,
3387
- ...box.text ? { text: box.text } : {},
3388
- ...box.label ? { label: box.label } : {},
3389
- ...box.footer ? { footer: box.footer } : {}
3390
- };
2904
+ function DrawingToolbar({
2905
+ tool,
2906
+ onToolChange,
2907
+ width,
2908
+ onWidthChange,
2909
+ onCopyMermaid,
2910
+ properties
2911
+ }) {
2912
+ const [moreOpen, setMoreOpen] = useState2(false);
2913
+ const [lastMore, setLastMore] = useState2("note");
2914
+ const [copied, setCopied] = useState2(false);
2915
+ const moreRef = useRef3(null);
2916
+ useEffect6(() => {
2917
+ if (!moreOpen) return;
2918
+ const close = (e) => {
2919
+ if (!moreRef.current?.contains(e.target)) setMoreOpen(false);
2920
+ };
2921
+ const key = (e) => {
2922
+ if (e.key === "Escape") setMoreOpen(false);
2923
+ };
2924
+ document.addEventListener("pointerdown", close);
2925
+ document.addEventListener("keydown", key);
2926
+ return () => {
2927
+ document.removeEventListener("pointerdown", close);
2928
+ document.removeEventListener("keydown", key);
2929
+ };
2930
+ }, [moreOpen]);
2931
+ const moreActive = MORE_SHAPES.includes(tool);
2932
+ const moreIcon = moreActive ? SHAPE_ICONS[tool] : SHAPE_ICONS[lastMore];
2933
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2934
+ /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group", role: "toolbar", "aria-label": "Drawing tools", children: [
2935
+ PRIMARY_TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx8(ToolButton, { label, active: tool === t, onClick: () => onToolChange(t), children: SHAPE_ICONS[t] }, t)),
2936
+ /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-more", ref: moreRef, children: [
2937
+ /* @__PURE__ */ jsxs7(
2938
+ "button",
2939
+ {
2940
+ type: "button",
2941
+ title: "More shapes",
2942
+ "aria-label": "More shapes",
2943
+ "aria-haspopup": "menu",
2944
+ "aria-expanded": moreOpen,
2945
+ className: `zui-drawing-tool zui-drawing-tool-more ${moreActive ? "is-active" : ""}`,
2946
+ onClick: () => setMoreOpen((open) => !open),
2947
+ children: [
2948
+ /* @__PURE__ */ jsx8(Icon, { children: moreIcon }),
2949
+ /* @__PURE__ */ jsx8("span", { className: "zui-drawing-caret", "aria-hidden": "true" })
2950
+ ]
2951
+ }
2952
+ ),
2953
+ moreOpen && /* @__PURE__ */ jsx8("div", { className: "zui-drawing-popover", role: "menu", children: MORE_SHAPES.map((type) => /* @__PURE__ */ jsxs7(
2954
+ "button",
2955
+ {
2956
+ type: "button",
2957
+ role: "menuitemradio",
2958
+ "aria-checked": tool === type,
2959
+ className: `zui-drawing-popover-item ${tool === type ? "is-active" : ""}`,
2960
+ onClick: () => {
2961
+ setLastMore(type);
2962
+ onToolChange(type);
2963
+ setMoreOpen(false);
2964
+ },
2965
+ children: [
2966
+ /* @__PURE__ */ jsx8(Icon, { size: 18, children: SHAPE_ICONS[type] }),
2967
+ /* @__PURE__ */ jsx8("span", { children: BOX_DEFINITIONS[type].label })
2968
+ ]
2969
+ },
2970
+ type
2971
+ )) })
2972
+ ] })
2973
+ ] }),
2974
+ properties,
2975
+ /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group zui-drawing-toolbar-group-end", children: [
2976
+ /* @__PURE__ */ jsx8(
2977
+ ToolButton,
2978
+ {
2979
+ label: copied ? "Copied Mermaid to clipboard" : "Copy as Mermaid",
2980
+ className: copied ? "is-success" : "",
2981
+ onClick: () => {
2982
+ void onCopyMermaid().then((ok) => {
2983
+ if (!ok) return;
2984
+ setCopied(true);
2985
+ setTimeout(() => setCopied(false), 1600);
2986
+ });
2987
+ },
2988
+ children: copied ? UI_ICONS.check : UI_ICONS.mermaid
2989
+ }
2990
+ ),
2991
+ /* @__PURE__ */ jsx8("span", { className: "zui-drawing-toolbar-divider" }),
2992
+ LAYOUT_OPTIONS.map(({ preset, label, icon, width: presetWidth }) => /* @__PURE__ */ jsx8(
2993
+ ToolButton,
2994
+ {
2995
+ label,
2996
+ active: layoutPreset(width) === preset,
2997
+ onClick: () => onWidthChange(presetWidth),
2998
+ children: icon
2999
+ },
3000
+ preset
3001
+ ))
3002
+ ] })
3003
+ ] });
3391
3004
  }
3392
- function expandConnector(connector, centers, fallbackId) {
3393
- const type = connector.type ?? "arrow";
3394
- const from = centers.get(endId(connector.from));
3395
- const to = centers.get(endId(connector.to));
3396
- return {
3397
- id: connector.id ?? fallbackId,
3398
- type,
3399
- x: from.x,
3400
- y: from.y,
3401
- w: to.x - from.x,
3402
- h: to.y - from.y,
3403
- stroke: connector.stroke ?? (connector.color ? COLOR_PRESETS[connector.color].stroke : DEFAULT_STROKE2),
3404
- fill: "transparent",
3405
- strokeWidth: STROKE_WIDTH,
3406
- startBinding: toBinding(connector.from),
3407
- endBinding: toBinding(connector.to),
3408
- ...connector.text ? { text: connector.text } : {},
3409
- ...type === "arrow" && connector.bidirectional ? { bidirectional: true } : {},
3410
- ...connector.routing === "elbow" ? { routing: "elbow" } : {}
3411
- };
3005
+
3006
+ // src/drawing/canvas/DrawingCanvas.tsx
3007
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3008
+ var MIN_HEIGHT = 120;
3009
+ var MAX_HEIGHT = 1200;
3010
+ var WIDTH_CLASS = {
3011
+ full: "",
3012
+ text: "is-text-width",
3013
+ content: "is-content-width"
3014
+ };
3015
+ var MIN_CONTENT_WIDTH = 240;
3016
+ var CONTENT_WIDTH_PADDING = 40;
3017
+ var EMPTY_SET = /* @__PURE__ */ new Set();
3018
+ function contentWidth(shapes, paths) {
3019
+ let right = 0;
3020
+ for (const shape of shapes) {
3021
+ if (isConnectorType(shape.type)) {
3022
+ for (const p of paths.get(shape.id) ?? []) right = Math.max(right, p.x);
3023
+ } else {
3024
+ const b = bbox(shape);
3025
+ right = Math.max(right, b.x + b.w);
3026
+ }
3027
+ }
3028
+ return Math.max(MIN_CONTENT_WIDTH, Math.ceil(right + CONTENT_WIDTH_PADDING));
3412
3029
  }
3413
- function expandText(text, fallbackId) {
3414
- const size = textBoxSize(text.text);
3415
- return {
3416
- id: text.id ?? fallbackId,
3417
- type: "text",
3418
- x: text.x,
3419
- y: text.y,
3420
- w: size.w,
3421
- h: size.h,
3422
- stroke: text.stroke ?? (text.color ? COLOR_PRESETS[text.color].stroke : DEFAULT_STROKE2),
3423
- fill: "transparent",
3424
- strokeWidth: STROKE_WIDTH,
3425
- text: text.text
3426
- };
3427
- }
3428
- function endId(end) {
3429
- return typeof end === "string" ? end : end.id;
3430
- }
3431
- function toBinding(end) {
3432
- if (typeof end === "string" || !end.side) return { id: endId(end) };
3433
- return { id: end.id, fixedPoint: SIDE_FIXED_POINTS[end.side] };
3434
- }
3435
- function shapeCenter(shape) {
3436
- const b = bbox(shape);
3437
- return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
3438
- }
3439
- function dedupeById(items) {
3440
- const seen = /* @__PURE__ */ new Set();
3441
- return items.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
3442
- }
3443
- function normalizeSkeleton(raw) {
3444
- const value = raw;
3445
- const boxes = value.boxes.flatMap((b) => {
3446
- const box = normalizeBox(b);
3447
- return box ? [box] : [];
3448
- });
3449
- const connectors = Array.isArray(value.connectors) ? value.connectors.flatMap((c) => {
3450
- const connector = normalizeConnector(c);
3451
- return connector ? [connector] : [];
3452
- }) : [];
3453
- const texts = Array.isArray(value.texts) ? value.texts.flatMap((t) => {
3454
- const text = normalizeText(t);
3455
- return text ? [text] : [];
3456
- }) : [];
3457
- const canvasWidth = finiteNumber(value.canvasWidth);
3458
- const canvasHeight = finiteNumber(value.canvasHeight);
3459
- return {
3460
- direction: value.direction === "down" ? "down" : "right",
3461
- ...canvasWidth !== void 0 ? { canvasWidth } : {},
3462
- ...canvasHeight !== void 0 ? { canvasHeight } : {},
3463
- ...isBlockWidth(value.width) ? { width: value.width } : {},
3464
- boxes,
3465
- connectors,
3466
- texts
3467
- };
3468
- }
3469
- function normalizeBox(value) {
3470
- if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
3471
- if (value.type !== void 0 && !(typeof value.type === "string" && isBoxType(value.type))) {
3472
- return null;
3473
- }
3474
- return compact({
3475
- id: value.id,
3476
- type: value.type,
3477
- label: nonEmptyString(value.label) ? value.label : void 0,
3478
- text: nonEmptyString(value.text) ? value.text : void 0,
3479
- footer: nonEmptyString(value.footer) ? value.footer : void 0,
3480
- x: finiteNumber(value.x),
3481
- y: finiteNumber(value.y),
3482
- w: positiveNumber(value.w),
3483
- h: positiveNumber(value.h),
3484
- color: colorName(value.color),
3485
- stroke: nonEmptyString(value.stroke) ? value.stroke : void 0,
3486
- fill: nonEmptyString(value.fill) ? value.fill : void 0
3487
- });
3488
- }
3489
- function normalizeConnector(value) {
3490
- if (!isRecord2(value)) return null;
3491
- const from = normalizeEnd(value.from);
3492
- const to = normalizeEnd(value.to);
3493
- if (!from || !to) return null;
3494
- if (value.type !== void 0 && !(typeof value.type === "string" && isConnectorType(value.type))) {
3495
- return null;
3030
+ function computePaths(shapes) {
3031
+ const paths = /* @__PURE__ */ new Map();
3032
+ for (const shape of shapes) {
3033
+ if (isConnectorType(shape.type)) paths.set(shape.id, connectorPoints(shape, shapes));
3496
3034
  }
3497
- return compact({
3498
- id: nonEmptyString(value.id) ? value.id : void 0,
3499
- type: value.type,
3500
- from,
3501
- to,
3502
- text: nonEmptyString(value.text) ? value.text : void 0,
3503
- bidirectional: value.bidirectional === true ? true : void 0,
3504
- routing: value.routing === "elbow" ? "elbow" : void 0,
3505
- color: colorName(value.color),
3506
- stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
3507
- });
3508
- }
3509
- function normalizeEnd(value) {
3510
- if (nonEmptyString(value)) return value;
3511
- if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
3512
- const side = value.side;
3513
- return typeof side === "string" && side in SIDE_FIXED_POINTS ? { id: value.id, side } : { id: value.id };
3514
- }
3515
- function normalizeText(value) {
3516
- if (!isRecord2(value) || !nonEmptyString(value.text)) return null;
3517
- const x = finiteNumber(value.x);
3518
- const y = finiteNumber(value.y);
3519
- if (x === void 0 || y === void 0) return null;
3520
- return compact({
3521
- id: nonEmptyString(value.id) ? value.id : void 0,
3522
- x,
3523
- y,
3524
- text: value.text,
3525
- color: colorName(value.color),
3526
- stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
3527
- });
3528
- }
3529
- function isRecord2(value) {
3530
- return typeof value === "object" && value !== null;
3531
- }
3532
- function nonEmptyString(value) {
3533
- return typeof value === "string" && value !== "";
3534
- }
3535
- function finiteNumber(value) {
3536
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3537
- }
3538
- function positiveNumber(value) {
3539
- const n2 = finiteNumber(value);
3540
- return n2 !== void 0 && n2 > 0 ? n2 : void 0;
3541
- }
3542
- function colorName(value) {
3543
- return typeof value === "string" && COLOR_NAMES.includes(value) ? value : void 0;
3035
+ return paths;
3544
3036
  }
3545
- function compact(value) {
3546
- return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
3037
+ function DrawingCanvas({ nodeKey, data }) {
3038
+ const [editor] = useLexicalComposerContext5();
3039
+ const isEditable = useLexicalEditable();
3040
+ const [shapes, setShapes] = useState3(data.shapes);
3041
+ const [canvasHeight, setCanvasHeight] = useState3(data.canvasHeight);
3042
+ const [width, setWidth] = useState3(data.width ?? "full");
3043
+ const [tool, setTool] = useState3("select");
3044
+ const [selectedIds, setSelectedIds] = useState3(EMPTY_SET);
3045
+ const [editingText, setEditingText] = useState3(null);
3046
+ const [stroke, setStroke] = useState3(STROKE_COLORS[0]);
3047
+ const [fill, setFill] = useState3(FILL_COLORS[0]);
3048
+ const [hoverBoxId, setHoverBoxId] = useState3(null);
3049
+ const [marquee, setMarquee] = useState3(null);
3050
+ const [scale, setScale] = useState3(1);
3051
+ const rootRef = useRef4(null);
3052
+ const svgRef = useRef4(null);
3053
+ const dragRef = useRef4(null);
3054
+ const pendingTextEditRef = useRef4(null);
3055
+ const pendingPointRef = useRef4(null);
3056
+ const frameRef = useRef4(null);
3057
+ const lastCommittedRef = useRef4(serializeDrawingData(data));
3058
+ const shapesRef = useRef4(shapes);
3059
+ shapesRef.current = shapes;
3060
+ const heightRef = useRef4(canvasHeight);
3061
+ heightRef.current = canvasHeight;
3062
+ const widthRef = useRef4(width);
3063
+ widthRef.current = width;
3064
+ const selectedRef = useRef4(selectedIds);
3065
+ selectedRef.current = selectedIds;
3066
+ const paths = useMemo(() => computePaths(shapes), [shapes]);
3067
+ const canvasWidth = data.canvasWidth;
3068
+ const logicalWidth = canvasWidth ?? (width === "content" ? contentWidth(shapes, paths) : null);
3069
+ useEffect7(() => {
3070
+ const incoming = serializeDrawingData(data);
3071
+ if (incoming !== lastCommittedRef.current) {
3072
+ lastCommittedRef.current = incoming;
3073
+ setShapes(data.shapes);
3074
+ setCanvasHeight(data.canvasHeight);
3075
+ setWidth(data.width ?? "full");
3076
+ setSelectedIds(EMPTY_SET);
3077
+ setEditingText(null);
3078
+ }
3079
+ }, [data]);
3080
+ useEffect7(() => {
3081
+ const svg = svgRef.current;
3082
+ if (!logicalWidth || !svg || typeof ResizeObserver === "undefined") {
3083
+ setScale(1);
3084
+ return;
3085
+ }
3086
+ const update = () => {
3087
+ const rect = svg.getBoundingClientRect();
3088
+ setScale(rect.width > 0 ? rect.width / logicalWidth : 1);
3089
+ };
3090
+ update();
3091
+ const observer = new ResizeObserver(update);
3092
+ observer.observe(svg);
3093
+ return () => observer.disconnect();
3094
+ }, [logicalWidth]);
3095
+ const commit = useCallback(
3096
+ (nextShapes, options) => {
3097
+ const nextWidth = options?.width ?? widthRef.current;
3098
+ const explicitFull = options?.width === void 0 && data.width !== void 0;
3099
+ const payload = {
3100
+ version: 2,
3101
+ ...canvasWidth ? { canvasWidth } : {},
3102
+ canvasHeight: options?.height ?? heightRef.current,
3103
+ ...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
3104
+ shapes: nextShapes
3105
+ };
3106
+ const json = serializeDrawingData(payload);
3107
+ if (json === lastCommittedRef.current) return;
3108
+ lastCommittedRef.current = json;
3109
+ editor.update(() => {
3110
+ const node = $getNodeByKey2(nodeKey);
3111
+ if ($isDrawingNode(node)) node.setData(payload);
3112
+ });
3113
+ },
3114
+ [editor, nodeKey, canvasWidth, data.width]
3115
+ );
3116
+ const updateShapes = useCallback(
3117
+ (updater, options) => {
3118
+ setShapes((prev) => {
3119
+ const next = resolveBindings(updater(prev));
3120
+ if (options?.commit) commit(next);
3121
+ return next;
3122
+ });
3123
+ },
3124
+ [commit]
3125
+ );
3126
+ const getPoint = useCallback((e) => {
3127
+ const svg = svgRef.current;
3128
+ if (!svg) return { x: 0, y: 0 };
3129
+ const ctm = svg.getScreenCTM();
3130
+ if (!ctm) {
3131
+ const rect = svg.getBoundingClientRect();
3132
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
3133
+ }
3134
+ const inverse = ctm.inverse();
3135
+ return {
3136
+ x: inverse.a * e.clientX + inverse.c * e.clientY + inverse.e,
3137
+ y: inverse.b * e.clientX + inverse.d * e.clientY + inverse.f
3138
+ };
3139
+ }, []);
3140
+ const selection = useMemo(
3141
+ () => shapes.filter((s) => selectedIds.has(s.id)),
3142
+ [shapes, selectedIds]
3143
+ );
3144
+ const single = selection.length === 1 ? selection[0] : null;
3145
+ const startTextEditing = useCallback((id, field) => {
3146
+ setEditingText({ id, field });
3147
+ setSelectedIds(/* @__PURE__ */ new Set([id]));
3148
+ }, []);
3149
+ const capture = (e) => {
3150
+ svgRef.current?.setPointerCapture(e.pointerId);
3151
+ };
3152
+ const handleBackgroundPointerDown = useCallback(
3153
+ (e) => {
3154
+ if (!isEditable || editingText || e.button !== 0) return;
3155
+ const point = getPoint(e);
3156
+ capture(e);
3157
+ if (tool === "select") {
3158
+ if (!e.shiftKey) setSelectedIds(EMPTY_SET);
3159
+ dragRef.current = { mode: "marquee", origin: point, current: point, additive: e.shiftKey };
3160
+ return;
3161
+ }
3162
+ if (tool === "text") {
3163
+ const id2 = createShapeId();
3164
+ const size = textBoxSize("");
3165
+ updateShapes((prev) => [
3166
+ ...prev,
3167
+ {
3168
+ id: id2,
3169
+ type: "text",
3170
+ x: point.x,
3171
+ y: point.y - FONT_SIZE / 2,
3172
+ w: size.w,
3173
+ h: size.h,
3174
+ stroke,
3175
+ fill: "transparent",
3176
+ strokeWidth: 2,
3177
+ text: ""
3178
+ }
3179
+ ]);
3180
+ setTool("select");
3181
+ setSelectedIds(/* @__PURE__ */ new Set([id2]));
3182
+ pendingTextEditRef.current = id2;
3183
+ return;
3184
+ }
3185
+ const id = createShapeId();
3186
+ const def = isBoxType(tool) ? BOX_DEFINITIONS[tool] : null;
3187
+ const shapeFill = def ? fill === "transparent" && def.defaultFill ? def.defaultFill : fill : "transparent";
3188
+ dragRef.current = { mode: "draw", id, origin: point };
3189
+ setSelectedIds(EMPTY_SET);
3190
+ updateShapes((prev) => [
3191
+ ...prev,
3192
+ { id, type: tool, x: point.x, y: point.y, w: 0, h: 0, stroke, fill: shapeFill, strokeWidth: 2 }
3193
+ ]);
3194
+ },
3195
+ [isEditable, editingText, getPoint, tool, stroke, fill, updateShapes, startTextEditing]
3196
+ );
3197
+ const handleShapePointerDown = useCallback(
3198
+ (e, shape) => {
3199
+ if (!isEditable || tool !== "select" || editingText || e.button !== 0) return;
3200
+ e.stopPropagation();
3201
+ const point = getPoint(e);
3202
+ capture(e);
3203
+ const selected = selectedRef.current;
3204
+ if (e.shiftKey) {
3205
+ if (selected.has(shape.id)) {
3206
+ const next2 = new Set(selected);
3207
+ next2.delete(shape.id);
3208
+ setSelectedIds(next2);
3209
+ return;
3210
+ }
3211
+ const next = new Set(selected).add(shape.id);
3212
+ setSelectedIds(next);
3213
+ dragRef.current = {
3214
+ mode: "move",
3215
+ clickedId: shape.id,
3216
+ origin: point,
3217
+ origs: moveGroup(shapesRef.current, next, shape.id),
3218
+ wasSelected: false
3219
+ };
3220
+ return;
3221
+ }
3222
+ const wasSelected = selected.size === 1 && selected.has(shape.id);
3223
+ const group = selected.has(shape.id) ? selected : /* @__PURE__ */ new Set([shape.id]);
3224
+ if (!selected.has(shape.id)) setSelectedIds(group);
3225
+ dragRef.current = {
3226
+ mode: "move",
3227
+ clickedId: shape.id,
3228
+ origin: point,
3229
+ origs: moveGroup(shapesRef.current, group, shape.id),
3230
+ wasSelected
3231
+ };
3232
+ },
3233
+ [isEditable, tool, editingText, getPoint]
3234
+ );
3235
+ const handleHandlePointerDown = useCallback(
3236
+ (e, drag) => {
3237
+ if (!isEditable || e.button !== 0) return;
3238
+ e.stopPropagation();
3239
+ capture(e);
3240
+ dragRef.current = drag;
3241
+ if (drag.mode === "endpoint") {
3242
+ const key = drag.end === "start" ? "startBinding" : "endBinding";
3243
+ updateShapes(
3244
+ (prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
3245
+ );
3246
+ }
3247
+ },
3248
+ [isEditable, updateShapes]
3249
+ );
3250
+ const flushPointer = useCallback(() => {
3251
+ frameRef.current = null;
3252
+ const point = pendingPointRef.current;
3253
+ const drag = dragRef.current;
3254
+ if (!point || !drag) return;
3255
+ if (drag.mode === "marquee") {
3256
+ drag.current = point;
3257
+ setMarquee({ origin: drag.origin, current: point });
3258
+ return;
3259
+ }
3260
+ updateShapes((prev) => applyDrag(prev, drag, point));
3261
+ if (drag.mode === "endpoint" || drag.mode === "draw" && isConnectorType(shapesRef.current.find((s) => s.id === drag.id)?.type ?? "")) {
3262
+ const box = findBoxAt(shapesRef.current, point, drag.id);
3263
+ setHoverBoxId((prev) => prev === (box?.id ?? null) ? prev : box?.id ?? null);
3264
+ }
3265
+ }, [updateShapes]);
3266
+ const handlePointerMove = useCallback(
3267
+ (e) => {
3268
+ if (!dragRef.current) return;
3269
+ pendingPointRef.current = getPoint(e);
3270
+ if (frameRef.current === null) {
3271
+ frameRef.current = requestAnimationFrame(flushPointer);
3272
+ }
3273
+ },
3274
+ [getPoint, flushPointer]
3275
+ );
3276
+ const handlePointerUp = useCallback(
3277
+ (e) => {
3278
+ const drag = dragRef.current;
3279
+ dragRef.current = null;
3280
+ if (frameRef.current !== null) {
3281
+ cancelAnimationFrame(frameRef.current);
3282
+ frameRef.current = null;
3283
+ }
3284
+ pendingPointRef.current = null;
3285
+ setHoverBoxId(null);
3286
+ if (pendingTextEditRef.current) {
3287
+ const id = pendingTextEditRef.current;
3288
+ pendingTextEditRef.current = null;
3289
+ startTextEditing(id, "text");
3290
+ return;
3291
+ }
3292
+ if (!drag) return;
3293
+ const point = getPoint(e);
3294
+ if (drag.mode === "marquee") {
3295
+ setMarquee(null);
3296
+ const rect = marqueeRect(drag.origin, point);
3297
+ if (rect.w < CLICK_TOLERANCE && rect.h < CLICK_TOLERANCE) return;
3298
+ const ids = shapesWithin(shapesRef.current, rect, paths);
3299
+ setSelectedIds((prev) => drag.additive ? /* @__PURE__ */ new Set([...prev, ...ids]) : new Set(ids));
3300
+ return;
3301
+ }
3302
+ if (drag.mode === "move" && drag.wasSelected) {
3303
+ const moved = Math.abs(point.x - drag.origin.x) > CLICK_TOLERANCE || Math.abs(point.y - drag.origin.y) > CLICK_TOLERANCE;
3304
+ const current = shapesRef.current.find((s) => s.id === drag.clickedId);
3305
+ if (!moved && current) {
3306
+ updateShapes((prev) => applyDrag(prev, drag, drag.origin));
3307
+ if (current.type === "text" || isConnectorType(current.type)) {
3308
+ startTextEditing(current.id, "text");
3309
+ } else {
3310
+ startTextEditing(current.id, slotAt(current, drag.origin.y));
3311
+ }
3312
+ return;
3313
+ }
3314
+ }
3315
+ if (drag.mode === "draw") {
3316
+ const drawn = shapesRef.current.find((s) => s.id === drag.id);
3317
+ if (drawn && Math.abs(drawn.w) < 4 && Math.abs(drawn.h) < 4) {
3318
+ if (drawn && isBoxType(drawn.type)) {
3319
+ const size = BOX_DEFINITIONS[drawn.type].defaultSize;
3320
+ updateShapes(
3321
+ (prev) => prev.map(
3322
+ (s) => s.id === drag.id ? { ...s, x: s.x - size.w / 2, y: s.y - size.h / 2, w: size.w, h: size.h } : s
3323
+ ),
3324
+ { commit: true }
3325
+ );
3326
+ setTool("select");
3327
+ setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
3328
+ } else {
3329
+ updateShapes((prev) => prev.filter((s) => s.id !== drag.id));
3330
+ }
3331
+ return;
3332
+ }
3333
+ if (drawn && isConnectorType(drawn.type)) {
3334
+ updateShapes((prev) => prev.map((s) => s.id === drag.id ? bindEndpoints(s, prev) : s));
3335
+ }
3336
+ setTool("select");
3337
+ setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
3338
+ }
3339
+ if (drag.mode === "endpoint") {
3340
+ updateShapes(
3341
+ (prev) => prev.map((s) => {
3342
+ if (s.id !== drag.id) return s;
3343
+ const end = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
3344
+ const box = findBoxAt(prev, end, s.id);
3345
+ const other = drag.end === "start" ? s.endBinding : s.startBinding;
3346
+ if (box && box.id === other?.id) return s;
3347
+ const key = drag.end === "start" ? "startBinding" : "endBinding";
3348
+ return { ...s, [key]: box ? makeBinding(box, end) : void 0 };
3349
+ })
3350
+ );
3351
+ }
3352
+ updateShapes((prev) => prev.map(normalize), { commit: true });
3353
+ },
3354
+ [getPoint, paths, updateShapes, startTextEditing]
3355
+ );
3356
+ const deleteSelection = useCallback(() => {
3357
+ const ids = selectedRef.current;
3358
+ if (!ids.size) return;
3359
+ setEditingText(null);
3360
+ setSelectedIds(EMPTY_SET);
3361
+ updateShapes(
3362
+ (prev) => prev.filter(
3363
+ (s) => !ids.has(s.id) && !(s.startBinding && ids.has(s.startBinding.id)) && !(s.endBinding && ids.has(s.endBinding.id))
3364
+ ),
3365
+ { commit: true }
3366
+ );
3367
+ }, [updateShapes]);
3368
+ const editingRef = useRef4(editingText);
3369
+ editingRef.current = editingText;
3370
+ useEffect7(() => {
3371
+ const root = rootRef.current;
3372
+ if (!root || !isEditable) return;
3373
+ const onKeyDown = (e) => {
3374
+ if (editingRef.current) return;
3375
+ const ids = selectedRef.current;
3376
+ if ((e.key === "Delete" || e.key === "Backspace") && ids.size) {
3377
+ e.preventDefault();
3378
+ e.stopPropagation();
3379
+ deleteSelection();
3380
+ } else if (e.key === "Enter" && ids.size === 1) {
3381
+ const [id] = ids;
3382
+ const shape = shapesRef.current.find((s) => s.id === id);
3383
+ if (shape) {
3384
+ e.preventDefault();
3385
+ e.stopPropagation();
3386
+ startTextEditing(shape.id, "text");
3387
+ }
3388
+ } else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
3389
+ e.preventDefault();
3390
+ e.stopPropagation();
3391
+ setSelectedIds(new Set(shapesRef.current.map((s) => s.id)));
3392
+ } else if (e.key === "Escape") {
3393
+ e.stopPropagation();
3394
+ setSelectedIds(EMPTY_SET);
3395
+ setTool("select");
3396
+ }
3397
+ };
3398
+ root.addEventListener("keydown", onKeyDown);
3399
+ return () => root.removeEventListener("keydown", onKeyDown);
3400
+ }, [isEditable, deleteSelection, startTextEditing]);
3401
+ const applyToSelection = useCallback(
3402
+ (patch) => {
3403
+ const ids = selectedRef.current;
3404
+ if (!ids.size) return;
3405
+ updateShapes((prev) => prev.map((s) => ids.has(s.id) ? patch(s) : s), { commit: true });
3406
+ },
3407
+ [updateShapes]
3408
+ );
3409
+ const applyColor = useCallback(
3410
+ (name) => {
3411
+ const preset = COLOR_PRESETS[name];
3412
+ setStroke(preset.stroke);
3413
+ setFill(preset.fill);
3414
+ applyToSelection(
3415
+ (s) => isBoxType(s.type) ? { ...s, stroke: preset.stroke, fill: preset.fill } : (
3416
+ // Connectors and text have nothing but their stroke to show
3417
+ { ...s, stroke: preset.stroke === "transparent" ? STROKE_COLORS[0] : preset.stroke }
3418
+ )
3419
+ );
3420
+ },
3421
+ [applyToSelection]
3422
+ );
3423
+ const applyRouting = useCallback(
3424
+ (elbow) => applyToSelection(
3425
+ (s) => isConnectorType(s.type) ? { ...s, routing: elbow ? "elbow" : void 0, elbow: void 0, waypoints: void 0 } : s
3426
+ ),
3427
+ [applyToSelection]
3428
+ );
3429
+ const applyDirection = useCallback(
3430
+ (bidirectional) => applyToSelection(
3431
+ (s) => s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
3432
+ ),
3433
+ [applyToSelection]
3434
+ );
3435
+ const applyWidth = useCallback(
3436
+ (next) => {
3437
+ setWidth(next);
3438
+ commit(shapesRef.current, { width: next });
3439
+ },
3440
+ [commit]
3441
+ );
3442
+ const addWaypoint = useCallback(
3443
+ (e, shapeId, segmentIndex, point) => {
3444
+ if (!isEditable || e.button !== 0) return;
3445
+ e.stopPropagation();
3446
+ capture(e);
3447
+ updateShapes(
3448
+ (prev) => prev.map((s) => {
3449
+ if (s.id !== shapeId || !isConnectorType(s.type)) return s;
3450
+ const interior = connectorPoints(s, prev).slice(1, -1);
3451
+ const waypoints = [...interior.slice(0, segmentIndex), point, ...interior.slice(segmentIndex)];
3452
+ return { ...s, waypoints, routing: void 0, elbow: void 0 };
3453
+ })
3454
+ );
3455
+ dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
3456
+ },
3457
+ [isEditable, updateShapes]
3458
+ );
3459
+ const removeWaypoint = useCallback(
3460
+ (shapeId, index) => {
3461
+ updateShapes(
3462
+ (prev) => prev.map((s) => {
3463
+ if (s.id !== shapeId || !s.waypoints) return s;
3464
+ const waypoints = s.waypoints.filter((_, i) => i !== index);
3465
+ return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
3466
+ }),
3467
+ { commit: true }
3468
+ );
3469
+ },
3470
+ [updateShapes]
3471
+ );
3472
+ const commitText = useCallback(
3473
+ (id, field, value) => {
3474
+ setEditingText(null);
3475
+ rootRef.current?.focus({ preventScroll: true });
3476
+ updateShapes(
3477
+ (prev) => prev.flatMap((s) => {
3478
+ if (s.id !== id) return [s];
3479
+ if (s.type === "text") {
3480
+ return value.trim() === "" ? [] : [{ ...s, text: value, ...textBoxSize(value) }];
3481
+ }
3482
+ return [{ ...s, [field]: value.trim() === "" ? void 0 : value }];
3483
+ }),
3484
+ { commit: true }
3485
+ );
3486
+ },
3487
+ [updateShapes]
3488
+ );
3489
+ const copyMermaid = useCallback(async () => {
3490
+ const payload = { version: 2, canvasHeight: heightRef.current, shapes: shapesRef.current };
3491
+ try {
3492
+ await navigator.clipboard.writeText(drawingToMermaid(payload));
3493
+ return true;
3494
+ } catch {
3495
+ return false;
3496
+ }
3497
+ }, []);
3498
+ const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
3499
+ const hoverBox = hoverBoxId ? shapes.find((s) => s.id === hoverBoxId) : null;
3500
+ const propStroke = single?.stroke ?? selection[0]?.stroke ?? stroke;
3501
+ const propFill = single?.fill ?? selection[0]?.fill ?? fill;
3502
+ const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
3503
+ return /* @__PURE__ */ jsxs8(
3504
+ "div",
3505
+ {
3506
+ ref: rootRef,
3507
+ className: `zui-drawing-canvas ${WIDTH_CLASS[width]} ${isEditable ? "is-editable" : ""}`,
3508
+ tabIndex: isEditable ? 0 : void 0,
3509
+ onFocus: () => editor.dispatchCommand(DRAWING_FOCUS_COMMAND, nodeKey),
3510
+ onBlur: (e) => {
3511
+ if (rootRef.current?.contains(e.relatedTarget)) return;
3512
+ editor.dispatchCommand(DRAWING_FOCUS_COMMAND, null);
3513
+ },
3514
+ children: [
3515
+ isEditable && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx9(
3516
+ DrawingToolbar,
3517
+ {
3518
+ tool,
3519
+ onToolChange: (t) => {
3520
+ setTool(t);
3521
+ if (t !== "select") setSelectedIds(EMPTY_SET);
3522
+ },
3523
+ width,
3524
+ onWidthChange: applyWidth,
3525
+ onCopyMermaid: copyMermaid,
3526
+ properties: (
3527
+ // Inline in the same row (so the stage never shifts); shown
3528
+ // only while the canvas has focus (see .zui-drawing-props-host)
3529
+ /* @__PURE__ */ jsx9("div", { className: "zui-drawing-props-host", children: /* @__PURE__ */ jsx9(
3530
+ PropertyBar,
3531
+ {
3532
+ selection,
3533
+ stroke: propStroke,
3534
+ fill: propFill,
3535
+ onColor: applyColor,
3536
+ onRouting: applyRouting,
3537
+ onDirection: applyDirection,
3538
+ onDelete: deleteSelection
3539
+ }
3540
+ ) })
3541
+ )
3542
+ }
3543
+ ) }),
3544
+ /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
3545
+ /* @__PURE__ */ jsxs8(
3546
+ "svg",
3547
+ {
3548
+ ref: svgRef,
3549
+ className: "zui-drawing-surface",
3550
+ style: {
3551
+ height: logicalWidth ? void 0 : canvasHeight,
3552
+ width: logicalWidth ?? void 0,
3553
+ maxWidth: "100%",
3554
+ aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
3555
+ cursor: canvasCursor
3556
+ },
3557
+ viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
3558
+ preserveAspectRatio: "xMinYMin meet",
3559
+ onPointerDown: handleBackgroundPointerDown,
3560
+ onPointerMove: handlePointerMove,
3561
+ onPointerUp: handlePointerUp,
3562
+ onPointerCancel: handlePointerUp,
3563
+ onDoubleClick: (e) => {
3564
+ if (!isEditable) return;
3565
+ const target = e.target.closest("[data-shape-id]");
3566
+ const id = target?.getAttribute("data-shape-id");
3567
+ const shape = id ? shapes.find((s) => s.id === id) : null;
3568
+ if (!shape) return;
3569
+ if (shape.type === "text" || isConnectorType(shape.type)) {
3570
+ startTextEditing(shape.id, "text");
3571
+ } else {
3572
+ startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
3573
+ }
3574
+ },
3575
+ children: [
3576
+ shapes.map((shape) => /* @__PURE__ */ jsxs8(
3577
+ "g",
3578
+ {
3579
+ "data-shape-id": shape.id,
3580
+ className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
3581
+ style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
3582
+ onPointerDown: (e) => handleShapePointerDown(e, shape),
3583
+ children: [
3584
+ /* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
3585
+ shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
3586
+ ShapeView,
3587
+ {
3588
+ shape,
3589
+ points: paths.get(shape.id),
3590
+ hideField: shape.id === editingText?.id ? editingText.field : null,
3591
+ showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
3592
+ }
3593
+ )
3594
+ ]
3595
+ },
3596
+ shape.id
3597
+ )),
3598
+ hoverBox && /* @__PURE__ */ jsx9(
3599
+ "polygon",
3600
+ {
3601
+ className: "zui-drawing-selection zui-drawing-bind-target",
3602
+ points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
3603
+ fill: "currentColor",
3604
+ fillOpacity: 0.06,
3605
+ stroke: "currentColor",
3606
+ strokeWidth: 2,
3607
+ strokeLinejoin: "round",
3608
+ pointerEvents: "none"
3609
+ }
3610
+ ),
3611
+ single && isEditable && !editingText && /* @__PURE__ */ jsx9(
3612
+ SelectionOverlay,
3613
+ {
3614
+ shape: single,
3615
+ points: paths.get(single.id) ?? [],
3616
+ onHandlePointerDown: handleHandlePointerDown,
3617
+ onWaypointAdd: addWaypoint,
3618
+ onWaypointRemove: removeWaypoint
3619
+ }
3620
+ ),
3621
+ selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
3622
+ marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
3623
+ ]
3624
+ }
3625
+ ),
3626
+ isEditable && shapes.length === 0 && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-empty", "aria-hidden": "true", children: "Pick a shape above, then click or drag on the canvas" }),
3627
+ editingShape && editingText && /* @__PURE__ */ jsx9(
3628
+ "div",
3629
+ {
3630
+ className: "zui-drawing-overlay",
3631
+ style: {
3632
+ transform: `scale(${scale})`,
3633
+ width: logicalWidth ?? void 0,
3634
+ height: logicalWidth ? canvasHeight : void 0
3635
+ },
3636
+ children: /* @__PURE__ */ jsx9(
3637
+ TextEditOverlay,
3638
+ {
3639
+ shape: editingShape,
3640
+ field: editingText.field,
3641
+ points: paths.get(editingShape.id),
3642
+ onCommit: commitText
3643
+ },
3644
+ `${editingShape.id}:${editingText.field}`
3645
+ )
3646
+ }
3647
+ ),
3648
+ isEditable && /* @__PURE__ */ jsx9(
3649
+ HeightHandle,
3650
+ {
3651
+ height: canvasHeight,
3652
+ scale,
3653
+ onChange: setCanvasHeight,
3654
+ onCommit: (h) => commit(shapesRef.current, { height: h })
3655
+ }
3656
+ )
3657
+ ] })
3658
+ ]
3659
+ }
3660
+ );
3547
3661
  }
3548
- var COLOR_SCHEMA = {
3549
- enum: COLOR_NAMES,
3550
- description: "Named color preset. Sets the stroke (outline + text) and, for boxes, a matching pastel fill. Omit for the default gray on transparent"
3551
- };
3552
- var END_SCHEMA = {
3553
- oneOf: [
3554
- {
3555
- type: "string",
3556
- description: "Id of a box. The connector attaches to its outline, aimed at the other end"
3557
- },
3662
+ function HeightHandle({
3663
+ height,
3664
+ scale,
3665
+ onChange,
3666
+ onCommit
3667
+ }) {
3668
+ const dragRef = useRef4(null);
3669
+ const latestRef = useRef4(height);
3670
+ latestRef.current = height;
3671
+ const clamp = (h) => Math.round(Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, h)));
3672
+ return /* @__PURE__ */ jsx9(
3673
+ "div",
3558
3674
  {
3559
- type: "object",
3560
- required: ["id"],
3561
- additionalProperties: false,
3562
- properties: {
3563
- id: { type: "string", description: "Id of a box" },
3564
- side: {
3565
- enum: ["top", "right", "bottom", "left"],
3566
- description: "Pin the endpoint to the middle of this side of the box instead of auto-aiming"
3567
- }
3568
- }
3569
- }
3570
- ],
3571
- description: "Box a connector end attaches to: a bare box id, or {id, side} to pick the attach edge"
3572
- };
3573
- var DRAWING_SKELETON_JSON_SCHEMA = {
3574
- $schema: "http://json-schema.org/draft-07/schema#",
3575
- title: "DrawingSkeleton",
3576
- description: "Compact intent description of a diagram. Boxes are sized from their text and laid out automatically along the connector flow; connectors are drawn between box outlines. Only say what is on the canvas \u2014 geometry is derived",
3577
- type: "object",
3578
- required: ["boxes"],
3579
- additionalProperties: false,
3580
- properties: {
3581
- direction: {
3582
- enum: ["right", "down"],
3583
- description: 'Auto-layout flow direction. Boxes connected a\u2192b are placed in successive ranks along this axis ("right" = columns left to right, "down" = rows top to bottom). Default "right"'
3584
- },
3585
- canvasWidth: {
3586
- type: "number",
3587
- minimum: 120,
3588
- description: "Logical canvas width in px. Omit for a fluid canvas in CSS pixels"
3589
- },
3590
- canvasHeight: {
3591
- type: "number",
3592
- minimum: 80,
3593
- description: "Canvas height in px. Omit to fit the content"
3594
- },
3595
- width: {
3596
- enum: ["full", "text", "content"],
3597
- description: 'Block width: "text" aligns the drawing with the text column (same as "full" unless the app sets a text measure); "content" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor'
3598
- },
3599
- boxes: {
3600
- type: "array",
3601
- items: { $ref: "#/definitions/box" },
3602
- description: "Card-like shapes carrying up to three text slots. Order is the render order and the stacking order within a layout rank"
3603
- },
3604
- connectors: {
3605
- type: "array",
3606
- items: { $ref: "#/definitions/connector" },
3607
- description: "Arrows and lines between boxes. Connectors whose from/to id does not match a box are dropped"
3608
- },
3609
- texts: {
3610
- type: "array",
3611
- items: { $ref: "#/definitions/text" },
3612
- description: "Free-floating annotations not attached to any box. Prefer box slots (label/text/footer) when the text belongs to a box"
3613
- }
3614
- },
3615
- definitions: {
3616
- box: {
3617
- type: "object",
3618
- required: ["id"],
3619
- additionalProperties: false,
3620
- properties: {
3621
- id: { type: "string", description: "Unique within the drawing; connectors reference it" },
3622
- type: {
3623
- enum: BOX_TYPES,
3624
- description: 'Shape: rect (default), ellipse, diamond (decision), note (sticky note, yellow by default), cylinder (database), cloud (external system), queue (message queue), actor (stick figure; only the "text" slot renders, below the figure)'
3625
- },
3626
- label: { type: "string", description: "Small bold heading at the top of the box" },
3627
- text: {
3628
- type: "string",
3629
- description: 'Main content, centered. Wraps automatically; "\\n" forces a line break'
3630
- },
3631
- footer: { type: "string", description: "Small dim line at the bottom of the box" },
3632
- x: { type: "number", description: "Top-left x in px. Omit (with y) to let the layout place the box" },
3633
- y: { type: "number", description: "Top-left y in px. Omit (with x) to let the layout place the box" },
3634
- w: {
3635
- type: "number",
3636
- exclusiveMinimum: 0,
3637
- description: "Width in px. Omit to size from the text (never smaller than the type default)"
3638
- },
3639
- h: {
3640
- type: "number",
3641
- exclusiveMinimum: 0,
3642
- description: "Height in px. Omit to size from the text (never smaller than the type default)"
3643
- },
3644
- color: COLOR_SCHEMA,
3645
- stroke: { type: "string", description: "CSS color of the outline and text; overrides the preset" },
3646
- fill: {
3647
- type: "string",
3648
- description: 'CSS color of the interior ("transparent" for none); overrides the preset'
3649
- }
3650
- }
3651
- },
3652
- connector: {
3653
- type: "object",
3654
- required: ["from", "to"],
3655
- additionalProperties: false,
3656
- properties: {
3657
- id: { type: "string", description: "Optional; generated when omitted" },
3658
- type: {
3659
- enum: CONNECTOR_TYPES,
3660
- description: 'arrow (default; arrowhead at "to") or line (no arrowheads)'
3661
- },
3662
- from: END_SCHEMA,
3663
- to: END_SCHEMA,
3664
- text: { type: "string", description: "Label rendered at the middle of the connector" },
3665
- bidirectional: {
3666
- type: "boolean",
3667
- description: "Arrow only: arrowheads on both ends"
3668
- },
3669
- routing: {
3670
- enum: ["straight", "elbow"],
3671
- description: "straight (default) draws a direct line; elbow draws a right-angled path routed around the boxes"
3672
- },
3673
- color: COLOR_SCHEMA,
3674
- stroke: { type: "string", description: "CSS color of the line and label; overrides the preset" }
3675
- }
3676
- },
3677
- text: {
3678
- type: "object",
3679
- required: ["x", "y", "text"],
3680
- additionalProperties: false,
3681
- properties: {
3682
- id: { type: "string", description: "Optional; generated when omitted" },
3683
- x: { type: "number", description: "Top-left x of the first line, in px" },
3684
- y: { type: "number", description: "Top-left y of the first line, in px" },
3685
- text: { type: "string", description: 'Content; "\\n" forces a line break' },
3686
- color: COLOR_SCHEMA,
3687
- stroke: { type: "string", description: "CSS color of the text; overrides the preset" }
3688
- }
3675
+ className: "zui-drawing-resize",
3676
+ title: "Drag to resize the canvas",
3677
+ role: "separator",
3678
+ "aria-orientation": "horizontal",
3679
+ onPointerDown: (e) => {
3680
+ e.preventDefault();
3681
+ e.stopPropagation();
3682
+ e.currentTarget.setPointerCapture(e.pointerId);
3683
+ dragRef.current = { startY: e.clientY, orig: height };
3684
+ },
3685
+ onPointerMove: (e) => {
3686
+ const drag = dragRef.current;
3687
+ if (!drag) return;
3688
+ onChange(clamp(drag.orig + (e.clientY - drag.startY) / Math.max(scale, 0.05)));
3689
+ },
3690
+ onPointerUp: () => {
3691
+ if (!dragRef.current) return;
3692
+ dragRef.current = null;
3693
+ onCommit(latestRef.current);
3694
+ },
3695
+ children: /* @__PURE__ */ jsx9(Icon, { size: 14, children: UI_ICONS.grip })
3689
3696
  }
3690
- }
3691
- };
3697
+ );
3698
+ }
3692
3699
 
3693
3700
  // src/nodes/DrawingNode.tsx
3694
3701
  import { jsx as jsx10 } from "react/jsx-runtime";
@@ -4726,24 +4733,7 @@ function HistoryButtons() {
4726
4733
  }
4727
4734
 
4728
4735
  // src/plugins/TableSettingsPlugin.tsx
4729
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
4730
- var DENSITY_OPTIONS = [
4731
- {
4732
- density: "compact",
4733
- label: "Compact rows",
4734
- icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 8.17h14M3 11.83h14M3 15.5h14" })
4735
- },
4736
- {
4737
- density: "comfortable",
4738
- label: "Comfortable rows",
4739
- icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 10h14M3 15.5h14" })
4740
- },
4741
- {
4742
- density: "spacious",
4743
- label: "Spacious rows",
4744
- icon: /* @__PURE__ */ jsx14("path", { d: "M3 5h14M3 15h14" })
4745
- }
4746
- ];
4736
+ import { jsx as jsx14 } from "react/jsx-runtime";
4747
4737
  function Icon3({ children }) {
4748
4738
  return /* @__PURE__ */ jsx14(
4749
4739
  "svg",
@@ -4806,42 +4796,29 @@ function TableSettingsPlugin() {
4806
4796
  [editor]
4807
4797
  );
4808
4798
  if (!isEditable || !anchor || !settings) return null;
4809
- return /* @__PURE__ */ jsxs11(
4799
+ return /* @__PURE__ */ jsx14(
4810
4800
  "div",
4811
4801
  {
4812
4802
  className: "zui-table-settings",
4813
4803
  role: "toolbar",
4814
4804
  "aria-label": "Table settings",
4815
4805
  style: { top: anchor.top, right: anchor.right },
4816
- children: [
4817
- BLOCK_WIDTH_OPTIONS.map(({ width, label, icon }) => /* @__PURE__ */ jsx14(
4818
- ToolbarButton,
4819
- {
4820
- label,
4821
- active: settings.width === width,
4822
- onClick: () => update({ width }),
4823
- children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4824
- },
4825
- width
4826
- )),
4827
- /* @__PURE__ */ jsx14(ToolbarDivider, {}),
4828
- DENSITY_OPTIONS.map(({ density, label, icon }) => /* @__PURE__ */ jsx14(
4829
- ToolbarButton,
4830
- {
4831
- label,
4832
- active: settings.density === density,
4833
- onClick: () => update({ density }),
4834
- children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4835
- },
4836
- density
4837
- ))
4838
- ]
4806
+ children: LAYOUT_OPTIONS.map(({ preset, label, icon, width, density }) => /* @__PURE__ */ jsx14(
4807
+ ToolbarButton,
4808
+ {
4809
+ label,
4810
+ active: layoutPreset(settings.width) === preset,
4811
+ onClick: () => update({ width, density }),
4812
+ children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4813
+ },
4814
+ preset
4815
+ ))
4839
4816
  }
4840
4817
  );
4841
4818
  }
4842
4819
 
4843
4820
  // src/EditorContent.tsx
4844
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
4821
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
4845
4822
  function EditorContent({
4846
4823
  placeholder = "Start writing...",
4847
4824
  foldable = true,
@@ -4862,8 +4839,8 @@ function EditorContent({
4862
4839
  }
4863
4840
  );
4864
4841
  }
4865
- return /* @__PURE__ */ jsxs12("div", { className: "zui-text-editor-body", children: [
4866
- /* @__PURE__ */ jsxs12("div", { className: "zui-text-editor-main", children: [
4842
+ return /* @__PURE__ */ jsxs11("div", { className: "zui-text-editor-body", children: [
4843
+ /* @__PURE__ */ jsxs11("div", { className: "zui-text-editor-main", children: [
4867
4844
  /* @__PURE__ */ jsx15(
4868
4845
  RichTextPlugin,
4869
4846
  {
@@ -4891,7 +4868,7 @@ import { useLexicalComposerContext as useLexicalComposerContext10 } from "@lexic
4891
4868
  import {
4892
4869
  TableOfContentsPlugin
4893
4870
  } from "@lexical/react/LexicalTableOfContentsPlugin";
4894
- import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
4871
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4895
4872
  var INDENT_PER_LEVEL = {
4896
4873
  h1: 0,
4897
4874
  h2: 1,
@@ -4947,7 +4924,7 @@ function OutlinePlugin() {
4947
4924
  for (const [key] of entries) {
4948
4925
  editor.getElementByKey(key)?.setAttribute("data-outline-key", key);
4949
4926
  }
4950
- return /* @__PURE__ */ jsxs13(
4927
+ return /* @__PURE__ */ jsxs12(
4951
4928
  "div",
4952
4929
  {
4953
4930
  className: `zui-text-editor-outline ${collapsed ? "is-collapsed" : ""}`,
@@ -4976,7 +4953,7 @@ function OutlinePlugin() {
4976
4953
  )
4977
4954
  }
4978
4955
  ),
4979
- !collapsed && /* @__PURE__ */ jsxs13("nav", { className: "zui-text-editor-outline-list", "aria-label": "Table of contents", children: [
4956
+ !collapsed && /* @__PURE__ */ jsxs12("nav", { className: "zui-text-editor-outline-list", "aria-label": "Table of contents", children: [
4980
4957
  entries.length === 0 && /* @__PURE__ */ jsx16("div", { className: "zui-text-editor-outline-empty", children: "No headings" }),
4981
4958
  entries.map(([key, text, tag]) => /* @__PURE__ */ jsx16(
4982
4959
  "button",
@@ -4999,7 +4976,7 @@ function OutlinePlugin() {
4999
4976
  }
5000
4977
 
5001
4978
  // src/MarkdownEditor.tsx
5002
- import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
4979
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
5003
4980
  var DEFAULT_ITEMS = {
5004
4981
  format: /* @__PURE__ */ jsx17(FormatButtons, {}),
5005
4982
  insert: /* @__PURE__ */ jsx17(InsertButtons, {}),
@@ -5020,7 +4997,7 @@ function MarkdownEditor({
5020
4997
  foldable = true,
5021
4998
  ...rootProps
5022
4999
  }) {
5023
- return /* @__PURE__ */ jsxs14(EditorRoot, { ...rootProps, children: [
5000
+ return /* @__PURE__ */ jsxs13(EditorRoot, { ...rootProps, children: [
5024
5001
  toolbar === true && /* @__PURE__ */ jsx17(EditorToolbar, {}),
5025
5002
  typeof toolbar === "function" && toolbar(DEFAULT_ITEMS),
5026
5003
  /* @__PURE__ */ jsx17(EditorContent, { placeholder, foldable, children: outline && /* @__PURE__ */ jsx17(OutlinePlugin, {}) })