@zuilib/text-editor 0.7.0 → 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,2134 +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 === "Enter" && !e.shiftKey) {
2238
- e.preventDefault();
2239
- onCommit(shape.id, field, valueRef.current);
2240
- } else if (e.key === "Escape") {
2241
- e.preventDefault();
2242
- 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
+ }
2243
1922
  }
2244
- };
2245
- const stop = (e) => e.stopPropagation();
2246
- el.addEventListener("keydown", onKeyDown);
2247
- el.addEventListener("keyup", stop);
2248
- el.addEventListener("keypress", stop);
2249
- return () => {
2250
- el.removeEventListener("keydown", onKeyDown);
2251
- el.removeEventListener("keyup", stop);
2252
- el.removeEventListener("keypress", stop);
2253
- };
2254
- }, []);
2255
- const style = {
2256
- color: shape.stroke,
2257
- fontFamily: "var(--zui-drawing-font)",
2258
- lineHeight: LINE_HEIGHT
2259
- };
2260
- if (shape.type === "text") {
2261
- const size = textBoxSize(value || " ");
2262
- Object.assign(style, {
2263
- left: shape.x,
2264
- top: shape.y,
2265
- fontSize: FONT_SIZE,
2266
- width: Math.max(80, size.w + 20),
2267
- height: size.h + 2,
2268
- whiteSpace: "pre"
2269
- });
2270
- } else if (isConnectorType(shape.type)) {
2271
- const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
2272
- const size = textBoxSize(lines.join("\n") || " ", CONNECTOR_FONT_SIZE);
2273
- const width = Math.max(70, size.w + 16);
2274
- const height = size.h + 4;
2275
- const mid = connectorMidpoint(
2276
- points ?? [
2277
- { x: shape.x, y: shape.y },
2278
- { x: shape.x + shape.w, y: shape.y + shape.h }
2279
- ]
2280
- );
2281
- Object.assign(style, {
2282
- left: mid.x - width / 2,
2283
- top: mid.y - height / 2,
2284
- width,
2285
- height,
2286
- fontSize: CONNECTOR_FONT_SIZE,
2287
- textAlign: "center",
2288
- whiteSpace: "pre-wrap"
2289
- });
2290
- } else {
2291
- const { area } = slotLayout(shape);
2292
- const fontSize = field === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
2293
- const width = Math.max(area.w, 40);
2294
- const lines = wrapText(value, width, fontSize).length;
2295
- const boxH = lines * fontSize * LINE_HEIGHT + 2;
2296
- const top = field === "label" ? area.y - 1 : field === "footer" ? area.y + area.h - boxH + 1 : area.y + area.h / 2 - boxH / 2;
2297
- Object.assign(style, {
2298
- left: area.x,
2299
- top,
2300
- width,
2301
- height: boxH,
2302
- fontSize,
2303
- fontWeight: field === "label" ? 600 : void 0,
2304
- letterSpacing: field === "label" ? 0.3 : void 0,
2305
- opacity: field === "text" ? 1 : field === "label" ? 0.85 : 0.65,
2306
- textAlign: "center",
2307
- whiteSpace: "pre-wrap"
2308
- });
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
+ }
2309
2044
  }
2310
- const placeholder = field === "label" ? "Label" : field === "footer" ? "Footer" : "Text";
2311
- return /* @__PURE__ */ jsx6(
2312
- "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",
2313
2058
  {
2314
- ref,
2315
- className: "zui-drawing-text-input",
2316
- style,
2317
- value,
2318
- placeholder,
2319
- spellCheck: false,
2320
- onChange: (e) => setValue(e.target.value),
2321
- onBlur: () => onCommit(shape.id, field, value),
2322
- 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
2323
2066
  }
2324
2067
  );
2325
2068
  }
2326
-
2327
- // src/drawing/canvas/Toolbar.tsx
2328
- import { useEffect as useEffect6, useRef as useRef3, useState as useState2 } from "react";
2329
-
2330
- // src/components/blockWidthOptions.tsx
2331
- import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2332
- var BLOCK_WIDTH_OPTIONS = [
2333
- {
2334
- width: "full",
2335
- label: "Full width",
2336
- icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2337
- /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2338
- /* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
2339
- ] })
2340
- },
2341
- {
2342
- width: "text",
2343
- label: "Text width",
2344
- icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2345
- /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2346
- /* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
2347
- ] })
2348
- },
2349
- {
2350
- width: "content",
2351
- label: "Content width",
2352
- icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2353
- /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2354
- /* @__PURE__ */ jsx7("path", { d: "M5.5 10h3M11.5 10h3M6.5 7.5L9 10l-2.5 2.5M13.5 7.5L11 10l2.5 2.5" })
2355
- ] })
2356
- }
2357
- ];
2358
-
2359
- // src/drawing/canvas/Toolbar.tsx
2360
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2361
- var PRIMARY_TOOLS = [
2362
- { tool: "select", label: "Select" },
2363
- { tool: "rect", label: "Rectangle" },
2364
- { tool: "ellipse", label: "Ellipse" },
2365
- { tool: "diamond", label: "Diamond" },
2366
- { tool: "arrow", label: "Arrow" },
2367
- { tool: "line", label: "Line" },
2368
- { tool: "text", label: "Text" }
2369
- ];
2370
- var MORE_SHAPES = ["note", "cylinder", "cloud", "queue", "actor"];
2371
- function ToolButton({
2069
+ function OptionButton({
2372
2070
  label,
2373
2071
  active,
2374
2072
  onClick,
2375
2073
  children,
2376
- className,
2377
- pressed
2074
+ className
2378
2075
  }) {
2379
- return /* @__PURE__ */ jsx8(
2076
+ return /* @__PURE__ */ jsx2(
2380
2077
  "button",
2381
2078
  {
2382
2079
  type: "button",
2383
2080
  title: label,
2384
2081
  "aria-label": label,
2385
- "aria-pressed": pressed ?? active,
2082
+ "aria-pressed": active,
2386
2083
  className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
2387
2084
  onClick,
2388
- children: /* @__PURE__ */ jsx8(Icon, { children })
2085
+ children: /* @__PURE__ */ jsx2(Icon, { children })
2389
2086
  }
2390
2087
  );
2391
2088
  }
2392
- function DrawingToolbar({
2393
- tool,
2394
- onToolChange,
2395
- width,
2396
- onWidthChange,
2397
- onCopyMermaid
2089
+ function PropertyBar({
2090
+ selection,
2091
+ stroke,
2092
+ fill,
2093
+ onColor,
2094
+ onRouting,
2095
+ onDirection,
2096
+ onDelete
2398
2097
  }) {
2399
- const [moreOpen, setMoreOpen] = useState2(false);
2400
- const [lastMore, setLastMore] = useState2("note");
2401
- const [copied, setCopied] = useState2(false);
2402
- const moreRef = useRef3(null);
2403
- useEffect6(() => {
2404
- if (!moreOpen) return;
2405
- const close = (e) => {
2406
- if (!moreRef.current?.contains(e.target)) setMoreOpen(false);
2407
- };
2408
- const key = (e) => {
2409
- if (e.key === "Escape") setMoreOpen(false);
2410
- };
2411
- document.addEventListener("pointerdown", close);
2412
- document.addEventListener("keydown", key);
2413
- return () => {
2414
- document.removeEventListener("pointerdown", close);
2415
- document.removeEventListener("keydown", key);
2416
- };
2417
- }, [moreOpen]);
2418
- const moreActive = MORE_SHAPES.includes(tool);
2419
- const moreIcon = moreActive ? SHAPE_ICONS[tool] : SHAPE_ICONS[lastMore];
2420
- return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2421
- /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group", role: "toolbar", "aria-label": "Drawing tools", children: [
2422
- PRIMARY_TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx8(ToolButton, { label, active: tool === t, onClick: () => onToolChange(t), children: SHAPE_ICONS[t] }, t)),
2423
- /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-more", ref: moreRef, children: [
2424
- /* @__PURE__ */ jsxs7(
2425
- "button",
2426
- {
2427
- type: "button",
2428
- title: "More shapes",
2429
- "aria-label": "More shapes",
2430
- "aria-haspopup": "menu",
2431
- "aria-expanded": moreOpen,
2432
- className: `zui-drawing-tool zui-drawing-tool-more ${moreActive ? "is-active" : ""}`,
2433
- onClick: () => setMoreOpen((open) => !open),
2434
- children: [
2435
- /* @__PURE__ */ jsx8(Icon, { children: moreIcon }),
2436
- /* @__PURE__ */ jsx8("span", { className: "zui-drawing-caret", "aria-hidden": "true" })
2437
- ]
2438
- }
2439
- ),
2440
- moreOpen && /* @__PURE__ */ jsx8("div", { className: "zui-drawing-popover", role: "menu", children: MORE_SHAPES.map((type) => /* @__PURE__ */ jsxs7(
2441
- "button",
2442
- {
2443
- type: "button",
2444
- role: "menuitemradio",
2445
- "aria-checked": tool === type,
2446
- className: `zui-drawing-popover-item ${tool === type ? "is-active" : ""}`,
2447
- onClick: () => {
2448
- setLastMore(type);
2449
- onToolChange(type);
2450
- setMoreOpen(false);
2451
- },
2452
- children: [
2453
- /* @__PURE__ */ jsx8(Icon, { size: 18, children: SHAPE_ICONS[type] }),
2454
- /* @__PURE__ */ jsx8("span", { children: BOX_DEFINITIONS[type].label })
2455
- ]
2456
- },
2457
- type
2458
- )) })
2459
- ] })
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 })
2460
2117
  ] }),
2461
- /* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group zui-drawing-toolbar-group-end", children: [
2462
- /* @__PURE__ */ jsx8(
2463
- 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",
2464
2174
  {
2465
- label: copied ? "Copied Mermaid to clipboard" : "Copy as Mermaid",
2466
- className: copied ? "is-success" : "",
2467
- onClick: () => {
2468
- void onCopyMermaid().then((ok) => {
2469
- if (!ok) return;
2470
- setCopied(true);
2471
- setTimeout(() => setCopied(false), 1600);
2472
- });
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);
2473
2193
  },
2474
- 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
+ })
2475
2223
  }
2476
2224
  ),
2477
- /* @__PURE__ */ jsx8("span", { className: "zui-drawing-toolbar-divider" }),
2478
- BLOCK_WIDTH_OPTIONS.map(({ width: option, label, icon }) => /* @__PURE__ */ jsx8(
2479
- ToolButton,
2480
- {
2481
- label,
2482
- active: width === option,
2483
- onClick: () => onWidthChange(option),
2484
- children: icon
2485
- },
2486
- option
2487
- ))
2488
- ] })
2489
- ] });
2490
- }
2491
-
2492
- // src/drawing/canvas/DrawingCanvas.tsx
2493
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2494
- var MIN_HEIGHT = 120;
2495
- var MAX_HEIGHT = 1200;
2496
- var WIDTH_CLASS = {
2497
- full: "",
2498
- text: "is-text-width",
2499
- content: "is-content-width"
2500
- };
2501
- var MIN_CONTENT_WIDTH = 240;
2502
- var CONTENT_WIDTH_PADDING = 40;
2503
- var EMPTY_SET = /* @__PURE__ */ new Set();
2504
- function contentWidth(shapes, paths) {
2505
- let right = 0;
2506
- for (const shape of shapes) {
2507
- if (isConnectorType(shape.type)) {
2508
- for (const p of paths.get(shape.id) ?? []) right = Math.max(right, p.x);
2509
- } else {
2510
- const b = bbox(shape);
2511
- right = Math.max(right, b.x + b.w);
2512
- }
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
+ ] });
2513
2239
  }
2514
- return Math.max(MIN_CONTENT_WIDTH, Math.ceil(right + CONTENT_WIDTH_PADDING));
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
+ ))
2262
+ ] });
2515
2263
  }
2516
- function computePaths(shapes) {
2517
- const paths = /* @__PURE__ */ new Map();
2518
- for (const shape of shapes) {
2519
- if (isConnectorType(shape.type)) paths.set(shape.id, connectorPoints(shape, shapes));
2520
- }
2521
- return paths;
2522
- }
2523
- function DrawingCanvas({ nodeKey, data }) {
2524
- const [editor] = useLexicalComposerContext5();
2525
- const isEditable = useLexicalEditable();
2526
- const [shapes, setShapes] = useState3(data.shapes);
2527
- const [canvasHeight, setCanvasHeight] = useState3(data.canvasHeight);
2528
- const [width, setWidth] = useState3(data.width ?? "full");
2529
- const [tool, setTool] = useState3("select");
2530
- const [selectedIds, setSelectedIds] = useState3(EMPTY_SET);
2531
- const [editingText, setEditingText] = useState3(null);
2532
- const [stroke, setStroke] = useState3(STROKE_COLORS[0]);
2533
- const [fill, setFill] = useState3(FILL_COLORS[0]);
2534
- const [hoverBoxId, setHoverBoxId] = useState3(null);
2535
- const [marquee, setMarquee] = useState3(null);
2536
- const [scale, setScale] = useState3(1);
2537
- const rootRef = useRef4(null);
2538
- const svgRef = useRef4(null);
2539
- const dragRef = useRef4(null);
2540
- const pendingPointRef = useRef4(null);
2541
- const frameRef = useRef4(null);
2542
- const lastCommittedRef = useRef4(serializeDrawingData(data));
2543
- const shapesRef = useRef4(shapes);
2544
- shapesRef.current = shapes;
2545
- const heightRef = useRef4(canvasHeight);
2546
- heightRef.current = canvasHeight;
2547
- const widthRef = useRef4(width);
2548
- widthRef.current = width;
2549
- const selectedRef = useRef4(selectedIds);
2550
- selectedRef.current = selectedIds;
2551
- const paths = useMemo(() => computePaths(shapes), [shapes]);
2552
- const canvasWidth = data.canvasWidth;
2553
- const logicalWidth = canvasWidth ?? (width === "content" ? contentWidth(shapes, paths) : null);
2554
- useEffect7(() => {
2555
- const incoming = serializeDrawingData(data);
2556
- if (incoming !== lastCommittedRef.current) {
2557
- lastCommittedRef.current = incoming;
2558
- setShapes(data.shapes);
2559
- setCanvasHeight(data.canvasHeight);
2560
- setWidth(data.width ?? "full");
2561
- setSelectedIds(EMPTY_SET);
2562
- setEditingText(null);
2563
- }
2564
- }, [data]);
2565
- useEffect7(() => {
2566
- const svg = svgRef.current;
2567
- if (!logicalWidth || !svg || typeof ResizeObserver === "undefined") {
2568
- setScale(1);
2569
- return;
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"
2570
2278
  }
2571
- const update = () => {
2572
- const rect = svg.getBoundingClientRect();
2573
- setScale(rect.width > 0 ? rect.width / logicalWidth : 1);
2574
- };
2575
- update();
2576
- const observer = new ResizeObserver(update);
2577
- observer.observe(svg);
2578
- return () => observer.disconnect();
2579
- }, [logicalWidth]);
2580
- const commit = useCallback(
2581
- (nextShapes, options) => {
2582
- const nextWidth = options?.width ?? widthRef.current;
2583
- const explicitFull = options?.width === void 0 && data.width !== void 0;
2584
- const payload = {
2585
- version: 2,
2586
- ...canvasWidth ? { canvasWidth } : {},
2587
- canvasHeight: options?.height ?? heightRef.current,
2588
- ...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
2589
- shapes: nextShapes
2590
- };
2591
- const json = serializeDrawingData(payload);
2592
- if (json === lastCommittedRef.current) return;
2593
- lastCommittedRef.current = json;
2594
- editor.update(() => {
2595
- const node = $getNodeByKey2(nodeKey);
2596
- if ($isDrawingNode(node)) node.setData(payload);
2597
- });
2598
- },
2599
- [editor, nodeKey, canvasWidth, data.width]
2600
- );
2601
- const updateShapes = useCallback(
2602
- (updater, options) => {
2603
- setShapes((prev) => {
2604
- const next = resolveBindings(updater(prev));
2605
- if (options?.commit) commit(next);
2606
- return next;
2607
- });
2608
- },
2609
- [commit]
2610
2279
  );
2611
- const getPoint = useCallback((e) => {
2612
- const svg = svgRef.current;
2613
- if (!svg) return { x: 0, y: 0 };
2614
- const ctm = svg.getScreenCTM();
2615
- if (!ctm) {
2616
- const rect = svg.getBoundingClientRect();
2617
- return { x: e.clientX - rect.left, y: e.clientY - rect.top };
2280
+ }
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
+ ] });
2313
+ }
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"
2618
2329
  }
2619
- const inverse = ctm.inverse();
2620
- return {
2621
- x: inverse.a * e.clientX + inverse.c * e.clientY + inverse.e,
2622
- y: inverse.b * e.clientX + inverse.d * e.clientY + inverse.f
2623
- };
2624
- }, []);
2625
- const selection = useMemo(
2626
- () => shapes.filter((s) => selectedIds.has(s.id)),
2627
- [shapes, selectedIds]
2628
2330
  );
2629
- const single = selection.length === 1 ? selection[0] : null;
2630
- const startTextEditing = useCallback((id, field) => {
2631
- setEditingText({ id, field });
2632
- setSelectedIds(/* @__PURE__ */ new Set([id]));
2633
- }, []);
2634
- const capture = (e) => {
2635
- svgRef.current?.setPointerCapture(e.pointerId);
2636
- };
2637
- const handleBackgroundPointerDown = useCallback(
2638
- (e) => {
2639
- if (!isEditable || editingText || e.button !== 0) return;
2640
- const point = getPoint(e);
2641
- capture(e);
2642
- if (tool === "select") {
2643
- if (!e.shiftKey) setSelectedIds(EMPTY_SET);
2644
- dragRef.current = { mode: "marquee", origin: point, current: point, additive: e.shiftKey };
2645
- return;
2646
- }
2647
- if (tool === "text") {
2648
- const id2 = createShapeId();
2649
- const size = textBoxSize("");
2650
- updateShapes((prev) => [
2651
- ...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",
2652
2384
  {
2653
- id: id2,
2654
- type: "text",
2655
- x: point.x,
2656
- y: point.y - FONT_SIZE / 2,
2657
- w: size.w,
2658
- h: size.h,
2659
- stroke,
2660
- fill: "transparent",
2661
- strokeWidth: 2,
2662
- 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
2663
2388
  }
2664
- ]);
2665
- setTool("select");
2666
- startTextEditing(id2, "text");
2667
- return;
2668
- }
2669
- const id = createShapeId();
2670
- const def = isBoxType(tool) ? BOX_DEFINITIONS[tool] : null;
2671
- const shapeFill = def ? fill === "transparent" && def.defaultFill ? def.defaultFill : fill : "transparent";
2672
- dragRef.current = { mode: "draw", id, origin: point };
2673
- setSelectedIds(EMPTY_SET);
2674
- updateShapes((prev) => [
2675
- ...prev,
2676
- { id, type: tool, x: point.x, y: point.y, w: 0, h: 0, stroke, fill: shapeFill, strokeWidth: 2 }
2677
- ]);
2678
- },
2679
- [isEditable, editingText, getPoint, tool, stroke, fill, updateShapes, startTextEditing]
2680
- );
2681
- const handleShapePointerDown = useCallback(
2682
- (e, shape) => {
2683
- if (!isEditable || tool !== "select" || editingText || e.button !== 0) return;
2684
- e.stopPropagation();
2685
- const point = getPoint(e);
2686
- capture(e);
2687
- const selected = selectedRef.current;
2688
- if (e.shiftKey) {
2689
- if (selected.has(shape.id)) {
2690
- const next2 = new Set(selected);
2691
- next2.delete(shape.id);
2692
- setSelectedIds(next2);
2693
- return;
2694
- }
2695
- const next = new Set(selected).add(shape.id);
2696
- setSelectedIds(next);
2697
- dragRef.current = {
2698
- mode: "move",
2699
- clickedId: shape.id,
2700
- origin: point,
2701
- origs: moveGroup(shapesRef.current, next, shape.id),
2702
- wasSelected: false
2703
- };
2704
- return;
2705
- }
2706
- const wasSelected = selected.size === 1 && selected.has(shape.id);
2707
- const group = selected.has(shape.id) ? selected : /* @__PURE__ */ new Set([shape.id]);
2708
- if (!selected.has(shape.id)) setSelectedIds(group);
2709
- dragRef.current = {
2710
- mode: "move",
2711
- clickedId: shape.id,
2712
- origin: point,
2713
- origs: moveGroup(shapesRef.current, group, shape.id),
2714
- wasSelected
2715
- };
2716
- },
2717
- [isEditable, tool, editingText, getPoint]
2718
- );
2719
- const handleHandlePointerDown = useCallback(
2720
- (e, drag) => {
2721
- if (!isEditable || e.button !== 0) return;
2722
- e.stopPropagation();
2723
- capture(e);
2724
- dragRef.current = drag;
2725
- if (drag.mode === "endpoint") {
2726
- const key = drag.end === "start" ? "startBinding" : "endBinding";
2727
- updateShapes(
2728
- (prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
2729
- );
2730
- }
2731
- },
2732
- [isEditable, updateShapes]
2733
- );
2734
- const flushPointer = useCallback(() => {
2735
- frameRef.current = null;
2736
- const point = pendingPointRef.current;
2737
- const drag = dragRef.current;
2738
- if (!point || !drag) return;
2739
- if (drag.mode === "marquee") {
2740
- drag.current = point;
2741
- setMarquee({ origin: drag.origin, current: point });
2742
- return;
2743
- }
2744
- updateShapes((prev) => applyDrag(prev, drag, point));
2745
- if (drag.mode === "endpoint" || drag.mode === "draw" && isConnectorType(shapesRef.current.find((s) => s.id === drag.id)?.type ?? "")) {
2746
- const box = findBoxAt(shapesRef.current, point, drag.id);
2747
- 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
+ ] });
2748
2399
  }
2749
- }, [updateShapes]);
2750
- const handlePointerMove = useCallback(
2751
- (e) => {
2752
- if (!dragRef.current) return;
2753
- pendingPointRef.current = getPoint(e);
2754
- if (frameRef.current === null) {
2755
- frameRef.current = requestAnimationFrame(flushPointer);
2756
- }
2757
- },
2758
- [getPoint, flushPointer]
2759
- );
2760
- const handlePointerUp = useCallback(
2761
- (e) => {
2762
- const drag = dragRef.current;
2763
- dragRef.current = null;
2764
- if (frameRef.current !== null) {
2765
- cancelAnimationFrame(frameRef.current);
2766
- frameRef.current = null;
2767
- }
2768
- pendingPointRef.current = null;
2769
- setHoverBoxId(null);
2770
- if (!drag) return;
2771
- const point = getPoint(e);
2772
- if (drag.mode === "marquee") {
2773
- setMarquee(null);
2774
- const rect = marqueeRect(drag.origin, point);
2775
- if (rect.w < CLICK_TOLERANCE && rect.h < CLICK_TOLERANCE) return;
2776
- const ids = shapesWithin(shapesRef.current, rect, paths);
2777
- setSelectedIds((prev) => drag.additive ? /* @__PURE__ */ new Set([...prev, ...ids]) : new Set(ids));
2778
- return;
2779
- }
2780
- if (drag.mode === "move" && drag.wasSelected) {
2781
- const moved = Math.abs(point.x - drag.origin.x) > CLICK_TOLERANCE || Math.abs(point.y - drag.origin.y) > CLICK_TOLERANCE;
2782
- const current = shapesRef.current.find((s) => s.id === drag.clickedId);
2783
- if (!moved && current) {
2784
- updateShapes((prev) => applyDrag(prev, drag, drag.origin));
2785
- if (current.type === "text" || isConnectorType(current.type)) {
2786
- startTextEditing(current.id, "text");
2787
- } else {
2788
- 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
2789
2411
  }
2790
- 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
2791
2424
  }
2792
- }
2793
- if (drag.mode === "draw") {
2794
- const drawn = shapesRef.current.find((s) => s.id === drag.id);
2795
- if (drawn && Math.abs(drawn.w) < 4 && Math.abs(drawn.h) < 4) {
2796
- if (drawn && isBoxType(drawn.type)) {
2797
- const size = BOX_DEFINITIONS[drawn.type].defaultSize;
2798
- updateShapes(
2799
- (prev) => prev.map(
2800
- (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
2801
- ),
2802
- { commit: true }
2803
- );
2804
- setTool("select");
2805
- setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
2806
- } else {
2807
- 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
2808
2437
  }
2809
- return;
2810
- }
2811
- if (drawn && isConnectorType(drawn.type)) {
2812
- updateShapes((prev) => prev.map((s) => s.id === drag.id ? bindEndpoints(s, prev) : s));
2813
- }
2814
- setTool("select");
2815
- setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
2816
- }
2817
- if (drag.mode === "endpoint") {
2818
- updateShapes(
2819
- (prev) => prev.map((s) => {
2820
- if (s.id !== drag.id) return s;
2821
- const end = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
2822
- const box = findBoxAt(prev, end, s.id);
2823
- const other = drag.end === "start" ? s.endBinding : s.startBinding;
2824
- if (box && box.id === other?.id) return s;
2825
- const key = drag.end === "start" ? "startBinding" : "endBinding";
2826
- return { ...s, [key]: box ? makeBinding(box, end) : void 0 };
2827
- })
2828
- );
2829
- }
2830
- updateShapes((prev) => prev.map(normalize), { commit: true });
2831
- },
2832
- [getPoint, paths, updateShapes, startTextEditing]
2833
- );
2834
- const deleteSelection = useCallback(() => {
2835
- const ids = selectedRef.current;
2836
- if (!ids.size) return;
2837
- setEditingText(null);
2838
- setSelectedIds(EMPTY_SET);
2839
- updateShapes(
2840
- (prev) => prev.filter(
2841
- (s) => !ids.has(s.id) && !(s.startBinding && ids.has(s.startBinding.id)) && !(s.endBinding && ids.has(s.endBinding.id))
2842
- ),
2843
- { commit: true }
2844
- );
2845
- }, [updateShapes]);
2846
- const editingRef = useRef4(editingText);
2847
- editingRef.current = editingText;
2848
- useEffect7(() => {
2849
- const root = rootRef.current;
2850
- if (!root || !isEditable) return;
2851
- const onKeyDown = (e) => {
2852
- if (editingRef.current) return;
2853
- const ids = selectedRef.current;
2854
- if ((e.key === "Delete" || e.key === "Backspace") && ids.size) {
2855
- e.preventDefault();
2856
- e.stopPropagation();
2857
- deleteSelection();
2858
- } else if (e.key === "Enter" && ids.size === 1) {
2859
- const [id] = ids;
2860
- const shape = shapesRef.current.find((s) => s.id === id);
2861
- if (shape) {
2862
- e.preventDefault();
2863
- e.stopPropagation();
2864
- startTextEditing(shape.id, "text");
2865
- }
2866
- } else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
2867
- e.preventDefault();
2868
- e.stopPropagation();
2869
- setSelectedIds(new Set(shapesRef.current.map((s) => s.id)));
2870
- } else if (e.key === "Escape") {
2871
- e.stopPropagation();
2872
- setSelectedIds(EMPTY_SET);
2873
- setTool("select");
2874
- }
2875
- };
2876
- root.addEventListener("keydown", onKeyDown);
2877
- return () => root.removeEventListener("keydown", onKeyDown);
2878
- }, [isEditable, deleteSelection, startTextEditing]);
2879
- const applyToSelection = useCallback(
2880
- (patch) => {
2881
- const ids = selectedRef.current;
2882
- if (!ids.size) return;
2883
- updateShapes((prev) => prev.map((s) => ids.has(s.id) ? patch(s) : s), { commit: true });
2884
- },
2885
- [updateShapes]
2886
- );
2887
- const applyStroke = useCallback(
2888
- (color) => {
2889
- setStroke(color);
2890
- applyToSelection((s) => ({ ...s, stroke: color }));
2891
- },
2892
- [applyToSelection]
2893
- );
2894
- const applyFill = useCallback(
2895
- (color) => {
2896
- setFill(color);
2897
- applyToSelection((s) => isBoxType(s.type) ? { ...s, fill: color } : s);
2898
- },
2899
- [applyToSelection]
2900
- );
2901
- const applyRouting = useCallback(
2902
- (elbow) => applyToSelection(
2903
- (s) => isConnectorType(s.type) ? { ...s, routing: elbow ? "elbow" : void 0, elbow: void 0, waypoints: void 0 } : s
2904
- ),
2905
- [applyToSelection]
2906
- );
2907
- const applyDirection = useCallback(
2908
- (bidirectional) => applyToSelection(
2909
- (s) => s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
2910
- ),
2911
- [applyToSelection]
2912
- );
2913
- const applyWidth = useCallback(
2914
- (next) => {
2915
- setWidth(next);
2916
- commit(shapesRef.current, { width: next });
2917
- },
2918
- [commit]
2919
- );
2920
- const addWaypoint = useCallback(
2921
- (e, shapeId, segmentIndex, point) => {
2922
- if (!isEditable || e.button !== 0) return;
2923
- e.stopPropagation();
2924
- capture(e);
2925
- updateShapes(
2926
- (prev) => prev.map((s) => {
2927
- if (s.id !== shapeId || !isConnectorType(s.type)) return s;
2928
- const interior = connectorPoints(s, prev).slice(1, -1);
2929
- const waypoints = [...interior.slice(0, segmentIndex), point, ...interior.slice(segmentIndex)];
2930
- return { ...s, waypoints, routing: void 0, elbow: void 0 };
2931
- })
2932
- );
2933
- dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
2934
- },
2935
- [isEditable, updateShapes]
2936
- );
2937
- const removeWaypoint = useCallback(
2938
- (shapeId, index) => {
2939
- updateShapes(
2940
- (prev) => prev.map((s) => {
2941
- if (s.id !== shapeId || !s.waypoints) return s;
2942
- const waypoints = s.waypoints.filter((_, i) => i !== index);
2943
- return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
2944
- }),
2945
- { commit: true }
2946
- );
2947
- },
2948
- [updateShapes]
2949
- );
2950
- const commitText = useCallback(
2951
- (id, field, value) => {
2952
- setEditingText(null);
2953
- updateShapes(
2954
- (prev) => prev.flatMap((s) => {
2955
- if (s.id !== id) return [s];
2956
- if (s.type === "text") {
2957
- 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
2958
2448
  }
2959
- return [{ ...s, [field]: value.trim() === "" ? void 0 : value }];
2960
- }),
2961
- { commit: true }
2962
- );
2963
- },
2964
- [updateShapes]
2965
- );
2966
- const copyMermaid = useCallback(async () => {
2967
- const payload = { version: 2, canvasHeight: heightRef.current, shapes: shapesRef.current };
2968
- try {
2969
- await navigator.clipboard.writeText(drawingToMermaid(payload));
2970
- return true;
2971
- } catch {
2972
- return false;
2449
+ )
2450
+ ] });
2973
2451
  }
2974
- }, []);
2975
- const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
2976
- const hoverBox = hoverBoxId ? shapes.find((s) => s.id === hoverBoxId) : null;
2977
- const showProps = isEditable && !editingText && (selection.length > 0 || tool !== "select");
2978
- const propStroke = single?.stroke ?? selection[0]?.stroke ?? stroke;
2979
- const propFill = single?.fill ?? selection.find((s) => isBoxType(s.type))?.fill ?? fill;
2980
- const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
2981
- return /* @__PURE__ */ jsxs8(
2982
- "div",
2983
- {
2984
- ref: rootRef,
2985
- className: `zui-drawing-canvas ${WIDTH_CLASS[width]} ${isEditable ? "is-editable" : ""}`,
2986
- tabIndex: isEditable ? 0 : void 0,
2987
- onFocus: () => editor.dispatchCommand(DRAWING_FOCUS_COMMAND, nodeKey),
2988
- onBlur: (e) => {
2989
- if (rootRef.current?.contains(e.relatedTarget)) return;
2990
- editor.dispatchCommand(DRAWING_FOCUS_COMMAND, null);
2991
- },
2992
- children: [
2993
- isEditable && /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: [
2994
- /* @__PURE__ */ jsx9(
2995
- DrawingToolbar,
2996
- {
2997
- tool,
2998
- onToolChange: (t) => {
2999
- setTool(t);
3000
- if (t !== "select") setSelectedIds(EMPTY_SET);
3001
- },
3002
- width,
3003
- onWidthChange: applyWidth,
3004
- onCopyMermaid: copyMermaid
3005
- }
3006
- ),
3007
- /* @__PURE__ */ jsx9("div", { className: `zui-drawing-props-host ${showProps ? "is-visible" : ""}`, children: /* @__PURE__ */ jsx9(
3008
- PropertyBar,
3009
- {
3010
- selection,
3011
- showFill: tool !== "select" && isBoxType(tool),
3012
- stroke: propStroke,
3013
- fill: propFill,
3014
- onStroke: applyStroke,
3015
- onFill: applyFill,
3016
- onRouting: applyRouting,
3017
- onDirection: applyDirection,
3018
- onDelete: deleteSelection
3019
- }
3020
- ) })
3021
- ] }),
3022
- /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
3023
- /* @__PURE__ */ jsxs8(
3024
- "svg",
3025
- {
3026
- ref: svgRef,
3027
- className: "zui-drawing-surface",
3028
- style: {
3029
- height: logicalWidth ? void 0 : canvasHeight,
3030
- width: logicalWidth ?? void 0,
3031
- maxWidth: "100%",
3032
- aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
3033
- cursor: canvasCursor
3034
- },
3035
- viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
3036
- preserveAspectRatio: "xMinYMin meet",
3037
- onPointerDown: handleBackgroundPointerDown,
3038
- onPointerMove: handlePointerMove,
3039
- onPointerUp: handlePointerUp,
3040
- onPointerCancel: handlePointerUp,
3041
- onDoubleClick: (e) => {
3042
- if (!isEditable) return;
3043
- const target = e.target.closest("[data-shape-id]");
3044
- const id = target?.getAttribute("data-shape-id");
3045
- const shape = id ? shapes.find((s) => s.id === id) : null;
3046
- if (!shape) return;
3047
- if (shape.type === "text" || isConnectorType(shape.type)) {
3048
- startTextEditing(shape.id, "text");
3049
- } else {
3050
- startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
3051
- }
3052
- },
3053
- children: [
3054
- shapes.map((shape) => /* @__PURE__ */ jsxs8(
3055
- "g",
3056
- {
3057
- "data-shape-id": shape.id,
3058
- className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
3059
- style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
3060
- onPointerDown: (e) => handleShapePointerDown(e, shape),
3061
- children: [
3062
- /* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
3063
- shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
3064
- ShapeView,
3065
- {
3066
- shape,
3067
- points: paths.get(shape.id),
3068
- hideField: shape.id === editingText?.id ? editingText.field : null,
3069
- showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
3070
- }
3071
- )
3072
- ]
3073
- },
3074
- shape.id
3075
- )),
3076
- hoverBox && /* @__PURE__ */ jsx9(
3077
- "polygon",
3078
- {
3079
- className: "zui-drawing-selection zui-drawing-bind-target",
3080
- points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
3081
- fill: "currentColor",
3082
- fillOpacity: 0.06,
3083
- stroke: "currentColor",
3084
- strokeWidth: 2,
3085
- strokeLinejoin: "round",
3086
- pointerEvents: "none"
3087
- }
3088
- ),
3089
- single && isEditable && !editingText && /* @__PURE__ */ jsx9(
3090
- SelectionOverlay,
3091
- {
3092
- shape: single,
3093
- points: paths.get(single.id) ?? [],
3094
- onHandlePointerDown: handleHandlePointerDown,
3095
- onWaypointAdd: addWaypoint,
3096
- onWaypointRemove: removeWaypoint
3097
- }
3098
- ),
3099
- selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
3100
- marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
3101
- ]
3102
- }
3103
- ),
3104
- 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" }),
3105
- editingShape && editingText && /* @__PURE__ */ jsx9(
3106
- "div",
3107
- {
3108
- className: "zui-drawing-overlay",
3109
- style: {
3110
- transform: `scale(${scale})`,
3111
- width: logicalWidth ?? void 0,
3112
- height: logicalWidth ? canvasHeight : void 0
3113
- },
3114
- children: /* @__PURE__ */ jsx9(
3115
- TextEditOverlay,
3116
- {
3117
- shape: editingShape,
3118
- field: editingText.field,
3119
- points: paths.get(editingShape.id),
3120
- onCommit: commitText
3121
- },
3122
- `${editingShape.id}:${editingText.field}`
3123
- )
3124
- }
3125
- ),
3126
- isEditable && /* @__PURE__ */ jsx9(
3127
- HeightHandle,
3128
- {
3129
- height: canvasHeight,
3130
- scale,
3131
- onChange: setCanvasHeight,
3132
- onCommit: (h) => commit(shapesRef.current, { height: h })
3133
- }
3134
- )
3135
- ] })
3136
- ]
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
+ ] });
3137
2471
  }
3138
- );
2472
+ default:
2473
+ return null;
2474
+ }
3139
2475
  }
3140
- function HeightHandle({
3141
- height,
3142
- scale,
3143
- onChange,
3144
- onCommit
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
3145
2514
  }) {
3146
- const dragRef = useRef4(null);
3147
- const latestRef = useRef4(height);
3148
- latestRef.current = height;
3149
- const clamp = (h) => Math.round(Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, h)));
3150
- return /* @__PURE__ */ jsx9(
3151
- "div",
3152
- {
3153
- className: "zui-drawing-resize",
3154
- title: "Drag to resize the canvas",
3155
- role: "separator",
3156
- "aria-orientation": "horizontal",
3157
- onPointerDown: (e) => {
3158
- e.preventDefault();
3159
- e.stopPropagation();
3160
- e.currentTarget.setPointerCapture(e.pointerId);
3161
- dragRef.current = { startY: e.clientY, orig: height };
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
3162
2542
  },
3163
- onPointerMove: (e) => {
3164
- const drag = dragRef.current;
3165
- if (!drag) return;
3166
- onChange(clamp(drag.orig + (e.clientY - drag.startY) / Math.max(scale, 0.05)));
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
3167
2565
  },
3168
- onPointerUp: () => {
3169
- if (!dragRef.current) return;
3170
- dragRef.current = null;
3171
- onCommit(latestRef.current);
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
3172
2589
  },
3173
- children: /* @__PURE__ */ jsx9(Icon, { size: 14, children: UI_ICONS.grip })
3174
- }
3175
- );
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
+ ] });
3176
2604
  }
3177
-
3178
- // src/drawing/skeleton.ts
3179
- var COLOR_PRESETS = {
3180
- gray: { stroke: "#1e1e1e", fill: "transparent" },
3181
- red: { stroke: "#e03131", fill: "#ffc9c9" },
3182
- green: { stroke: "#2f9e44", fill: "#b2f2bb" },
3183
- blue: { stroke: "#1971c2", fill: "#a5d8ff" },
3184
- orange: { stroke: "#f08c00", fill: "#ffec99" },
3185
- purple: { stroke: "#7048e8", fill: "#d0bfff" }
3186
- };
3187
- var COLOR_NAMES = Object.keys(COLOR_PRESETS);
3188
- var TEXT_WRAP_WIDTH = 260;
3189
- var SLOT_GAP = 6;
3190
- var RANK_GAP = 90;
3191
- var STACK_GAP = 40;
3192
- var LAYOUT_ORIGIN = 32;
3193
- var CANVAS_MARGIN = 32;
3194
- var MIN_AUTO_CANVAS_HEIGHT = 120;
3195
- var DEFAULT_STROKE2 = "#1e1e1e";
3196
- var STROKE_WIDTH = 2;
3197
- function isDrawingSkeleton(value) {
3198
- return isRecord2(value) && Array.isArray(value.boxes);
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
2638
+ },
2639
+ i
2640
+ ))
2641
+ ] });
3199
2642
  }
3200
- function parseDrawingSkeleton(json) {
3201
- try {
3202
- const parsed = JSON.parse(json);
3203
- if (!isDrawingSkeleton(parsed)) return EMPTY_DRAWING;
3204
- return expandSkeleton(normalizeSkeleton(parsed));
3205
- } catch {
3206
- return EMPTY_DRAWING;
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
+ ] });
3207
2660
  }
3208
- }
3209
- function expandSkeleton(skeleton) {
3210
- const boxes = dedupeById(skeleton.boxes);
3211
- const boxTypes = new Map(boxes.map((box) => [box.id, box.type ?? "rect"]));
3212
- const sizes = new Map(boxes.map((box) => [box.id, measureBox(box, boxTypes.get(box.id))]));
3213
- const boxIds = new Set(boxTypes.keys());
3214
- const connectors = (skeleton.connectors ?? []).filter(
3215
- (c) => boxIds.has(endId(c.from)) && boxIds.has(endId(c.to)) && endId(c.from) !== endId(c.to)
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))
2681
+ }
3216
2682
  );
3217
- const positions = layoutBoxes(boxes, sizes, connectors, skeleton.direction ?? "right");
3218
- const boxShapes = boxes.map(
3219
- (box) => expandBox(box, boxTypes.get(box.id), sizes.get(box.id), positions.get(box.id))
3220
- );
3221
- const centers = new Map(boxShapes.map((shape) => [shape.id, shapeCenter(shape)]));
3222
- const used = new Set(boxIds);
3223
- const nextId = (prefix) => {
3224
- let i = used.size + 1;
3225
- while (used.has(`${prefix}${i}`)) i += 1;
3226
- const id = `${prefix}${i}`;
3227
- used.add(id);
3228
- return id;
3229
- };
3230
- const connectorShapes = connectors.map((c) => expandConnector(c, centers, nextId("c")));
3231
- const textShapes = (skeleton.texts ?? []).map((t) => expandText(t, nextId("t")));
3232
- const shapes = resolveBindings([...boxShapes, ...connectorShapes, ...textShapes]);
3233
- const bottom = shapes.reduce((max, shape) => {
3234
- const b = bbox(shape);
3235
- return Math.max(max, b.y + b.h);
3236
- }, 0);
3237
- return normalizeDrawingData({
3238
- version: 2,
3239
- ...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
3240
- canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
3241
- ...skeleton.width !== void 0 ? { width: skeleton.width } : {},
3242
- shapes
3243
- });
3244
- }
3245
- function measureBox(box, type) {
3246
- const def = BOX_DEFINITIONS[type];
3247
- const probe = {
3248
- id: "",
3249
- type,
3250
- x: 0,
3251
- y: 0,
3252
- w: def.defaultSize.w,
3253
- h: def.defaultSize.h,
3254
- stroke: "",
3255
- fill: "",
3256
- strokeWidth: 0
3257
- };
3258
- const area = def.textArea(probe);
3259
- const ratioW = area.w / def.defaultSize.w;
3260
- const ratioH = area.h / def.defaultSize.h;
3261
- const wrapWidth = box.w !== void 0 ? Math.max(20, box.w * ratioW) : TEXT_WRAP_WIDTH;
3262
- const slots = def.textSlots.flatMap((slot) => {
3263
- const size = slotSize(box, slot, wrapWidth);
3264
- return size ? [size] : [];
3265
- });
3266
- const innerW = slots.reduce((max, s) => Math.max(max, s.w), 0);
3267
- const innerH = slots.reduce((sum, s) => sum + s.h, 0) + Math.max(0, slots.length - 1) * SLOT_GAP;
3268
- return {
3269
- w: box.w ?? Math.max(def.defaultSize.w, Math.ceil(innerW / ratioW)),
3270
- h: box.h ?? Math.max(def.defaultSize.h, Math.ceil(innerH / ratioH))
3271
- };
3272
- }
3273
- function slotSize(box, slot, wrapWidth) {
3274
- const value = box[slot];
3275
- if (!value) return null;
3276
- const fontSize = slot === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
3277
- return textBoxSize(wrapText(value, wrapWidth, fontSize).join("\n"), fontSize);
3278
2683
  }
3279
- function layoutBoxes(boxes, sizes, connectors, direction) {
3280
- const positions = /* @__PURE__ */ new Map();
3281
- const auto = boxes.filter((box) => box.x === void 0 || box.y === void 0);
3282
- const autoIds = new Set(auto.map((box) => box.id));
3283
- 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));
3284
- const ranks = rankNodes(
3285
- auto.map((box) => box.id),
3286
- acyclicEdges(
3287
- auto.map((box) => box.id),
3288
- edges
3289
- )
3290
- );
3291
- const rows = [];
3292
- for (const box of auto) {
3293
- const rank = ranks.get(box.id) ?? 0;
3294
- (rows[rank] ?? (rows[rank] = [])).push(box.id);
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 });
3295
2690
  }
3296
- const main = direction === "right" ? "w" : "h";
3297
- const cross = direction === "right" ? "h" : "w";
3298
- const rowMain = rows.map((row) => row.reduce((max, id) => Math.max(max, sizes.get(id)[main]), 0));
3299
- const rowCross = rows.map(
3300
- (row) => row.reduce((sum, id) => sum + sizes.get(id)[cross], 0) + Math.max(0, row.length - 1) * STACK_GAP
3301
- );
3302
- const maxCross = rowCross.reduce((max, c) => Math.max(max, c), 0);
3303
- let mainOffset = LAYOUT_ORIGIN;
3304
- rows.forEach((row, rank) => {
3305
- let crossOffset = LAYOUT_ORIGIN + (maxCross - rowCross[rank]) / 2;
3306
- for (const id of row) {
3307
- const size = sizes.get(id);
3308
- const mainPos = Math.round(mainOffset + (rowMain[rank] - size[main]) / 2);
3309
- const crossPos = Math.round(crossOffset);
3310
- positions.set(
3311
- id,
3312
- direction === "right" ? { x: mainPos, y: crossPos } : { x: crossPos, y: mainPos }
3313
- );
3314
- crossOffset += size[cross] + STACK_GAP;
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"
3315
2701
  }
3316
- mainOffset += rowMain[rank] + RANK_GAP;
3317
- });
3318
- for (const box of boxes) {
3319
- const computed = positions.get(box.id) ?? { x: LAYOUT_ORIGIN, y: LAYOUT_ORIGIN };
3320
- positions.set(box.id, { x: box.x ?? computed.x, y: box.y ?? computed.y });
3321
- }
3322
- return positions;
2702
+ );
3323
2703
  }
3324
- function acyclicEdges(ids, edges) {
3325
- const outgoing = /* @__PURE__ */ new Map();
3326
- for (const edge of edges) (outgoing.get(edge.from) ?? outgoing.set(edge.from, []).get(edge.from)).push(edge);
3327
- const state = /* @__PURE__ */ new Map();
3328
- const kept = [];
3329
- const visit = (id) => {
3330
- state.set(id, "visiting");
3331
- for (const edge of outgoing.get(id) ?? []) {
3332
- const target = state.get(edge.to);
3333
- if (target === "visiting") continue;
3334
- kept.push(edge);
3335
- if (target === void 0) visit(edge.to);
3336
- }
3337
- state.set(id, "done");
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
3338
2760
  };
3339
- for (const id of ids) if (!state.has(id)) visit(id);
3340
- return kept;
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
+ });
2812
+ }
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
+ }
2827
+ );
3341
2828
  }
3342
- function rankNodes(ids, edges) {
3343
- const incoming = /* @__PURE__ */ new Map();
3344
- for (const edge of edges) (incoming.get(edge.to) ?? incoming.set(edge.to, []).get(edge.to)).push(edge.from);
3345
- const ranks = /* @__PURE__ */ new Map();
3346
- const rankOf = (id) => {
3347
- const known = ranks.get(id);
3348
- if (known !== void 0) return known;
3349
- const rank = (incoming.get(id) ?? []).reduce((max, from) => Math.max(max, rankOf(from) + 1), 0);
3350
- ranks.set(id, rank);
3351
- return rank;
3352
- };
3353
- for (const id of ids) rankOf(id);
3354
- return ranks;
3355
- }
3356
- function expandBox(box, type, size, position) {
3357
- const preset = box.color ? COLOR_PRESETS[box.color] : void 0;
3358
- const fill = box.fill ?? preset?.fill ?? BOX_DEFINITIONS[type].defaultFill ?? "transparent";
3359
- return {
3360
- id: box.id,
3361
- type,
3362
- x: position.x,
3363
- y: position.y,
3364
- w: size.w,
3365
- h: size.h,
3366
- stroke: box.stroke ?? preset?.stroke ?? DEFAULT_STROKE2,
3367
- fill,
3368
- strokeWidth: STROKE_WIDTH,
3369
- ...box.text ? { text: box.text } : {},
3370
- ...box.label ? { label: box.label } : {},
3371
- ...box.footer ? { footer: box.footer } : {}
3372
- };
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
+ ] })
2865
+ }
2866
+ ];
2867
+ function layoutPreset(width) {
2868
+ return width === "content" ? "compact" : width === "text" ? "comfortable" : "full";
3373
2869
  }
3374
- function expandConnector(connector, centers, fallbackId) {
3375
- const type = connector.type ?? "arrow";
3376
- const from = centers.get(endId(connector.from));
3377
- const to = centers.get(endId(connector.to));
3378
- return {
3379
- id: connector.id ?? fallbackId,
3380
- type,
3381
- x: from.x,
3382
- y: from.y,
3383
- w: to.x - from.x,
3384
- h: to.y - from.y,
3385
- stroke: connector.stroke ?? (connector.color ? COLOR_PRESETS[connector.color].stroke : DEFAULT_STROKE2),
3386
- fill: "transparent",
3387
- strokeWidth: STROKE_WIDTH,
3388
- startBinding: toBinding(connector.from),
3389
- endBinding: toBinding(connector.to),
3390
- ...connector.text ? { text: connector.text } : {},
3391
- ...type === "arrow" && connector.bidirectional ? { bidirectional: true } : {},
3392
- ...connector.routing === "elbow" ? { routing: "elbow" } : {}
3393
- };
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 })
2901
+ }
2902
+ );
3394
2903
  }
3395
- function expandText(text, fallbackId) {
3396
- const size = textBoxSize(text.text);
3397
- return {
3398
- id: text.id ?? fallbackId,
3399
- type: "text",
3400
- x: text.x,
3401
- y: text.y,
3402
- w: size.w,
3403
- h: size.h,
3404
- stroke: text.stroke ?? (text.color ? COLOR_PRESETS[text.color].stroke : DEFAULT_STROKE2),
3405
- fill: "transparent",
3406
- strokeWidth: STROKE_WIDTH,
3407
- text: text.text
3408
- };
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
+ ] });
3409
3004
  }
3410
- function endId(end) {
3411
- return typeof end === "string" ? end : end.id;
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 toBinding(end) {
3414
- if (typeof end === "string" || !end.side) return { id: endId(end) };
3415
- return { id: end.id, fixedPoint: SIDE_FIXED_POINTS[end.side] };
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));
3034
+ }
3035
+ return paths;
3416
3036
  }
3417
- function shapeCenter(shape) {
3418
- const b = bbox(shape);
3419
- return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
3420
- }
3421
- function dedupeById(items) {
3422
- const seen = /* @__PURE__ */ new Set();
3423
- return items.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
3424
- }
3425
- function normalizeSkeleton(raw) {
3426
- const value = raw;
3427
- const boxes = value.boxes.flatMap((b) => {
3428
- const box = normalizeBox(b);
3429
- return box ? [box] : [];
3430
- });
3431
- const connectors = Array.isArray(value.connectors) ? value.connectors.flatMap((c) => {
3432
- const connector = normalizeConnector(c);
3433
- return connector ? [connector] : [];
3434
- }) : [];
3435
- const texts = Array.isArray(value.texts) ? value.texts.flatMap((t) => {
3436
- const text = normalizeText(t);
3437
- return text ? [text] : [];
3438
- }) : [];
3439
- const canvasWidth = finiteNumber(value.canvasWidth);
3440
- const canvasHeight = finiteNumber(value.canvasHeight);
3441
- return {
3442
- direction: value.direction === "down" ? "down" : "right",
3443
- ...canvasWidth !== void 0 ? { canvasWidth } : {},
3444
- ...canvasHeight !== void 0 ? { canvasHeight } : {},
3445
- ...isBlockWidth(value.width) ? { width: value.width } : {},
3446
- boxes,
3447
- connectors,
3448
- texts
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);
3449
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
+ );
3450
3661
  }
3451
- function normalizeBox(value) {
3452
- if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
3453
- if (value.type !== void 0 && !(typeof value.type === "string" && isBoxType(value.type))) {
3454
- return null;
3455
- }
3456
- return compact({
3457
- id: value.id,
3458
- type: value.type,
3459
- label: nonEmptyString(value.label) ? value.label : void 0,
3460
- text: nonEmptyString(value.text) ? value.text : void 0,
3461
- footer: nonEmptyString(value.footer) ? value.footer : void 0,
3462
- x: finiteNumber(value.x),
3463
- y: finiteNumber(value.y),
3464
- w: positiveNumber(value.w),
3465
- h: positiveNumber(value.h),
3466
- color: colorName(value.color),
3467
- stroke: nonEmptyString(value.stroke) ? value.stroke : void 0,
3468
- fill: nonEmptyString(value.fill) ? value.fill : void 0
3469
- });
3470
- }
3471
- function normalizeConnector(value) {
3472
- if (!isRecord2(value)) return null;
3473
- const from = normalizeEnd(value.from);
3474
- const to = normalizeEnd(value.to);
3475
- if (!from || !to) return null;
3476
- if (value.type !== void 0 && !(typeof value.type === "string" && isConnectorType(value.type))) {
3477
- return null;
3478
- }
3479
- return compact({
3480
- id: nonEmptyString(value.id) ? value.id : void 0,
3481
- type: value.type,
3482
- from,
3483
- to,
3484
- text: nonEmptyString(value.text) ? value.text : void 0,
3485
- bidirectional: value.bidirectional === true ? true : void 0,
3486
- routing: value.routing === "elbow" ? "elbow" : void 0,
3487
- color: colorName(value.color),
3488
- stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
3489
- });
3490
- }
3491
- function normalizeEnd(value) {
3492
- if (nonEmptyString(value)) return value;
3493
- if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
3494
- const side = value.side;
3495
- return typeof side === "string" && side in SIDE_FIXED_POINTS ? { id: value.id, side } : { id: value.id };
3496
- }
3497
- function normalizeText(value) {
3498
- if (!isRecord2(value) || !nonEmptyString(value.text)) return null;
3499
- const x = finiteNumber(value.x);
3500
- const y = finiteNumber(value.y);
3501
- if (x === void 0 || y === void 0) return null;
3502
- return compact({
3503
- id: nonEmptyString(value.id) ? value.id : void 0,
3504
- x,
3505
- y,
3506
- text: value.text,
3507
- color: colorName(value.color),
3508
- stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
3509
- });
3510
- }
3511
- function isRecord2(value) {
3512
- return typeof value === "object" && value !== null;
3513
- }
3514
- function nonEmptyString(value) {
3515
- return typeof value === "string" && value !== "";
3516
- }
3517
- function finiteNumber(value) {
3518
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3519
- }
3520
- function positiveNumber(value) {
3521
- const n2 = finiteNumber(value);
3522
- return n2 !== void 0 && n2 > 0 ? n2 : void 0;
3523
- }
3524
- function colorName(value) {
3525
- return typeof value === "string" && COLOR_NAMES.includes(value) ? value : void 0;
3526
- }
3527
- function compact(value) {
3528
- return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
3529
- }
3530
- var COLOR_SCHEMA = {
3531
- enum: COLOR_NAMES,
3532
- description: "Named color preset. Sets the stroke (outline + text) and, for boxes, a matching pastel fill. Omit for the default gray on transparent"
3533
- };
3534
- var END_SCHEMA = {
3535
- oneOf: [
3536
- {
3537
- type: "string",
3538
- description: "Id of a box. The connector attaches to its outline, aimed at the other end"
3539
- },
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",
3540
3674
  {
3541
- type: "object",
3542
- required: ["id"],
3543
- additionalProperties: false,
3544
- properties: {
3545
- id: { type: "string", description: "Id of a box" },
3546
- side: {
3547
- enum: ["top", "right", "bottom", "left"],
3548
- description: "Pin the endpoint to the middle of this side of the box instead of auto-aiming"
3549
- }
3550
- }
3551
- }
3552
- ],
3553
- description: "Box a connector end attaches to: a bare box id, or {id, side} to pick the attach edge"
3554
- };
3555
- var DRAWING_SKELETON_JSON_SCHEMA = {
3556
- $schema: "http://json-schema.org/draft-07/schema#",
3557
- title: "DrawingSkeleton",
3558
- 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",
3559
- type: "object",
3560
- required: ["boxes"],
3561
- additionalProperties: false,
3562
- properties: {
3563
- direction: {
3564
- enum: ["right", "down"],
3565
- 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"'
3566
- },
3567
- canvasWidth: {
3568
- type: "number",
3569
- minimum: 120,
3570
- description: "Logical canvas width in px. Omit for a fluid canvas in CSS pixels"
3571
- },
3572
- canvasHeight: {
3573
- type: "number",
3574
- minimum: 80,
3575
- description: "Canvas height in px. Omit to fit the content"
3576
- },
3577
- width: {
3578
- enum: ["full", "text", "content"],
3579
- 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'
3580
- },
3581
- boxes: {
3582
- type: "array",
3583
- items: { $ref: "#/definitions/box" },
3584
- description: "Card-like shapes carrying up to three text slots. Order is the render order and the stacking order within a layout rank"
3585
- },
3586
- connectors: {
3587
- type: "array",
3588
- items: { $ref: "#/definitions/connector" },
3589
- description: "Arrows and lines between boxes. Connectors whose from/to id does not match a box are dropped"
3590
- },
3591
- texts: {
3592
- type: "array",
3593
- items: { $ref: "#/definitions/text" },
3594
- description: "Free-floating annotations not attached to any box. Prefer box slots (label/text/footer) when the text belongs to a box"
3595
- }
3596
- },
3597
- definitions: {
3598
- box: {
3599
- type: "object",
3600
- required: ["id"],
3601
- additionalProperties: false,
3602
- properties: {
3603
- id: { type: "string", description: "Unique within the drawing; connectors reference it" },
3604
- type: {
3605
- enum: BOX_TYPES,
3606
- 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)'
3607
- },
3608
- label: { type: "string", description: "Small bold heading at the top of the box" },
3609
- text: {
3610
- type: "string",
3611
- description: 'Main content, centered. Wraps automatically; "\\n" forces a line break'
3612
- },
3613
- footer: { type: "string", description: "Small dim line at the bottom of the box" },
3614
- x: { type: "number", description: "Top-left x in px. Omit (with y) to let the layout place the box" },
3615
- y: { type: "number", description: "Top-left y in px. Omit (with x) to let the layout place the box" },
3616
- w: {
3617
- type: "number",
3618
- exclusiveMinimum: 0,
3619
- description: "Width in px. Omit to size from the text (never smaller than the type default)"
3620
- },
3621
- h: {
3622
- type: "number",
3623
- exclusiveMinimum: 0,
3624
- description: "Height in px. Omit to size from the text (never smaller than the type default)"
3625
- },
3626
- color: COLOR_SCHEMA,
3627
- stroke: { type: "string", description: "CSS color of the outline and text; overrides the preset" },
3628
- fill: {
3629
- type: "string",
3630
- description: 'CSS color of the interior ("transparent" for none); overrides the preset'
3631
- }
3632
- }
3633
- },
3634
- connector: {
3635
- type: "object",
3636
- required: ["from", "to"],
3637
- additionalProperties: false,
3638
- properties: {
3639
- id: { type: "string", description: "Optional; generated when omitted" },
3640
- type: {
3641
- enum: CONNECTOR_TYPES,
3642
- description: 'arrow (default; arrowhead at "to") or line (no arrowheads)'
3643
- },
3644
- from: END_SCHEMA,
3645
- to: END_SCHEMA,
3646
- text: { type: "string", description: "Label rendered at the middle of the connector" },
3647
- bidirectional: {
3648
- type: "boolean",
3649
- description: "Arrow only: arrowheads on both ends"
3650
- },
3651
- routing: {
3652
- enum: ["straight", "elbow"],
3653
- description: "straight (default) draws a direct line; elbow draws a right-angled path routed around the boxes"
3654
- },
3655
- color: COLOR_SCHEMA,
3656
- stroke: { type: "string", description: "CSS color of the line and label; overrides the preset" }
3657
- }
3658
- },
3659
- text: {
3660
- type: "object",
3661
- required: ["x", "y", "text"],
3662
- additionalProperties: false,
3663
- properties: {
3664
- id: { type: "string", description: "Optional; generated when omitted" },
3665
- x: { type: "number", description: "Top-left x of the first line, in px" },
3666
- y: { type: "number", description: "Top-left y of the first line, in px" },
3667
- text: { type: "string", description: 'Content; "\\n" forces a line break' },
3668
- color: COLOR_SCHEMA,
3669
- stroke: { type: "string", description: "CSS color of the text; overrides the preset" }
3670
- }
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 })
3671
3696
  }
3672
- }
3673
- };
3697
+ );
3698
+ }
3674
3699
 
3675
3700
  // src/nodes/DrawingNode.tsx
3676
3701
  import { jsx as jsx10 } from "react/jsx-runtime";
@@ -4708,24 +4733,7 @@ function HistoryButtons() {
4708
4733
  }
4709
4734
 
4710
4735
  // src/plugins/TableSettingsPlugin.tsx
4711
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
4712
- var DENSITY_OPTIONS = [
4713
- {
4714
- density: "compact",
4715
- label: "Compact rows",
4716
- icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 8.17h14M3 11.83h14M3 15.5h14" })
4717
- },
4718
- {
4719
- density: "comfortable",
4720
- label: "Comfortable rows",
4721
- icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 10h14M3 15.5h14" })
4722
- },
4723
- {
4724
- density: "spacious",
4725
- label: "Spacious rows",
4726
- icon: /* @__PURE__ */ jsx14("path", { d: "M3 5h14M3 15h14" })
4727
- }
4728
- ];
4736
+ import { jsx as jsx14 } from "react/jsx-runtime";
4729
4737
  function Icon3({ children }) {
4730
4738
  return /* @__PURE__ */ jsx14(
4731
4739
  "svg",
@@ -4788,42 +4796,29 @@ function TableSettingsPlugin() {
4788
4796
  [editor]
4789
4797
  );
4790
4798
  if (!isEditable || !anchor || !settings) return null;
4791
- return /* @__PURE__ */ jsxs11(
4799
+ return /* @__PURE__ */ jsx14(
4792
4800
  "div",
4793
4801
  {
4794
4802
  className: "zui-table-settings",
4795
4803
  role: "toolbar",
4796
4804
  "aria-label": "Table settings",
4797
4805
  style: { top: anchor.top, right: anchor.right },
4798
- children: [
4799
- BLOCK_WIDTH_OPTIONS.map(({ width, label, icon }) => /* @__PURE__ */ jsx14(
4800
- ToolbarButton,
4801
- {
4802
- label,
4803
- active: settings.width === width,
4804
- onClick: () => update({ width }),
4805
- children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4806
- },
4807
- width
4808
- )),
4809
- /* @__PURE__ */ jsx14(ToolbarDivider, {}),
4810
- DENSITY_OPTIONS.map(({ density, label, icon }) => /* @__PURE__ */ jsx14(
4811
- ToolbarButton,
4812
- {
4813
- label,
4814
- active: settings.density === density,
4815
- onClick: () => update({ density }),
4816
- children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4817
- },
4818
- density
4819
- ))
4820
- ]
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
+ ))
4821
4816
  }
4822
4817
  );
4823
4818
  }
4824
4819
 
4825
4820
  // src/EditorContent.tsx
4826
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
4821
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
4827
4822
  function EditorContent({
4828
4823
  placeholder = "Start writing...",
4829
4824
  foldable = true,
@@ -4844,8 +4839,8 @@ function EditorContent({
4844
4839
  }
4845
4840
  );
4846
4841
  }
4847
- return /* @__PURE__ */ jsxs12("div", { className: "zui-text-editor-body", children: [
4848
- /* @__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: [
4849
4844
  /* @__PURE__ */ jsx15(
4850
4845
  RichTextPlugin,
4851
4846
  {
@@ -4873,7 +4868,7 @@ import { useLexicalComposerContext as useLexicalComposerContext10 } from "@lexic
4873
4868
  import {
4874
4869
  TableOfContentsPlugin
4875
4870
  } from "@lexical/react/LexicalTableOfContentsPlugin";
4876
- import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
4871
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4877
4872
  var INDENT_PER_LEVEL = {
4878
4873
  h1: 0,
4879
4874
  h2: 1,
@@ -4929,7 +4924,7 @@ function OutlinePlugin() {
4929
4924
  for (const [key] of entries) {
4930
4925
  editor.getElementByKey(key)?.setAttribute("data-outline-key", key);
4931
4926
  }
4932
- return /* @__PURE__ */ jsxs13(
4927
+ return /* @__PURE__ */ jsxs12(
4933
4928
  "div",
4934
4929
  {
4935
4930
  className: `zui-text-editor-outline ${collapsed ? "is-collapsed" : ""}`,
@@ -4958,7 +4953,7 @@ function OutlinePlugin() {
4958
4953
  )
4959
4954
  }
4960
4955
  ),
4961
- !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: [
4962
4957
  entries.length === 0 && /* @__PURE__ */ jsx16("div", { className: "zui-text-editor-outline-empty", children: "No headings" }),
4963
4958
  entries.map(([key, text, tag]) => /* @__PURE__ */ jsx16(
4964
4959
  "button",
@@ -4981,7 +4976,7 @@ function OutlinePlugin() {
4981
4976
  }
4982
4977
 
4983
4978
  // src/MarkdownEditor.tsx
4984
- import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
4979
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
4985
4980
  var DEFAULT_ITEMS = {
4986
4981
  format: /* @__PURE__ */ jsx17(FormatButtons, {}),
4987
4982
  insert: /* @__PURE__ */ jsx17(InsertButtons, {}),
@@ -5002,7 +4997,7 @@ function MarkdownEditor({
5002
4997
  foldable = true,
5003
4998
  ...rootProps
5004
4999
  }) {
5005
- return /* @__PURE__ */ jsxs14(EditorRoot, { ...rootProps, children: [
5000
+ return /* @__PURE__ */ jsxs13(EditorRoot, { ...rootProps, children: [
5006
5001
  toolbar === true && /* @__PURE__ */ jsx17(EditorToolbar, {}),
5007
5002
  typeof toolbar === "function" && toolbar(DEFAULT_ITEMS),
5008
5003
  /* @__PURE__ */ jsx17(EditorContent, { placeholder, foldable, children: outline && /* @__PURE__ */ jsx17(OutlinePlugin, {}) })