canvu-react 0.3.17 → 0.3.19

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/react.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import getStroke from 'perfect-freehand';
2
- import { createContext, forwardRef, useRef, useState, useCallback, useMemo, useId, useEffect, useImperativeHandle, useLayoutEffect, useContext } from 'react';
3
- import { Hand, MousePointer2, Square, Circle, Minus, ArrowUpRight, PenLine, Highlighter, Eraser, Type, Image as Image$1, Undo2, Redo2, Lock, LockOpen, Shapes, ChevronDown } from 'lucide-react';
2
+ import { createContext, forwardRef, useRef, useState, useCallback, useMemo, useId, useEffect, useImperativeHandle, useContext, useLayoutEffect, Children, isValidElement } from 'react';
3
+ import { useSensors, useSensor, PointerSensor, DndContext } from '@dnd-kit/core';
4
+ import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
5
+ import { CSS } from '@dnd-kit/utilities';
6
+ import { Undo2, Redo2, Shapes, ChevronDown, Hand, MousePointer2, Square, Circle, Minus, ArrowUpRight, PenLine, Highlighter, Eraser, Type, Image as Image$1, Lock, LockOpen, Images, ChevronRight, GripVertical, Copy, RotateCw, Trash2 } from 'lucide-react';
4
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
8
  import { createPortal } from 'react-dom';
6
9
 
@@ -1503,6 +1506,517 @@ function useCanvuChromeContext() {
1503
1506
  return useContext(CanvuChromeContext);
1504
1507
  }
1505
1508
 
1509
+ // src/scene/clone-item.ts
1510
+ init_shape_builders();
1511
+ function cloneVectorSceneItemWithNewId(item) {
1512
+ const id = createShapeId();
1513
+ const copy = JSON.parse(JSON.stringify(item));
1514
+ let next = { ...copy, id };
1515
+ if (next.toolKind === "arrow" && next.line) {
1516
+ next = {
1517
+ ...next,
1518
+ childrenSvg: buildArrowSvg(id, next.line, resolveStrokeStyle(next))
1519
+ };
1520
+ }
1521
+ if (next.toolKind === "text" && next.text !== void 0) {
1522
+ return rebuildItemSvg(next);
1523
+ }
1524
+ if (next.toolKind === "custom" && next.customInnerSvg && next.customIntrinsicSize) {
1525
+ return rebuildItemSvg(next);
1526
+ }
1527
+ return next;
1528
+ }
1529
+ function cloneVectorSceneItemsWithNewIds(items) {
1530
+ return items.map(cloneVectorSceneItemWithNewId);
1531
+ }
1532
+
1533
+ // src/scene/managed-images.ts
1534
+ var MANAGED_KEY = "managed";
1535
+ var STACK_GAP_WORLD = 16;
1536
+ function isManagedImage(item) {
1537
+ const data = item.pluginData;
1538
+ return data != null && data[MANAGED_KEY] === true;
1539
+ }
1540
+ function markImageAsManaged(item) {
1541
+ return {
1542
+ ...item,
1543
+ locked: true,
1544
+ pluginData: { ...item.pluginData ?? {}, [MANAGED_KEY]: true }
1545
+ };
1546
+ }
1547
+ function restackManagedImages(items) {
1548
+ let anchor;
1549
+ for (const item of items) {
1550
+ if (!isManagedImage(item)) continue;
1551
+ if (!anchor || item.bounds.y < anchor.bounds.y) anchor = item;
1552
+ }
1553
+ if (!anchor) return [...items];
1554
+ const anchorCenterX = anchor.bounds.x + anchor.bounds.width / 2;
1555
+ const anchorTopY = anchor.bounds.y;
1556
+ let cursorY = anchorTopY;
1557
+ return items.map((item) => {
1558
+ if (!isManagedImage(item)) return item;
1559
+ const newX = anchorCenterX - item.bounds.width / 2;
1560
+ const newY = cursorY;
1561
+ cursorY = newY + item.bounds.height + STACK_GAP_WORLD;
1562
+ if (item.bounds.x === newX && item.bounds.y === newY) return item;
1563
+ return {
1564
+ ...item,
1565
+ x: newX,
1566
+ y: newY,
1567
+ bounds: { ...item.bounds, x: newX, y: newY }
1568
+ };
1569
+ });
1570
+ }
1571
+ function copyManagedImage(items, id) {
1572
+ const idx = items.findIndex((i) => i.id === id);
1573
+ if (idx < 0) return [...items];
1574
+ const source = items[idx];
1575
+ if (!source) return [...items];
1576
+ const clone = markImageAsManaged(cloneVectorSceneItemWithNewId(source));
1577
+ const inserted = [...items.slice(0, idx + 1), clone, ...items.slice(idx + 1)];
1578
+ return restackManagedImages(inserted);
1579
+ }
1580
+ function rotateManagedImage(items, id) {
1581
+ return items.map(
1582
+ (i) => i.id === id ? { ...i, rotation: ((i.rotation ?? 0) + Math.PI / 2) % (Math.PI * 2) } : i
1583
+ );
1584
+ }
1585
+ function deleteManagedImage(items, id) {
1586
+ return restackManagedImages(items.filter((i) => i.id !== id));
1587
+ }
1588
+ function reorderManagedImages(items, orderedManagedIds) {
1589
+ const managedSlots = [];
1590
+ for (let i = 0; i < items.length; i++) {
1591
+ const item = items[i];
1592
+ if (item && isManagedImage(item)) managedSlots.push(i);
1593
+ }
1594
+ if (managedSlots.length !== orderedManagedIds.length) {
1595
+ return [...items];
1596
+ }
1597
+ const byId = new Map(items.map((i) => [i.id, i]));
1598
+ const next = [...items];
1599
+ managedSlots.forEach((slot, k) => {
1600
+ const orderedId = orderedManagedIds[k];
1601
+ if (orderedId === void 0) return;
1602
+ const replacement = byId.get(orderedId);
1603
+ if (replacement) next[slot] = replacement;
1604
+ });
1605
+ return restackManagedImages(next);
1606
+ }
1607
+ var panelStyle = {
1608
+ width: "fit-content",
1609
+ maxHeight: "min(85dvh, 820px)",
1610
+ overflow: "auto",
1611
+ backgroundColor: "#ffffff",
1612
+ border: "1px solid #e2e8f0",
1613
+ borderRadius: 10,
1614
+ boxShadow: "0 10px 40px rgba(15, 23, 42, 0.12)",
1615
+ fontFamily: "system-ui, sans-serif",
1616
+ fontSize: 14,
1617
+ color: "#0f172a"
1618
+ };
1619
+ var headerStyle = {
1620
+ display: "flex",
1621
+ alignItems: "center",
1622
+ justifyContent: "space-between",
1623
+ gap: 8,
1624
+ padding: "8px 8px 8px 14px",
1625
+ borderBottom: "1px solid #e2e8f0",
1626
+ fontWeight: 600,
1627
+ letterSpacing: "0.02em"
1628
+ };
1629
+ var headerTitleStyle = {
1630
+ display: "inline-flex",
1631
+ alignItems: "center",
1632
+ gap: 8
1633
+ };
1634
+ var countStyle = {
1635
+ color: "#64748b",
1636
+ fontWeight: 500
1637
+ };
1638
+ var listStyle = {
1639
+ display: "flex",
1640
+ flexDirection: "column",
1641
+ padding: 8,
1642
+ gap: 6
1643
+ };
1644
+ var rowStyle = {
1645
+ display: "flex",
1646
+ alignItems: "center",
1647
+ gap: 12,
1648
+ padding: "10px 12px",
1649
+ borderRadius: 10
1650
+ };
1651
+ var handleStyle = {
1652
+ display: "inline-flex",
1653
+ alignItems: "center",
1654
+ justifyContent: "center",
1655
+ width: 28,
1656
+ height: 128,
1657
+ border: "none",
1658
+ background: "transparent",
1659
+ color: "#94a3b8",
1660
+ cursor: "grab",
1661
+ touchAction: "none",
1662
+ padding: 0
1663
+ };
1664
+ var thumbBoxStyle = {
1665
+ width: 128,
1666
+ height: 128,
1667
+ flex: "0 0 auto",
1668
+ overflow: "hidden",
1669
+ borderRadius: 8,
1670
+ backgroundColor: "#e2e8f0",
1671
+ display: "flex",
1672
+ alignItems: "center",
1673
+ justifyContent: "center"
1674
+ };
1675
+ var thumbImgStyle = {
1676
+ width: "100%",
1677
+ height: "100%",
1678
+ objectFit: "cover",
1679
+ display: "block"
1680
+ };
1681
+ var actionsColumnStyle = {
1682
+ display: "flex",
1683
+ flexDirection: "column",
1684
+ alignItems: "center",
1685
+ justifyContent: "center",
1686
+ gap: 4
1687
+ };
1688
+ var actionButtonStyle = {
1689
+ display: "inline-flex",
1690
+ alignItems: "center",
1691
+ justifyContent: "center",
1692
+ width: 32,
1693
+ height: 32,
1694
+ border: "none",
1695
+ borderRadius: 6,
1696
+ background: "transparent",
1697
+ color: "#0f172a",
1698
+ cursor: "pointer",
1699
+ padding: 0
1700
+ };
1701
+ var tooltipWrapperStyle = {
1702
+ position: "relative",
1703
+ display: "inline-flex"
1704
+ };
1705
+ var tooltipBubbleStyle = {
1706
+ position: "absolute",
1707
+ right: "calc(100% + 8px)",
1708
+ top: "50%",
1709
+ transform: "translateY(-50%)",
1710
+ padding: "4px 8px",
1711
+ backgroundColor: "#0f172a",
1712
+ color: "#ffffff",
1713
+ fontSize: 11,
1714
+ fontWeight: 500,
1715
+ borderRadius: 4,
1716
+ whiteSpace: "nowrap",
1717
+ pointerEvents: "none",
1718
+ zIndex: 100,
1719
+ boxShadow: "0 2px 8px rgba(15, 23, 42, 0.25)"
1720
+ };
1721
+ var dangerColor = "#b91c1c";
1722
+ var collapseButtonStyle = {
1723
+ display: "inline-flex",
1724
+ alignItems: "center",
1725
+ justifyContent: "center",
1726
+ width: 32,
1727
+ height: 32,
1728
+ border: "none",
1729
+ borderRadius: 8,
1730
+ background: "transparent",
1731
+ color: "#0f172a",
1732
+ cursor: "pointer",
1733
+ padding: 0
1734
+ };
1735
+ var collapsedButtonStyle = {
1736
+ display: "inline-flex",
1737
+ alignItems: "center",
1738
+ justifyContent: "center",
1739
+ gap: 6,
1740
+ height: 44,
1741
+ minWidth: 44,
1742
+ padding: "0 12px",
1743
+ border: "1px solid #e2e8f0",
1744
+ borderRadius: 22,
1745
+ background: "#ffffff",
1746
+ color: "#0f172a",
1747
+ cursor: "pointer",
1748
+ fontFamily: "system-ui, sans-serif",
1749
+ fontSize: 13,
1750
+ fontWeight: 600,
1751
+ boxShadow: "0 8px 24px rgba(15, 23, 42, 0.12)"
1752
+ };
1753
+ var collapsedCountStyle = {
1754
+ display: "inline-flex",
1755
+ alignItems: "center",
1756
+ justifyContent: "center",
1757
+ minWidth: 20,
1758
+ height: 20,
1759
+ padding: "0 6px",
1760
+ borderRadius: 10,
1761
+ backgroundColor: "#0f172a",
1762
+ color: "#ffffff",
1763
+ fontSize: 11,
1764
+ fontWeight: 600
1765
+ };
1766
+ var defaultLabels = {
1767
+ title: "Images",
1768
+ dragHandle: "Drag to reorder",
1769
+ focus: "Focus on canvas",
1770
+ copy: "Copy",
1771
+ rotate: "Rotate",
1772
+ delete: "Delete",
1773
+ collapse: "Collapse images menu",
1774
+ expand: "Open images menu"
1775
+ };
1776
+ function ImagesMenu({
1777
+ items,
1778
+ onItemsChange,
1779
+ onFocusItem,
1780
+ labels
1781
+ }) {
1782
+ const managed = useMemo(() => items.filter(isManagedImage), [items]);
1783
+ const sensors = useSensors(
1784
+ useSensor(PointerSensor, {
1785
+ activationConstraint: { distance: 4 }
1786
+ })
1787
+ );
1788
+ const [collapsed, setCollapsed] = useState(false);
1789
+ if (managed.length === 0) {
1790
+ return null;
1791
+ }
1792
+ const resolvedLabels = { ...defaultLabels, ...labels };
1793
+ if (collapsed) {
1794
+ return /* @__PURE__ */ jsxs(
1795
+ "button",
1796
+ {
1797
+ type: "button",
1798
+ "data-slot": "images-menu-collapsed",
1799
+ style: collapsedButtonStyle,
1800
+ "aria-label": resolvedLabels.expand,
1801
+ title: resolvedLabels.expand,
1802
+ onClick: () => setCollapsed(false),
1803
+ children: [
1804
+ /* @__PURE__ */ jsx(Images, { size: 20 }),
1805
+ /* @__PURE__ */ jsx("span", { style: collapsedCountStyle, children: managed.length })
1806
+ ]
1807
+ }
1808
+ );
1809
+ }
1810
+ const onDragEnd = (event) => {
1811
+ const { active, over } = event;
1812
+ if (!over || active.id === over.id) return;
1813
+ const oldIndex = managed.findIndex((i) => i.id === active.id);
1814
+ const newIndex = managed.findIndex((i) => i.id === over.id);
1815
+ if (oldIndex < 0 || newIndex < 0) return;
1816
+ const reorderedManaged = arrayMove(managed, oldIndex, newIndex);
1817
+ const orderedIds = reorderedManaged.map((i) => i.id);
1818
+ onItemsChange(reorderManagedImages(items, orderedIds));
1819
+ };
1820
+ return /* @__PURE__ */ jsxs(
1821
+ "section",
1822
+ {
1823
+ "data-slot": "images-menu",
1824
+ style: panelStyle,
1825
+ "aria-label": resolvedLabels.title,
1826
+ children: [
1827
+ /* @__PURE__ */ jsxs("div", { style: headerStyle, children: [
1828
+ /* @__PURE__ */ jsxs("span", { style: headerTitleStyle, children: [
1829
+ /* @__PURE__ */ jsx("span", { children: resolvedLabels.title }),
1830
+ /* @__PURE__ */ jsx("span", { style: countStyle, children: managed.length })
1831
+ ] }),
1832
+ /* @__PURE__ */ jsx(
1833
+ "button",
1834
+ {
1835
+ type: "button",
1836
+ style: collapseButtonStyle,
1837
+ "aria-label": resolvedLabels.collapse,
1838
+ title: resolvedLabels.collapse,
1839
+ onClick: () => setCollapsed(true),
1840
+ children: /* @__PURE__ */ jsx(ChevronRight, { size: 20 })
1841
+ }
1842
+ )
1843
+ ] }),
1844
+ /* @__PURE__ */ jsx(DndContext, { sensors, onDragEnd, children: /* @__PURE__ */ jsx(
1845
+ SortableContext,
1846
+ {
1847
+ items: managed.map((i) => i.id),
1848
+ strategy: verticalListSortingStrategy,
1849
+ children: /* @__PURE__ */ jsx("div", { style: listStyle, children: managed.map((item) => /* @__PURE__ */ jsx(
1850
+ ImagesMenuRow,
1851
+ {
1852
+ item,
1853
+ labels: resolvedLabels,
1854
+ onFocus: onFocusItem ? () => onFocusItem(item) : void 0,
1855
+ onCopy: () => onItemsChange(copyManagedImage(items, item.id)),
1856
+ onRotate: () => onItemsChange(rotateManagedImage(items, item.id)),
1857
+ onDelete: () => onItemsChange(deleteManagedImage(items, item.id))
1858
+ },
1859
+ item.id
1860
+ )) })
1861
+ }
1862
+ ) })
1863
+ ]
1864
+ }
1865
+ );
1866
+ }
1867
+ function ImagesMenuRow({
1868
+ item,
1869
+ labels,
1870
+ onFocus,
1871
+ onCopy,
1872
+ onRotate,
1873
+ onDelete
1874
+ }) {
1875
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id });
1876
+ const wrapperStyle = {
1877
+ ...rowStyle,
1878
+ transform: CSS.Transform.toString(transform),
1879
+ transition,
1880
+ background: isDragging ? "#eef2f7" : "transparent",
1881
+ opacity: isDragging ? 0.85 : 1
1882
+ };
1883
+ const src = item.imageThumbnailHref ?? item.imageRasterHref ?? "";
1884
+ return /* @__PURE__ */ jsxs("div", { ref: setNodeRef, style: wrapperStyle, children: [
1885
+ /* @__PURE__ */ jsx(
1886
+ "button",
1887
+ {
1888
+ type: "button",
1889
+ style: handleStyle,
1890
+ "aria-label": labels.dragHandle,
1891
+ title: labels.dragHandle,
1892
+ ...attributes,
1893
+ ...listeners,
1894
+ children: /* @__PURE__ */ jsx(GripVertical, { size: 18 })
1895
+ }
1896
+ ),
1897
+ /* @__PURE__ */ jsx(FocusTarget, { label: labels.focus, onFocus, children: /* @__PURE__ */ jsx("div", { style: thumbBoxStyle, children: src ? /* @__PURE__ */ jsx("img", { src, alt: "", style: thumbImgStyle, draggable: false }) : null }) }),
1898
+ /* @__PURE__ */ jsxs("div", { style: actionsColumnStyle, children: [
1899
+ /* @__PURE__ */ jsx(ImagesMenuAction, { label: labels.copy, onClick: onCopy, children: /* @__PURE__ */ jsx(Copy, { size: 18 }) }),
1900
+ /* @__PURE__ */ jsx(ImagesMenuAction, { label: labels.rotate, onClick: onRotate, children: /* @__PURE__ */ jsx(RotateCw, { size: 18 }) }),
1901
+ /* @__PURE__ */ jsx(ImagesMenuAction, { label: labels.delete, onClick: onDelete, danger: true, children: /* @__PURE__ */ jsx(Trash2, { size: 18 }) })
1902
+ ] })
1903
+ ] });
1904
+ }
1905
+ var focusTargetBaseStyle = {
1906
+ display: "flex",
1907
+ alignItems: "center",
1908
+ flex: "0 0 auto",
1909
+ padding: 0,
1910
+ border: "none",
1911
+ background: "transparent",
1912
+ textAlign: "left",
1913
+ color: "inherit",
1914
+ font: "inherit"
1915
+ };
1916
+ function FocusTarget({ label, onFocus, children }) {
1917
+ if (!onFocus) {
1918
+ return /* @__PURE__ */ jsx("div", { style: { ...focusTargetBaseStyle, cursor: "default" }, children });
1919
+ }
1920
+ return /* @__PURE__ */ jsx(
1921
+ "button",
1922
+ {
1923
+ type: "button",
1924
+ style: { ...focusTargetBaseStyle, cursor: "pointer" },
1925
+ "aria-label": label,
1926
+ title: label,
1927
+ onClick: onFocus,
1928
+ children
1929
+ }
1930
+ );
1931
+ }
1932
+ function ImagesMenuAction({
1933
+ label,
1934
+ onClick,
1935
+ danger,
1936
+ children
1937
+ }) {
1938
+ const [open, setOpen] = useState(false);
1939
+ const style = {
1940
+ ...actionButtonStyle,
1941
+ color: danger ? dangerColor : actionButtonStyle.color
1942
+ };
1943
+ return /* @__PURE__ */ jsxs("span", { style: tooltipWrapperStyle, children: [
1944
+ /* @__PURE__ */ jsx(
1945
+ "button",
1946
+ {
1947
+ type: "button",
1948
+ style,
1949
+ "aria-label": label,
1950
+ onClick,
1951
+ onMouseEnter: () => setOpen(true),
1952
+ onMouseLeave: () => setOpen(false),
1953
+ onFocus: () => setOpen(true),
1954
+ onBlur: () => setOpen(false),
1955
+ children
1956
+ }
1957
+ ),
1958
+ open ? /* @__PURE__ */ jsx("span", { role: "tooltip", style: tooltipBubbleStyle, children: label }) : null
1959
+ ] });
1960
+ }
1961
+
1962
+ // src/react/board-position.ts
1963
+ function getBoardPositionStyle(position, inset = 12, zIndex = 40) {
1964
+ const base2 = { position: "absolute", zIndex };
1965
+ switch (position) {
1966
+ case "fill":
1967
+ return { ...base2, inset };
1968
+ case "top-left":
1969
+ case "left-top":
1970
+ return { ...base2, top: inset, left: inset };
1971
+ case "top-center":
1972
+ return {
1973
+ ...base2,
1974
+ top: inset,
1975
+ left: "50%",
1976
+ transform: "translateX(-50%)"
1977
+ };
1978
+ case "top-right":
1979
+ case "right-top":
1980
+ return { ...base2, top: inset, right: inset };
1981
+ case "bottom-left":
1982
+ case "left-bottom":
1983
+ return { ...base2, bottom: inset, left: inset };
1984
+ case "bottom-center":
1985
+ return {
1986
+ ...base2,
1987
+ bottom: inset,
1988
+ left: "50%",
1989
+ transform: "translateX(-50%)"
1990
+ };
1991
+ case "bottom-right":
1992
+ case "right-bottom":
1993
+ return { ...base2, bottom: inset, right: inset };
1994
+ case "left-center":
1995
+ return {
1996
+ ...base2,
1997
+ left: inset,
1998
+ top: "50%",
1999
+ transform: "translateY(-50%)"
2000
+ };
2001
+ case "right-center":
2002
+ return {
2003
+ ...base2,
2004
+ right: inset,
2005
+ top: "50%",
2006
+ transform: "translateY(-50%)"
2007
+ };
2008
+ case "center":
2009
+ return {
2010
+ ...base2,
2011
+ top: "50%",
2012
+ left: "50%",
2013
+ transform: "translate(-50%, -50%)"
2014
+ };
2015
+ default:
2016
+ return base2;
2017
+ }
2018
+ }
2019
+
1506
2020
  // src/math/item-transform.ts
1507
2021
  init_rect();
1508
2022
  function getItemRotationRad(item) {
@@ -1564,1444 +2078,1498 @@ function boundsAabbForRotatedItem(item) {
1564
2078
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1565
2079
  }
1566
2080
 
1567
- // src/react/NavMenu.tsx
2081
+ // src/react/navmenu/minimap.tsx
1568
2082
  init_rect();
1569
- var shellLook = {
1570
- display: "flex",
1571
- flexDirection: "column",
1572
- gap: 8,
1573
- minWidth: 160,
1574
- padding: "10px 12px",
1575
- borderRadius: 8,
1576
- border: "1px solid rgba(0,0,0,0.12)",
1577
- background: "rgba(255,255,255,0.96)",
1578
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
1579
- pointerEvents: "auto"
2083
+ var NavMenuMinimapSlotContext = createContext(null);
2084
+ function noop() {
2085
+ }
2086
+ function preventMouseDefault(e) {
2087
+ if (e.pointerType === "mouse") {
2088
+ e.preventDefault();
2089
+ }
2090
+ }
2091
+ var BG = "#ffffff";
2092
+ var BORDER_COLOR = "rgba(0,0,0,0.12)";
2093
+ var BORDER_RADIUS = 8;
2094
+ var btnStyle = {
2095
+ pointerEvents: "auto",
2096
+ width: 36,
2097
+ height: 36,
2098
+ display: "inline-flex",
2099
+ alignItems: "center",
2100
+ justifyContent: "center",
2101
+ cursor: "pointer",
2102
+ fontSize: 18,
2103
+ lineHeight: 1,
2104
+ color: "#18181b",
2105
+ padding: 0,
2106
+ border: "none",
2107
+ outline: "none",
2108
+ background: "none",
2109
+ WebkitTapHighlightColor: "transparent"
2110
+ };
2111
+ var chevronBtnStyle = {
2112
+ pointerEvents: "auto",
2113
+ width: 24,
2114
+ height: 36,
2115
+ display: "inline-flex",
2116
+ alignItems: "center",
2117
+ justifyContent: "center",
2118
+ cursor: "pointer",
2119
+ fontSize: 12,
2120
+ lineHeight: 1,
2121
+ color: "#52525b",
2122
+ padding: 0,
2123
+ border: "none",
2124
+ outline: "none",
2125
+ background: "none",
2126
+ WebkitTapHighlightColor: "transparent"
2127
+ };
2128
+ var undoBtnStyle = {
2129
+ ...chevronBtnStyle,
2130
+ width: 30,
2131
+ opacity: 1,
2132
+ color: "#18181b"
1580
2133
  };
1581
2134
  var labelStyle = {
2135
+ fontSize: 10,
2136
+ fontWeight: 600,
2137
+ color: "#52525b",
2138
+ textAlign: "center",
2139
+ userSelect: "none",
2140
+ padding: "2px 0"
2141
+ };
2142
+ var panelLayoutStyle = {
1582
2143
  display: "flex",
1583
2144
  flexDirection: "column",
2145
+ alignItems: "flex-start",
1584
2146
  gap: 4,
1585
- fontSize: 11,
1586
- fontWeight: 600,
1587
- color: "#52525b"
2147
+ touchAction: "none",
2148
+ pointerEvents: "none",
2149
+ userSelect: "none"
1588
2150
  };
1589
- function normalizeHex(stroke) {
1590
- if (/^#[0-9A-Fa-f]{6}$/.test(stroke)) return stroke;
1591
- return "#2563eb";
1592
- }
1593
- function isStylableKind(tk) {
1594
- return tk === "rect" || tk === "ellipse" || tk === "line" || tk === "arrow" || tk === "draw" || tk === "pencil" || tk === "brush" || tk === "marker" || tk === "text";
1595
- }
1596
- function VectorSelectionInspector({
1597
- items: itemsProp,
1598
- activeToolStyle: activeToolStyleProp,
1599
- onChange: onChangeProp,
1600
- position = "top-left",
1601
- inset = 12,
1602
- zIndex = 24,
2151
+ var innerStyle = {
2152
+ pointerEvents: "auto",
2153
+ display: "flex",
2154
+ flexDirection: "row",
2155
+ alignItems: "center",
2156
+ gap: 4,
2157
+ background: BG,
2158
+ borderRadius: BORDER_RADIUS,
2159
+ border: `1px solid ${BORDER_COLOR}`,
2160
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
2161
+ padding: 4
2162
+ };
2163
+ var minimapSlotStyle = {
2164
+ display: "contents"
2165
+ };
2166
+ var MINIMAP_W = 180;
2167
+ var MINIMAP_H = 120;
2168
+ var PADDING = 12;
2169
+ var ITEM_FILL = "rgba(0,0,0,0.08)";
2170
+ var ITEM_STROKE = "rgba(0,0,0,0.15)";
2171
+ var VIEWPORT_FILL = "rgba(59,130,246,0.08)";
2172
+ var VIEWPORT_STROKE = "#3b82f6";
2173
+ function NavMenuMinimap({
1603
2174
  className,
1604
- style
1605
- }) {
2175
+ defaultExpanded = false,
2176
+ camera: cameraProp,
2177
+ viewportWidth: viewportWidthProp,
2178
+ viewportHeight: viewportHeightProp,
2179
+ items: itemsProp,
2180
+ onRequestRender: onRequestRenderProp
2181
+ } = {}) {
1606
2182
  const ctx = useCanvuChromeContext();
1607
- const items = itemsProp ?? ctx?.selectedItems ?? [];
1608
- const activeToolStyle = activeToolStyleProp === void 0 ? ctx?.activeToolStyle ?? null : activeToolStyleProp;
1609
- const onChange = onChangeProp ?? ctx?.onSelectionStyleChange ?? null;
1610
- if (!onChange) return null;
1611
- const shell = {
1612
- ...getBoardPositionStyle(position, inset, zIndex),
1613
- ...shellLook,
1614
- ...style
2183
+ const camera = cameraProp ?? ctx?.camera ?? null;
2184
+ const viewportWidth = viewportWidthProp ?? ctx?.viewportWidth ?? 0;
2185
+ const viewportHeight = viewportHeightProp ?? ctx?.viewportHeight ?? 0;
2186
+ const items = itemsProp ?? ctx?.items ?? [];
2187
+ const onRequestRender = onRequestRenderProp ?? ctx?.onRequestRender ?? noop;
2188
+ const slotEl = useContext(NavMenuMinimapSlotContext);
2189
+ const [expanded, setExpanded] = useState(defaultExpanded);
2190
+ const svgRef = useRef(null);
2191
+ const [dragging, setDragging] = useState(false);
2192
+ const [mouseDown, setMouseDown] = useState(false);
2193
+ const worldBounds = computeWorldBounds(items);
2194
+ const isEmpty = worldBounds.width <= 0 || worldBounds.height <= 0;
2195
+ const scale = Math.min(
2196
+ (MINIMAP_W - PADDING * 2) / Math.max(worldBounds.width, 1),
2197
+ (MINIMAP_H - PADDING * 2) / Math.max(worldBounds.height, 1)
2198
+ );
2199
+ const originX = worldBounds.x;
2200
+ const originY = worldBounds.y;
2201
+ const toMinimapX = (wx) => (wx - originX) * scale + PADDING;
2202
+ const toMinimapY = (wy) => (wy - originY) * scale + PADDING;
2203
+ const toMinimapW = (ww) => ww * scale;
2204
+ const toMinimapH = (wh) => wh * scale;
2205
+ const viewportWorld = camera ? camera.getVisibleWorldRect(viewportWidth, viewportHeight) : { x: 0, y: 0, width: 0, height: 0 };
2206
+ const vpMinimap = {
2207
+ x: toMinimapX(viewportWorld.x),
2208
+ y: toMinimapY(viewportWorld.y),
2209
+ width: toMinimapW(viewportWorld.width),
2210
+ height: toMinimapH(viewportWorld.height)
1615
2211
  };
1616
- if (activeToolStyle) {
1617
- const stroke2 = activeToolStyle.stroke;
1618
- const strokeWidth2 = activeToolStyle.strokeWidth;
1619
- const hex2 = normalizeHex(stroke2);
1620
- const showMarkerOpacity2 = activeToolStyle.toolKind === "marker";
1621
- const opacityPct2 = Math.round((activeToolStyle.strokeOpacity ?? 1) * 100);
1622
- return /* @__PURE__ */ jsxs(
1623
- "section",
2212
+ const worldFromMinimap = useCallback(
2213
+ (mx, my) => ({
2214
+ worldX: (mx - PADDING) / scale + originX,
2215
+ worldY: (my - PADDING) / scale + originY
2216
+ }),
2217
+ [scale, originX, originY]
2218
+ );
2219
+ const panTo = useCallback(
2220
+ (mx, my) => {
2221
+ if (!camera) return;
2222
+ const { worldX, worldY } = worldFromMinimap(mx, my);
2223
+ camera.x = viewportWidth / 2 - worldX * camera.zoom;
2224
+ camera.y = viewportHeight / 2 - worldY * camera.zoom;
2225
+ onRequestRender();
2226
+ },
2227
+ [worldFromMinimap, camera, viewportWidth, viewportHeight, onRequestRender]
2228
+ );
2229
+ const handlePointerDown = useCallback(
2230
+ (e) => {
2231
+ if (isEmpty) return;
2232
+ setDragging(true);
2233
+ setMouseDown(true);
2234
+ const rect = svgRef.current?.getBoundingClientRect();
2235
+ if (!rect) return;
2236
+ panTo(e.clientX - rect.left, e.clientY - rect.top);
2237
+ svgRef.current?.setPointerCapture(e.pointerId);
2238
+ },
2239
+ [isEmpty, panTo]
2240
+ );
2241
+ const handlePointerMove = useCallback(
2242
+ (e) => {
2243
+ if (!mouseDown || isEmpty) return;
2244
+ const rect = svgRef.current?.getBoundingClientRect();
2245
+ if (!rect) return;
2246
+ panTo(e.clientX - rect.left, e.clientY - rect.top);
2247
+ },
2248
+ [mouseDown, isEmpty, panTo]
2249
+ );
2250
+ const handlePointerUp = useCallback(() => {
2251
+ setDragging(false);
2252
+ setMouseDown(false);
2253
+ }, []);
2254
+ const toggleExpanded = useCallback(() => {
2255
+ setExpanded((v) => !v);
2256
+ }, []);
2257
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2258
+ /* @__PURE__ */ jsx(
2259
+ "button",
1624
2260
  {
1625
- "data-slot": "vector-selection-inspector",
1626
- "data-position": position,
2261
+ type: "button",
1627
2262
  className,
1628
- "aria-label": activeToolStyle.label ?? "Estilo da ferramenta",
1629
- style: shell,
1630
- children: [
1631
- /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
1632
- "Cor",
1633
- /* @__PURE__ */ jsx(
1634
- "input",
2263
+ style: chevronBtnStyle,
2264
+ "aria-label": expanded ? "Hide minimap" : "Show minimap",
2265
+ title: expanded ? "Hide minimap" : "Show minimap",
2266
+ "aria-expanded": expanded,
2267
+ onPointerDown: preventMouseDefault,
2268
+ onClick: toggleExpanded,
2269
+ children: /* @__PURE__ */ jsx(
2270
+ "svg",
2271
+ {
2272
+ width: "12",
2273
+ height: "12",
2274
+ viewBox: "0 0 12 12",
2275
+ style: {
2276
+ transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
2277
+ transition: "transform 0.15s ease"
2278
+ },
2279
+ "aria-hidden": true,
2280
+ children: /* @__PURE__ */ jsx(
2281
+ "path",
1635
2282
  {
1636
- type: "color",
1637
- value: hex2,
1638
- onChange: (e) => onChange({
1639
- stroke: e.target.value,
1640
- strokeWidth: strokeWidth2,
1641
- ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
1642
- }),
1643
- style: {
1644
- width: "100%",
1645
- height: 32,
1646
- padding: 0,
1647
- border: "none",
1648
- cursor: "pointer",
1649
- background: "transparent"
1650
- }
2283
+ d: "M2 4 L6 8 L10 4",
2284
+ fill: "none",
2285
+ stroke: "currentColor",
2286
+ strokeWidth: "1.5",
2287
+ strokeLinecap: "round",
2288
+ strokeLinejoin: "round"
1651
2289
  }
1652
2290
  )
1653
- ] }),
1654
- /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
1655
- "Grossura",
2291
+ }
2292
+ )
2293
+ }
2294
+ ),
2295
+ expanded && slotEl ? createPortal(
2296
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Minimap", style: { pointerEvents: "auto" }, children: /* @__PURE__ */ jsx(
2297
+ "svg",
2298
+ {
2299
+ ref: svgRef,
2300
+ style: {
2301
+ width: MINIMAP_W,
2302
+ height: MINIMAP_H,
2303
+ borderRadius: BORDER_RADIUS,
2304
+ border: `1px solid ${BORDER_COLOR}`,
2305
+ background: BG,
2306
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
2307
+ cursor: isEmpty ? "default" : dragging ? "grabbing" : "grab",
2308
+ display: "block",
2309
+ pointerEvents: "auto"
2310
+ },
2311
+ onPointerDown: handlePointerDown,
2312
+ onPointerMove: handlePointerMove,
2313
+ onPointerUp: handlePointerUp,
2314
+ onPointerCancel: handlePointerUp,
2315
+ children: isEmpty ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
2316
+ items.map((it) => {
2317
+ const b = normalizeRect(boundsAabbForRotatedItem(it));
2318
+ return /* @__PURE__ */ jsx(
2319
+ "rect",
2320
+ {
2321
+ x: toMinimapX(b.x),
2322
+ y: toMinimapY(b.y),
2323
+ width: toMinimapW(b.width),
2324
+ height: toMinimapH(b.height),
2325
+ fill: ITEM_FILL,
2326
+ stroke: ITEM_STROKE,
2327
+ strokeWidth: 0.5,
2328
+ rx: 1
2329
+ },
2330
+ it.id
2331
+ );
2332
+ }),
1656
2333
  /* @__PURE__ */ jsx(
1657
- "input",
2334
+ "rect",
1658
2335
  {
1659
- type: "range",
1660
- min: 1,
1661
- max: 48,
1662
- value: strokeWidth2,
1663
- onChange: (e) => onChange({
1664
- stroke: hex2,
1665
- strokeWidth: Number(e.target.value),
1666
- ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
1667
- })
2336
+ x: vpMinimap.x,
2337
+ y: vpMinimap.y,
2338
+ width: vpMinimap.width,
2339
+ height: vpMinimap.height,
2340
+ fill: VIEWPORT_FILL,
2341
+ stroke: VIEWPORT_STROKE,
2342
+ strokeWidth: 1.5,
2343
+ rx: 2
1668
2344
  }
1669
2345
  )
1670
- ] }),
1671
- showMarkerOpacity2 && /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
1672
- "Opacidade (marcador)",
1673
- /* @__PURE__ */ jsx(
1674
- "input",
1675
- {
1676
- type: "range",
1677
- min: 10,
1678
- max: 100,
1679
- value: opacityPct2,
1680
- onChange: (e) => {
1681
- const v = Number(e.target.value) / 100;
1682
- onChange({
1683
- stroke: hex2,
1684
- strokeWidth: strokeWidth2,
1685
- strokeOpacity: v
1686
- });
1687
- }
1688
- }
1689
- ),
1690
- /* @__PURE__ */ jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1691
- opacityPct2,
1692
- "%"
1693
- ] })
1694
2346
  ] })
1695
- ]
1696
- }
1697
- );
2347
+ }
2348
+ ) }),
2349
+ slotEl
2350
+ ) : null
2351
+ ] });
2352
+ }
2353
+ function computeWorldBounds(items) {
2354
+ if (items.length === 0) {
2355
+ return { x: 0, y: 0, width: 0, height: 0 };
1698
2356
  }
1699
- const stylable = items.filter(
1700
- (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
1701
- );
1702
- if (stylable.length === 0) return null;
1703
- const first = stylable[0];
1704
- if (!first) return null;
1705
- const allSameStroke = stylable.every(
1706
- (it) => (it.stroke ?? "#2563eb") === (first.stroke ?? "#2563eb")
1707
- );
1708
- const allSameWidth = stylable.every(
1709
- (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
1710
- );
1711
- const stroke = first.stroke ?? "#2563eb";
1712
- const strokeWidth = first.strokeWidth ?? 2;
1713
- const hex = normalizeHex(stroke);
1714
- const markers = stylable.filter((it) => it.toolKind === "marker");
1715
- const showMarkerOpacity = markers.length > 0;
1716
- const firstMarker = markers[0];
1717
- const allSameMarkerOpacity = markers.length > 0 && markers.every(
1718
- (it) => (it.strokeOpacity ?? 1) === (firstMarker?.strokeOpacity ?? 1)
2357
+ let minX = Infinity;
2358
+ let minY = Infinity;
2359
+ let maxX = -Infinity;
2360
+ let maxY = -Infinity;
2361
+ for (const it of items) {
2362
+ const b = boundsAabbForRotatedItem(it);
2363
+ minX = Math.min(minX, b.x);
2364
+ minY = Math.min(minY, b.y);
2365
+ maxX = Math.max(maxX, b.x + b.width);
2366
+ maxY = Math.max(maxY, b.y + b.height);
2367
+ }
2368
+ const pad = Math.max((maxX - minX) * 0.1, (maxY - minY) * 0.1, 40);
2369
+ return {
2370
+ x: minX - pad,
2371
+ y: minY - pad,
2372
+ width: maxX - minX + pad * 2,
2373
+ height: maxY - minY + pad * 2
2374
+ };
2375
+ }
2376
+ function NavMenuUndoRedo({
2377
+ className,
2378
+ style,
2379
+ onUndo: onUndoProp,
2380
+ onRedo: onRedoProp,
2381
+ canUndo: canUndoProp,
2382
+ canRedo: canRedoProp
2383
+ } = {}) {
2384
+ const ctx = useCanvuChromeContext();
2385
+ const onUndo = onUndoProp ?? ctx?.onUndo ?? noop;
2386
+ const onRedo = onRedoProp ?? ctx?.onRedo ?? noop;
2387
+ const canUndo = canUndoProp ?? ctx?.canUndo ?? false;
2388
+ const canRedo = canRedoProp ?? ctx?.canRedo ?? false;
2389
+ return /* @__PURE__ */ jsxs(
2390
+ "span",
2391
+ {
2392
+ className,
2393
+ style: { display: "inline-flex", alignItems: "center", ...style },
2394
+ children: [
2395
+ /* @__PURE__ */ jsx(
2396
+ "button",
2397
+ {
2398
+ type: "button",
2399
+ style: undoBtnStyle,
2400
+ "aria-label": "Undo",
2401
+ title: "Undo",
2402
+ disabled: !canUndo,
2403
+ onPointerDown: preventMouseDefault,
2404
+ onClick: onUndo,
2405
+ children: /* @__PURE__ */ jsx(Undo2, { size: 16 })
2406
+ }
2407
+ ),
2408
+ /* @__PURE__ */ jsx(
2409
+ "button",
2410
+ {
2411
+ type: "button",
2412
+ style: undoBtnStyle,
2413
+ "aria-label": "Redo",
2414
+ title: "Redo",
2415
+ disabled: !canRedo,
2416
+ onPointerDown: preventMouseDefault,
2417
+ onClick: onRedo,
2418
+ children: /* @__PURE__ */ jsx(Redo2, { size: 16 })
2419
+ }
2420
+ )
2421
+ ]
2422
+ }
1719
2423
  );
1720
- const opacityPct = firstMarker ? Math.round((firstMarker.strokeOpacity ?? 1) * 100) : 100;
2424
+ }
2425
+ function NavMenuZoomControls({
2426
+ className,
2427
+ style,
2428
+ zoomPercent: zoomPercentProp,
2429
+ onZoomIn: onZoomInProp,
2430
+ onZoomOut: onZoomOutProp
2431
+ } = {}) {
2432
+ const ctx = useCanvuChromeContext();
2433
+ const zoomPercent = zoomPercentProp ?? ctx?.zoomPercent ?? 100;
2434
+ const onZoomIn = onZoomInProp ?? ctx?.onZoomIn ?? noop;
2435
+ const onZoomOut = onZoomOutProp ?? ctx?.onZoomOut ?? noop;
1721
2436
  return /* @__PURE__ */ jsxs(
1722
- "section",
2437
+ "span",
1723
2438
  {
1724
- "data-slot": "vector-selection-inspector",
1725
- "data-position": position,
1726
2439
  className,
1727
- "aria-label": "Estilo da sele\xE7\xE3o",
1728
- style: shell,
2440
+ style: { display: "inline-flex", alignItems: "center", ...style },
1729
2441
  children: [
1730
- stylable.length > 1 && /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
1731
- stylable.length,
1732
- " objetos selecionados"
2442
+ /* @__PURE__ */ jsx(
2443
+ "button",
2444
+ {
2445
+ type: "button",
2446
+ style: btnStyle,
2447
+ "aria-label": "Zoom out",
2448
+ title: "Zoom out",
2449
+ onPointerDown: preventMouseDefault,
2450
+ onClick: onZoomOut,
2451
+ children: "\u2212"
2452
+ }
2453
+ ),
2454
+ /* @__PURE__ */ jsxs("span", { style: labelStyle, "aria-hidden": true, children: [
2455
+ zoomPercent,
2456
+ "%"
1733
2457
  ] }),
1734
- /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
1735
- "Cor",
1736
- !allSameStroke && /* @__PURE__ */ jsxs("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: [
1737
- " ",
1738
- "(valores misturados \u2014 novo valor aplica a todos)"
1739
- ] }),
2458
+ /* @__PURE__ */ jsx(
2459
+ "button",
2460
+ {
2461
+ type: "button",
2462
+ style: btnStyle,
2463
+ "aria-label": "Zoom in",
2464
+ title: "Zoom in",
2465
+ onPointerDown: preventMouseDefault,
2466
+ onClick: onZoomIn,
2467
+ children: "+"
2468
+ }
2469
+ )
2470
+ ]
2471
+ }
2472
+ );
2473
+ }
2474
+ function NavMenuComponent({
2475
+ camera: cameraProp,
2476
+ viewportWidth: viewportWidthProp,
2477
+ viewportHeight: viewportHeightProp,
2478
+ items: itemsProp,
2479
+ zoomPercent: zoomPercentProp,
2480
+ onZoomIn: onZoomInProp,
2481
+ onZoomOut: onZoomOutProp,
2482
+ onUndo: onUndoProp,
2483
+ onRedo: onRedoProp,
2484
+ canUndo: canUndoProp,
2485
+ canRedo: canRedoProp,
2486
+ onRequestRender: onRequestRenderProp,
2487
+ position = "bottom-left",
2488
+ inset = 12,
2489
+ zIndex = 23,
2490
+ className,
2491
+ style,
2492
+ children
2493
+ }) {
2494
+ const [slotEl, setSlotEl] = useState(null);
2495
+ const anchorStyle = getBoardPositionStyle(position, inset, zIndex);
2496
+ return /* @__PURE__ */ jsx(
2497
+ "fieldset",
2498
+ {
2499
+ "data-slot": "canvu-nav-menu",
2500
+ "data-position": position,
2501
+ className,
2502
+ style: {
2503
+ ...anchorStyle,
2504
+ ...panelLayoutStyle,
2505
+ border: "none",
2506
+ margin: 0,
2507
+ padding: 0,
2508
+ minWidth: 0,
2509
+ ...style
2510
+ },
2511
+ "aria-label": "Zoom and minimap controls",
2512
+ children: /* @__PURE__ */ jsxs(NavMenuMinimapSlotContext.Provider, { value: slotEl, children: [
2513
+ /* @__PURE__ */ jsx("div", { style: innerStyle, children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1740
2514
  /* @__PURE__ */ jsx(
1741
- "input",
2515
+ NavMenuZoomControls,
1742
2516
  {
1743
- type: "color",
1744
- value: hex,
1745
- onChange: (e) => onChange({
1746
- stroke: e.target.value,
1747
- strokeWidth
1748
- }),
1749
- style: {
1750
- width: "100%",
1751
- height: 32,
1752
- padding: 0,
1753
- border: "none",
1754
- cursor: "pointer",
1755
- background: "transparent"
1756
- }
2517
+ zoomPercent: zoomPercentProp,
2518
+ onZoomIn: onZoomInProp,
2519
+ onZoomOut: onZoomOutProp
1757
2520
  }
1758
- )
1759
- ] }),
1760
- /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
1761
- "Grossura",
1762
- !allSameWidth && /* @__PURE__ */ jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
2521
+ ),
1763
2522
  /* @__PURE__ */ jsx(
1764
- "input",
2523
+ NavMenuUndoRedo,
1765
2524
  {
1766
- type: "range",
1767
- min: 1,
1768
- max: 48,
1769
- value: strokeWidth,
1770
- onChange: (e) => onChange({
1771
- stroke: hex,
1772
- strokeWidth: Number(e.target.value)
1773
- })
2525
+ onUndo: onUndoProp,
2526
+ onRedo: onRedoProp,
2527
+ canUndo: canUndoProp,
2528
+ canRedo: canRedoProp
1774
2529
  }
1775
- )
1776
- ] }),
1777
- showMarkerOpacity && firstMarker && /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
1778
- "Opacidade (marcador)",
1779
- !allSameMarkerOpacity && markers.length > 1 && /* @__PURE__ */ jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
2530
+ ),
1780
2531
  /* @__PURE__ */ jsx(
1781
- "input",
2532
+ NavMenuMinimap,
1782
2533
  {
1783
- type: "range",
1784
- min: 10,
1785
- max: 100,
1786
- value: opacityPct,
1787
- onChange: (e) => {
1788
- const v = Number(e.target.value) / 100;
1789
- onChange({
1790
- stroke: hex,
1791
- strokeWidth,
1792
- strokeOpacity: v
1793
- });
1794
- }
2534
+ camera: cameraProp,
2535
+ viewportWidth: viewportWidthProp,
2536
+ viewportHeight: viewportHeightProp,
2537
+ items: itemsProp,
2538
+ onRequestRender: onRequestRenderProp
1795
2539
  }
1796
- ),
1797
- /* @__PURE__ */ jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1798
- opacityPct,
1799
- "%"
1800
- ] })
1801
- ] })
1802
- ]
2540
+ )
2541
+ ] }) }),
2542
+ /* @__PURE__ */ jsx("div", { ref: setSlotEl, style: minimapSlotStyle })
2543
+ ] })
1803
2544
  }
1804
2545
  );
1805
2546
  }
1806
- function getBoardPositionStyle(position, inset = 12, zIndex = 40) {
1807
- const base2 = { position: "absolute", zIndex };
1808
- switch (position) {
1809
- case "fill":
1810
- return { ...base2, inset };
1811
- case "top-left":
1812
- case "left-top":
1813
- return { ...base2, top: inset, left: inset };
1814
- case "top-center":
1815
- return {
1816
- ...base2,
1817
- top: inset,
1818
- left: "50%",
1819
- transform: "translateX(-50%)"
1820
- };
1821
- case "top-right":
1822
- case "right-top":
1823
- return { ...base2, top: inset, right: inset };
1824
- case "bottom-left":
1825
- case "left-bottom":
1826
- return { ...base2, bottom: inset, left: inset };
1827
- case "bottom-center":
1828
- return {
1829
- ...base2,
1830
- bottom: inset,
1831
- left: "50%",
1832
- transform: "translateX(-50%)"
1833
- };
1834
- case "bottom-right":
1835
- case "right-bottom":
1836
- return { ...base2, bottom: inset, right: inset };
1837
- case "left-center":
1838
- return {
1839
- ...base2,
1840
- left: inset,
1841
- top: "50%",
1842
- transform: "translateY(-50%)"
1843
- };
1844
- case "right-center":
1845
- return {
1846
- ...base2,
1847
- right: inset,
1848
- top: "50%",
1849
- transform: "translateY(-50%)"
1850
- };
1851
- case "center":
1852
- return {
1853
- ...base2,
1854
- top: "50%",
1855
- left: "50%",
1856
- transform: "translate(-50%, -50%)"
1857
- };
1858
- default:
1859
- return base2;
1860
- }
1861
- }
1862
- var rootStyle = {
1863
- display: "flex",
1864
- flexDirection: "column",
1865
- height: "100%",
1866
- minHeight: 0,
1867
- width: "100%"
1868
- };
1869
- var headerStyle = {
1870
- flexShrink: 0,
1871
- display: "flex",
1872
- flexWrap: "wrap",
1873
- alignItems: "center",
1874
- gap: "0.75rem",
1875
- padding: "0.75rem 1rem",
1876
- borderBottom: "1px solid #e4e4e7",
1877
- background: "#fff",
1878
- fontSize: "0.875rem"
1879
- };
1880
- var bodyStyle = {
1881
- flex: 1,
1882
- minHeight: 0,
1883
- display: "flex",
1884
- flexDirection: "column"
1885
- };
1886
- var mainStyle = {
1887
- flex: 1,
1888
- minHeight: 0,
1889
- position: "relative",
1890
- display: "flex",
1891
- flexDirection: "column"
1892
- };
1893
- var viewportSurfaceStyle = {
1894
- flex: 1,
1895
- minHeight: 0,
1896
- position: "relative",
1897
- width: "100%",
1898
- alignSelf: "stretch",
1899
- background: "#fff",
1900
- touchAction: "none"
1901
- };
1902
- function mergeStyle(base2, style) {
1903
- return style ? { ...base2, ...style } : base2;
2547
+ NavMenuComponent.displayName = "NavMenu";
2548
+ var NavMenu = Object.assign(NavMenuComponent, {
2549
+ ZoomControls: NavMenuZoomControls,
2550
+ UndoRedo: NavMenuUndoRedo,
2551
+ Minimap: NavMenuMinimap
2552
+ });
2553
+
2554
+ // src/react/persistence/indexed-db-adapter.ts
2555
+ init_shape_builders();
2556
+ var DOCUMENT_STORE = "document";
2557
+ var DOCUMENT_KEY = "main";
2558
+ function openDocumentDb(dbName) {
2559
+ return new Promise((resolve, reject) => {
2560
+ const req = indexedDB.open(dbName, 1);
2561
+ req.onupgradeneeded = () => {
2562
+ const db = req.result;
2563
+ if (!db.objectStoreNames.contains(DOCUMENT_STORE)) {
2564
+ db.createObjectStore(DOCUMENT_STORE);
2565
+ }
2566
+ };
2567
+ req.onsuccess = () => resolve(req.result);
2568
+ req.onerror = () => reject(req.error);
2569
+ });
1904
2570
  }
1905
- function vectorCanvasSpaceStyle(position, inset, zIndex) {
2571
+ function createIndexedDbPersistenceAdapter(options = {}) {
2572
+ const dbName = options.dbName ?? "canvu-document";
2573
+ const imageStore = new IndexedDbImageStore();
2574
+ let documentDbPromise = null;
2575
+ function getDocDb() {
2576
+ if (!documentDbPromise) {
2577
+ documentDbPromise = openDocumentDb(dbName);
2578
+ }
2579
+ return documentDbPromise;
2580
+ }
2581
+ async function getFromDocDb(key) {
2582
+ const db = await getDocDb();
2583
+ return new Promise((resolve, reject) => {
2584
+ const tx = db.transaction(DOCUMENT_STORE, "readonly");
2585
+ const req = tx.objectStore(DOCUMENT_STORE).get(key);
2586
+ req.onsuccess = () => resolve(req.result);
2587
+ req.onerror = () => reject(req.error);
2588
+ });
2589
+ }
2590
+ async function putInDocDb(key, value) {
2591
+ const db = await getDocDb();
2592
+ return new Promise((resolve, reject) => {
2593
+ const tx = db.transaction(DOCUMENT_STORE, "readwrite");
2594
+ const req = tx.objectStore(DOCUMENT_STORE).put(value, key);
2595
+ req.onsuccess = () => resolve();
2596
+ req.onerror = () => reject(req.error);
2597
+ });
2598
+ }
2599
+ const liveHrefs = /* @__PURE__ */ new Set();
2600
+ function stripBlobsForPersist(item) {
2601
+ const out = { ...item };
2602
+ delete out.imageRasterHref;
2603
+ delete out.imageThumbnailHref;
2604
+ if (item.toolKind === "image") {
2605
+ out.childrenSvg = "";
2606
+ }
2607
+ return out;
2608
+ }
2609
+ function collectLiveHrefs(items) {
2610
+ const set = /* @__PURE__ */ new Set();
2611
+ for (const item of items) {
2612
+ if (item.imageRasterHref?.startsWith("blob:")) {
2613
+ set.add(item.imageRasterHref);
2614
+ }
2615
+ if (item.imageThumbnailHref?.startsWith("blob:")) {
2616
+ set.add(item.imageThumbnailHref);
2617
+ }
2618
+ }
2619
+ return set;
2620
+ }
1906
2621
  return {
1907
- ...getBoardPositionStyle(position, inset, zIndex),
1908
- pointerEvents: "none"
2622
+ async load() {
2623
+ const raw = await getFromDocDb(DOCUMENT_KEY);
2624
+ if (!raw?.items) return null;
2625
+ const resolved = [];
2626
+ for (const item of raw.items) {
2627
+ if (item.toolKind === "image" && item.imageBlobId) {
2628
+ const itemWithBlob = { ...item };
2629
+ if (item.imageThumbnailBlobId) {
2630
+ const thumbUrl = await createThumbnailBlobUrlFromStore(
2631
+ imageStore,
2632
+ item.imageThumbnailBlobId
2633
+ );
2634
+ if (thumbUrl) {
2635
+ itemWithBlob.imageThumbnailHref = thumbUrl;
2636
+ }
2637
+ }
2638
+ const fullUrl = await createBlobUrlFromStore(imageStore, item.imageBlobId);
2639
+ if (fullUrl) {
2640
+ itemWithBlob.imageRasterHref = fullUrl;
2641
+ }
2642
+ const rebuilt = rebuildItemSvg(itemWithBlob);
2643
+ resolved.push(rebuilt);
2644
+ } else {
2645
+ resolved.push(item);
2646
+ }
2647
+ }
2648
+ return { items: resolved, version: raw.version ?? 1 };
2649
+ },
2650
+ async save(snapshot) {
2651
+ const nextLive = collectLiveHrefs(snapshot.items);
2652
+ for (const href of liveHrefs) {
2653
+ if (!nextLive.has(href)) {
2654
+ URL.revokeObjectURL(href);
2655
+ }
2656
+ }
2657
+ liveHrefs.clear();
2658
+ for (const href of nextLive) liveHrefs.add(href);
2659
+ const cleanItems = snapshot.items.map(stripBlobsForPersist);
2660
+ await putInDocDb(DOCUMENT_KEY, {
2661
+ items: cleanItems,
2662
+ version: snapshot.version ?? 1
2663
+ });
2664
+ }
1909
2665
  };
1910
2666
  }
1911
- function VectorCanvasRoot({
1912
- children,
1913
- className,
1914
- style
1915
- }) {
1916
- return /* @__PURE__ */ jsx(
1917
- "div",
1918
- {
1919
- "data-slot": "vector-canvas-root",
1920
- className,
1921
- style: mergeStyle(rootStyle, style),
1922
- children
2667
+
2668
+ // src/react/persistence/local-storage-adapter.ts
2669
+ var DEFAULT_VECTOR_CANVAS_STORAGE_KEY = "trazo.vector-canvas.v1";
2670
+ var SNAPSHOT_VERSION = 1;
2671
+ function createLocalStoragePersistenceAdapter(options = {}) {
2672
+ const key = options.key ?? DEFAULT_VECTOR_CANVAS_STORAGE_KEY;
2673
+ const storage = options.storage ?? (typeof globalThis !== "undefined" && "localStorage" in globalThis && globalThis.localStorage ? globalThis.localStorage : null);
2674
+ return {
2675
+ load() {
2676
+ if (!storage) return Promise.resolve(null);
2677
+ try {
2678
+ const raw = storage.getItem(key);
2679
+ if (!raw) return Promise.resolve(null);
2680
+ const parsed = JSON.parse(raw);
2681
+ if (!parsed || typeof parsed !== "object" || !("items" in parsed) || !Array.isArray(parsed.items)) {
2682
+ return Promise.resolve(null);
2683
+ }
2684
+ return Promise.resolve(parsed);
2685
+ } catch {
2686
+ return Promise.resolve(null);
2687
+ }
2688
+ },
2689
+ save(snapshot) {
2690
+ if (!storage) return Promise.resolve();
2691
+ try {
2692
+ storage.setItem(
2693
+ key,
2694
+ JSON.stringify({
2695
+ ...snapshot,
2696
+ version: snapshot.version ?? SNAPSHOT_VERSION
2697
+ })
2698
+ );
2699
+ } catch {
2700
+ }
2701
+ return Promise.resolve();
1923
2702
  }
1924
- );
2703
+ };
1925
2704
  }
1926
- function VectorCanvasHeader({
1927
- children,
1928
- className,
1929
- style
1930
- }) {
1931
- return /* @__PURE__ */ jsx(
1932
- "header",
1933
- {
1934
- "data-slot": "vector-canvas-header",
1935
- className,
1936
- style: mergeStyle(headerStyle, style),
1937
- children
1938
- }
1939
- );
2705
+ function createNoopPersistenceAdapter() {
2706
+ return {
2707
+ load: () => Promise.resolve(null),
2708
+ save: () => Promise.resolve()
2709
+ };
1940
2710
  }
1941
- function VectorCanvasBody({
1942
- children,
1943
- className,
1944
- style
1945
- }) {
1946
- return /* @__PURE__ */ jsx(
1947
- "div",
1948
- {
1949
- "data-slot": "vector-canvas-body",
1950
- className,
1951
- style: mergeStyle(bodyStyle, style),
1952
- children
1953
- }
1954
- );
1955
- }
1956
- function VectorCanvasMain({
1957
- children,
1958
- className,
1959
- style
1960
- }) {
1961
- return /* @__PURE__ */ jsx(
1962
- "div",
1963
- {
1964
- "data-slot": "vector-canvas-main",
1965
- className,
1966
- style: mergeStyle(mainStyle, style),
1967
- children
1968
- }
2711
+ function useVectorCanvasDocument(options = {}) {
2712
+ const {
2713
+ persistenceKey = DEFAULT_VECTOR_CANVAS_STORAGE_KEY,
2714
+ persistence,
2715
+ debounceMs = 400,
2716
+ remote
2717
+ } = options;
2718
+ const adapter = useMemo(() => {
2719
+ if (persistence === false) return createNoopPersistenceAdapter();
2720
+ if (persistence) return persistence;
2721
+ return createLocalStoragePersistenceAdapter({ key: persistenceKey });
2722
+ }, [persistence, persistenceKey]);
2723
+ const [items, setItems] = useState([]);
2724
+ const [isHydrated, setIsHydrated] = useState(false);
2725
+ const saveTimerRef = useRef(null);
2726
+ const adapterRef = useRef(adapter);
2727
+ adapterRef.current = adapter;
2728
+ useEffect(() => {
2729
+ let cancelled = false;
2730
+ adapter.load().then((snap) => {
2731
+ if (cancelled) return;
2732
+ if (snap?.items && snap.items.length > 0) {
2733
+ setItems(snap.items);
2734
+ }
2735
+ setIsHydrated(true);
2736
+ });
2737
+ return () => {
2738
+ cancelled = true;
2739
+ };
2740
+ }, [adapter]);
2741
+ const persist = useCallback((next) => {
2742
+ void adapterRef.current.save({ items: next, version: 1 });
2743
+ }, []);
2744
+ const onItemsChange = useCallback(
2745
+ (next) => {
2746
+ setItems(next);
2747
+ remote?.send?.(next);
2748
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
2749
+ saveTimerRef.current = setTimeout(() => {
2750
+ persist(next);
2751
+ saveTimerRef.current = null;
2752
+ }, debounceMs);
2753
+ },
2754
+ [debounceMs, persist, remote]
1969
2755
  );
1970
- }
1971
- function VectorCanvasViewportSurface({
1972
- children,
1973
- className,
1974
- style
1975
- }) {
1976
- return /* @__PURE__ */ jsx(
1977
- "div",
1978
- {
1979
- "data-slot": "vector-canvas-viewport-surface",
1980
- className,
1981
- style: mergeStyle(viewportSurfaceStyle, style),
1982
- children
1983
- }
2756
+ useEffect(() => {
2757
+ if (!remote || !isHydrated) return;
2758
+ return remote.subscribe((serverItems) => {
2759
+ setItems(serverItems);
2760
+ persist(serverItems);
2761
+ });
2762
+ }, [remote, isHydrated, persist]);
2763
+ useEffect(
2764
+ () => () => {
2765
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
2766
+ },
2767
+ []
1984
2768
  );
2769
+ const clearPersistedDocument = useCallback(() => {
2770
+ setItems([]);
2771
+ void adapterRef.current.save({ items: [], version: 1 });
2772
+ }, []);
2773
+ return {
2774
+ items,
2775
+ onItemsChange,
2776
+ setItems: onItemsChange,
2777
+ isHydrated,
2778
+ clearPersistedDocument
2779
+ };
1985
2780
  }
1986
- function VectorCanvasToolbar({
1987
- children,
1988
- className,
1989
- style,
1990
- position = "bottom-center",
1991
- inset = 12,
1992
- zIndex = 30
1993
- }) {
1994
- const base2 = {
1995
- ...getBoardPositionStyle(position, inset, zIndex),
1996
- display: "flex",
1997
- justifyContent: "center",
1998
- alignItems: "center",
1999
- maxWidth: "calc(100% - 24px)",
2000
- pointerEvents: "none"
2781
+ var CanvuPluginContext = createContext(
2782
+ null
2783
+ );
2784
+ function createCanvuPlugin(plugin) {
2785
+ return plugin;
2786
+ }
2787
+ function useCanvuPluginContext() {
2788
+ const ctx = useContext(CanvuPluginContext);
2789
+ if (!ctx) {
2790
+ throw new Error(
2791
+ "useCanvuPluginContext must be used inside a VectorViewport plugin runtime."
2792
+ );
2793
+ }
2794
+ return ctx;
2795
+ }
2796
+ function useCanvuViewportContext() {
2797
+ const { viewportRef, viewport } = useCanvuPluginContext();
2798
+ return { viewportRef, viewport };
2799
+ }
2800
+ function useCanvuDocumentContext() {
2801
+ const { viewport } = useCanvuPluginContext();
2802
+ return {
2803
+ items: viewport.items,
2804
+ onItemsChange: viewport.onItemsChange
2001
2805
  };
2002
- return /* @__PURE__ */ jsx(
2003
- "div",
2004
- {
2005
- "data-slot": "vector-canvas-toolbar",
2006
- "data-position": position,
2007
- className,
2008
- style: mergeStyle(base2, style),
2009
- children: /* @__PURE__ */ jsx("div", { style: { pointerEvents: "auto" }, children })
2010
- }
2011
- );
2012
2806
  }
2013
- function VectorCanvasSpace({
2014
- children,
2015
- className,
2016
- style,
2017
- position = "top-right",
2018
- inset = 12,
2019
- zIndex = 40,
2020
- contentPointerEvents = "auto"
2807
+ function useCanvuResolvedTools() {
2808
+ return useCanvuPluginContext().resolvedTools;
2809
+ }
2810
+ function useCanvuPluginContribution(pluginId, contribution) {
2811
+ const { registerContribution, unregisterContribution } = useCanvuPluginContext();
2812
+ useLayoutEffect(() => {
2813
+ registerContribution(pluginId, contribution);
2814
+ return () => unregisterContribution(pluginId);
2815
+ }, [contribution, pluginId, registerContribution, unregisterContribution]);
2816
+ }
2817
+ function ToolPluginComponent({
2818
+ pluginId,
2819
+ tool,
2820
+ toolTransform,
2821
+ createItem
2021
2822
  }) {
2022
- return /* @__PURE__ */ jsx(
2023
- "div",
2024
- {
2025
- "data-slot": `vector-canvas-space-${position}`,
2026
- className,
2027
- style: mergeStyle(vectorCanvasSpaceStyle(position, inset, zIndex), style),
2028
- children: /* @__PURE__ */ jsx("div", { style: { pointerEvents: contentPointerEvents }, children })
2029
- }
2823
+ const contribution = useMemo(
2824
+ () => ({
2825
+ tools: [tool],
2826
+ toolTransform,
2827
+ customPlacements: createItem ? [
2828
+ {
2829
+ toolId: tool.id,
2830
+ createItem
2831
+ }
2832
+ ] : void 0
2833
+ }),
2834
+ [createItem, tool, toolTransform]
2030
2835
  );
2836
+ useCanvuPluginContribution(pluginId, contribution);
2837
+ return null;
2031
2838
  }
2032
- var VectorCanvas = {
2033
- Root: VectorCanvasRoot,
2034
- Header: VectorCanvasHeader,
2035
- Body: VectorCanvasBody,
2036
- Main: VectorCanvasMain,
2037
- ViewportSurface: VectorCanvasViewportSurface,
2038
- Toolbar: VectorCanvasToolbar,
2039
- Space: VectorCanvasSpace,
2040
- NavMenu,
2041
- SelectionInspector: VectorSelectionInspector
2042
- };
2043
- var MINIMAP_W = 180;
2044
- var MINIMAP_H = 120;
2045
- var PADDING = 12;
2046
- var BORDER_RADIUS = 8;
2047
- var BG = "#ffffff";
2048
- var BORDER_COLOR = "rgba(0,0,0,0.12)";
2049
- var ITEM_FILL = "rgba(0,0,0,0.08)";
2050
- var ITEM_STROKE = "rgba(0,0,0,0.15)";
2051
- var VIEWPORT_FILL = "rgba(59,130,246,0.08)";
2052
- var VIEWPORT_STROKE = "#3b82f6";
2053
- var btnStyle = {
2054
- pointerEvents: "auto",
2055
- width: 36,
2056
- height: 36,
2057
- display: "inline-flex",
2058
- alignItems: "center",
2059
- justifyContent: "center",
2060
- cursor: "pointer",
2061
- fontSize: 18,
2062
- lineHeight: 1,
2063
- color: "#18181b",
2064
- padding: 0,
2065
- border: "none",
2066
- outline: "none",
2067
- background: "none",
2068
- WebkitTapHighlightColor: "transparent"
2839
+ function createToolPlugin(options) {
2840
+ const { createItem, toolTransform, ...tool } = options;
2841
+ const pluginId = `canvu.plugin.tool:${tool.id}`;
2842
+ return createCanvuPlugin({
2843
+ id: pluginId,
2844
+ Component() {
2845
+ return /* @__PURE__ */ jsx(
2846
+ ToolPluginComponent,
2847
+ {
2848
+ pluginId,
2849
+ tool,
2850
+ toolTransform,
2851
+ createItem
2852
+ }
2853
+ );
2854
+ }
2855
+ });
2856
+ }
2857
+ var menuStyle = {
2858
+ position: "fixed",
2859
+ zIndex: 1e4,
2860
+ minWidth: 200,
2861
+ padding: 4,
2862
+ backgroundColor: "#ffffff",
2863
+ border: "1px solid #e2e8f0",
2864
+ borderRadius: 8,
2865
+ boxShadow: "0 10px 40px rgba(15, 23, 42, 0.12)",
2866
+ fontSize: 13,
2867
+ fontFamily: "system-ui, sans-serif",
2868
+ color: "#0f172a"
2069
2869
  };
2070
- var chevronBtnStyle = {
2071
- pointerEvents: "auto",
2072
- width: 24,
2073
- height: 36,
2074
- display: "inline-flex",
2075
- alignItems: "center",
2076
- justifyContent: "center",
2077
- cursor: "pointer",
2078
- fontSize: 12,
2079
- lineHeight: 1,
2080
- color: "#52525b",
2081
- padding: 0,
2870
+ var itemStyle = {
2871
+ display: "block",
2872
+ width: "100%",
2873
+ textAlign: "left",
2874
+ padding: "8px 12px",
2875
+ margin: 0,
2082
2876
  border: "none",
2083
- outline: "none",
2084
- background: "none",
2085
- WebkitTapHighlightColor: "transparent"
2086
- };
2087
- var undoBtnStyle = {
2088
- ...chevronBtnStyle,
2089
- width: 30,
2090
- opacity: 1,
2091
- color: "#18181b"
2092
- };
2093
- var labelStyle2 = {
2094
- fontSize: 10,
2095
- fontWeight: 600,
2096
- color: "#52525b",
2097
- textAlign: "center",
2098
- userSelect: "none",
2099
- padding: "2px 0"
2100
- };
2101
- var panelLayoutStyle = {
2102
- display: "flex",
2103
- flexDirection: "column",
2104
- alignItems: "flex-start",
2105
- gap: 4,
2106
- touchAction: "none",
2107
- pointerEvents: "none",
2108
- userSelect: "none"
2877
+ borderRadius: 4,
2878
+ background: "transparent",
2879
+ cursor: "pointer"
2109
2880
  };
2110
- var innerStyle = {
2111
- pointerEvents: "auto",
2112
- display: "flex",
2113
- flexDirection: "row",
2114
- alignItems: "center",
2115
- gap: 4,
2116
- background: BG,
2117
- borderRadius: BORDER_RADIUS,
2118
- border: `1px solid ${BORDER_COLOR}`,
2119
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
2120
- padding: 4
2881
+ var dividerStyle = {
2882
+ height: 1,
2883
+ margin: "4px 8px",
2884
+ background: "#e2e8f0"
2121
2885
  };
2122
- function NavMenu({
2123
- camera: cameraProp,
2124
- viewportWidth: viewportWidthProp,
2125
- viewportHeight: viewportHeightProp,
2126
- items: itemsProp,
2127
- zoomPercent: zoomPercentProp,
2128
- onZoomIn: onZoomInProp,
2129
- onZoomOut: onZoomOutProp,
2130
- onUndo: onUndoProp,
2131
- onRedo: onRedoProp,
2132
- canUndo: canUndoProp,
2133
- canRedo: canRedoProp,
2134
- onRequestRender: onRequestRenderProp,
2135
- position = "bottom-left",
2136
- inset = 12,
2137
- zIndex = 23,
2138
- className,
2139
- style
2886
+ function ShapeContextMenu({
2887
+ x,
2888
+ y,
2889
+ allSelectedLocked,
2890
+ onClose,
2891
+ onToggleLock,
2892
+ onCut,
2893
+ onCopy,
2894
+ onBringToFront,
2895
+ onBringForward,
2896
+ onSendBackward,
2897
+ onSendToBack,
2898
+ onDuplicate,
2899
+ onDelete
2140
2900
  }) {
2141
- const ctx = useCanvuChromeContext();
2142
- const camera = cameraProp ?? ctx?.camera ?? null;
2143
- const viewportWidth = viewportWidthProp ?? ctx?.viewportWidth ?? 0;
2144
- const viewportHeight = viewportHeightProp ?? ctx?.viewportHeight ?? 0;
2145
- const items = itemsProp ?? ctx?.items ?? [];
2146
- const zoomPercent = zoomPercentProp ?? ctx?.zoomPercent ?? 100;
2147
- const onZoomIn = onZoomInProp ?? ctx?.onZoomIn ?? noop;
2148
- const onZoomOut = onZoomOutProp ?? ctx?.onZoomOut ?? noop;
2149
- const onUndo = onUndoProp ?? ctx?.onUndo ?? noop;
2150
- const onRedo = onRedoProp ?? ctx?.onRedo ?? noop;
2151
- const canUndo = canUndoProp ?? ctx?.canUndo ?? false;
2152
- const canRedo = canRedoProp ?? ctx?.canRedo ?? false;
2153
- const onRequestRender = onRequestRenderProp ?? ctx?.onRequestRender ?? noop;
2154
- const [expanded, setExpanded] = useState(false);
2155
- const svgRef = useRef(null);
2156
- const [dragging, setDragging] = useState(false);
2157
- const [mouseDown, setMouseDown] = useState(false);
2158
- const worldBounds = computeWorldBounds(items);
2159
- const isEmpty = worldBounds.width <= 0 || worldBounds.height <= 0;
2160
- const scale = Math.min(
2161
- (MINIMAP_W - PADDING * 2) / Math.max(worldBounds.width, 1),
2162
- (MINIMAP_H - PADDING * 2) / Math.max(worldBounds.height, 1)
2163
- );
2164
- const originX = worldBounds.x;
2165
- const originY = worldBounds.y;
2166
- const toMinimapX = (wx) => (wx - originX) * scale + PADDING;
2167
- const toMinimapY = (wy) => (wy - originY) * scale + PADDING;
2168
- const toMinimapW = (ww) => ww * scale;
2169
- const toMinimapH = (wh) => wh * scale;
2170
- const viewportWorld = camera ? camera.getVisibleWorldRect(viewportWidth, viewportHeight) : { x: 0, y: 0, width: 0, height: 0 };
2171
- const vpMinimap = {
2172
- x: toMinimapX(viewportWorld.x),
2173
- y: toMinimapY(viewportWorld.y),
2174
- width: toMinimapW(viewportWorld.width),
2175
- height: toMinimapH(viewportWorld.height)
2901
+ const rootRef = useRef(null);
2902
+ useLayoutEffect(() => {
2903
+ const el = rootRef.current;
2904
+ if (!el) return;
2905
+ const pad = 8;
2906
+ const w = el.offsetWidth || 200;
2907
+ const h = el.offsetHeight || 160;
2908
+ let left = x;
2909
+ let top = y;
2910
+ if (left + w + pad > window.innerWidth) {
2911
+ left = Math.max(pad, window.innerWidth - w - pad);
2912
+ }
2913
+ if (top + h + pad > window.innerHeight) {
2914
+ top = Math.max(pad, window.innerHeight - h - pad);
2915
+ }
2916
+ el.style.left = `${left}px`;
2917
+ el.style.top = `${top}px`;
2918
+ }, [x, y]);
2919
+ useEffect(() => {
2920
+ const onKey = (e) => {
2921
+ if (e.key === "Escape") {
2922
+ onClose();
2923
+ }
2924
+ };
2925
+ const onPointerDown = (e) => {
2926
+ const t = e.target;
2927
+ if (t && rootRef.current?.contains(t)) {
2928
+ return;
2929
+ }
2930
+ onClose();
2931
+ };
2932
+ document.addEventListener("keydown", onKey);
2933
+ document.addEventListener("pointerdown", onPointerDown, true);
2934
+ return () => {
2935
+ document.removeEventListener("keydown", onKey);
2936
+ document.removeEventListener("pointerdown", onPointerDown, true);
2937
+ };
2938
+ }, [onClose]);
2939
+ const run = (fn) => () => {
2940
+ fn();
2941
+ onClose();
2176
2942
  };
2177
- const worldFromMinimap = useCallback(
2178
- (mx, my) => ({
2179
- worldX: (mx - PADDING) / scale + originX,
2180
- worldY: (my - PADDING) / scale + originY
2181
- }),
2182
- [scale, originX, originY]
2183
- );
2184
- const panTo = useCallback(
2185
- (mx, my) => {
2186
- if (!camera) return;
2187
- const { worldX, worldY } = worldFromMinimap(mx, my);
2188
- camera.x = viewportWidth / 2 - worldX * camera.zoom;
2189
- camera.y = viewportHeight / 2 - worldY * camera.zoom;
2190
- onRequestRender();
2191
- },
2192
- [worldFromMinimap, camera, viewportWidth, viewportHeight, onRequestRender]
2193
- );
2194
- const handlePointerDown = useCallback(
2195
- (e) => {
2196
- if (isEmpty) return;
2197
- setDragging(true);
2198
- setMouseDown(true);
2199
- const rect = svgRef.current?.getBoundingClientRect();
2200
- if (!rect) return;
2201
- panTo(e.clientX - rect.left, e.clientY - rect.top);
2202
- svgRef.current?.setPointerCapture(e.pointerId);
2203
- },
2204
- [isEmpty, panTo]
2205
- );
2206
- const handlePointerMove = useCallback(
2207
- (e) => {
2208
- if (!mouseDown || isEmpty) return;
2209
- const rect = svgRef.current?.getBoundingClientRect();
2210
- if (!rect) return;
2211
- panTo(e.clientX - rect.left, e.clientY - rect.top);
2212
- },
2213
- [mouseDown, isEmpty, panTo]
2214
- );
2215
- const handlePointerUp = useCallback(() => {
2216
- setDragging(false);
2217
- setMouseDown(false);
2218
- }, []);
2219
- const toggleExpanded = useCallback(() => {
2220
- setExpanded((v) => !v);
2221
- }, []);
2222
- const anchorStyle = getBoardPositionStyle(position, inset, zIndex);
2223
- return /* @__PURE__ */ jsxs(
2224
- "fieldset",
2943
+ const renderAction = (label, onClick, options) => /* @__PURE__ */ jsx(
2944
+ "button",
2225
2945
  {
2226
- "data-slot": "canvu-nav-menu",
2227
- "data-position": position,
2228
- className,
2946
+ type: "button",
2947
+ role: "menuitem",
2229
2948
  style: {
2230
- ...anchorStyle,
2231
- ...panelLayoutStyle,
2232
- border: "none",
2233
- margin: 0,
2234
- padding: 0,
2235
- minWidth: 0,
2236
- ...style
2949
+ ...itemStyle,
2950
+ ...options?.danger ? { color: "#b91c1c" } : {}
2237
2951
  },
2238
- "aria-label": "Zoom and minimap controls",
2952
+ onMouseEnter: (e) => {
2953
+ e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
2954
+ },
2955
+ onMouseLeave: (e) => {
2956
+ e.currentTarget.style.background = "transparent";
2957
+ },
2958
+ onClick: run(onClick),
2959
+ children: label
2960
+ }
2961
+ );
2962
+ const menu = /* @__PURE__ */ jsxs(
2963
+ "div",
2964
+ {
2965
+ ref: rootRef,
2966
+ "data-slot": "shape-context-menu",
2967
+ style: { ...menuStyle, left: x, top: y },
2968
+ role: "menu",
2239
2969
  children: [
2240
- /* @__PURE__ */ jsxs("div", { style: innerStyle, children: [
2241
- /* @__PURE__ */ jsx(
2242
- "button",
2243
- {
2244
- type: "button",
2245
- style: btnStyle,
2246
- "aria-label": "Zoom out",
2247
- title: "Zoom out",
2248
- onPointerDown: (e) => {
2249
- if (e.pointerType === "mouse") e.preventDefault();
2250
- },
2251
- onClick: onZoomOut,
2252
- children: "\u2212"
2253
- }
2254
- ),
2255
- /* @__PURE__ */ jsxs("span", { style: labelStyle2, "aria-hidden": true, children: [
2256
- zoomPercent,
2257
- "%"
2258
- ] }),
2259
- /* @__PURE__ */ jsx(
2260
- "button",
2261
- {
2262
- type: "button",
2263
- style: btnStyle,
2264
- "aria-label": "Zoom in",
2265
- title: "Zoom in",
2266
- onPointerDown: (e) => {
2267
- if (e.pointerType === "mouse") e.preventDefault();
2268
- },
2269
- onClick: onZoomIn,
2270
- children: "+"
2271
- }
2272
- ),
2273
- /* @__PURE__ */ jsx(
2274
- "button",
2275
- {
2276
- type: "button",
2277
- style: undoBtnStyle,
2278
- "aria-label": "Undo",
2279
- title: "Undo",
2280
- disabled: !canUndo,
2281
- onPointerDown: (e) => {
2282
- if (e.pointerType === "mouse") e.preventDefault();
2283
- },
2284
- onClick: onUndo,
2285
- children: /* @__PURE__ */ jsx(Undo2, { size: 16 })
2286
- }
2287
- ),
2288
- /* @__PURE__ */ jsx(
2289
- "button",
2290
- {
2291
- type: "button",
2292
- style: undoBtnStyle,
2293
- "aria-label": "Redo",
2294
- title: "Redo",
2295
- disabled: !canRedo,
2296
- onPointerDown: (e) => {
2297
- if (e.pointerType === "mouse") e.preventDefault();
2298
- },
2299
- onClick: onRedo,
2300
- children: /* @__PURE__ */ jsx(Redo2, { size: 16 })
2301
- }
2302
- ),
2303
- /* @__PURE__ */ jsx(
2304
- "button",
2305
- {
2306
- type: "button",
2307
- style: chevronBtnStyle,
2308
- "aria-label": expanded ? "Hide minimap" : "Show minimap",
2309
- title: expanded ? "Hide minimap" : "Show minimap",
2310
- onPointerDown: (e) => {
2311
- if (e.pointerType === "mouse") e.preventDefault();
2312
- },
2313
- onClick: toggleExpanded,
2314
- children: /* @__PURE__ */ jsx(
2315
- "svg",
2316
- {
2317
- width: "12",
2318
- height: "12",
2319
- viewBox: "0 0 12 12",
2320
- style: {
2321
- transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
2322
- transition: "transform 0.15s ease"
2323
- },
2324
- "aria-hidden": true,
2325
- children: /* @__PURE__ */ jsx(
2326
- "path",
2327
- {
2328
- d: "M2 4 L6 8 L10 4",
2329
- fill: "none",
2330
- stroke: "currentColor",
2331
- strokeWidth: "1.5",
2332
- strokeLinecap: "round",
2333
- strokeLinejoin: "round"
2334
- }
2335
- )
2336
- }
2337
- )
2338
- }
2339
- )
2340
- ] }),
2341
- expanded && /* @__PURE__ */ jsx("nav", { "aria-label": "Minimap", children: /* @__PURE__ */ jsx(
2342
- "svg",
2343
- {
2344
- ref: svgRef,
2345
- style: {
2346
- width: MINIMAP_W,
2347
- height: MINIMAP_H,
2348
- borderRadius: BORDER_RADIUS,
2349
- border: `1px solid ${BORDER_COLOR}`,
2350
- background: BG,
2351
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
2352
- cursor: isEmpty ? "default" : dragging ? "grabbing" : "grab",
2353
- display: "block",
2354
- pointerEvents: "auto"
2355
- },
2356
- onPointerDown: handlePointerDown,
2357
- onPointerMove: handlePointerMove,
2358
- onPointerUp: handlePointerUp,
2359
- onPointerCancel: handlePointerUp,
2360
- children: isEmpty ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
2361
- items.map((it) => {
2362
- const b = normalizeRect(boundsAabbForRotatedItem(it));
2363
- return /* @__PURE__ */ jsx(
2364
- "rect",
2365
- {
2366
- x: toMinimapX(b.x),
2367
- y: toMinimapY(b.y),
2368
- width: toMinimapW(b.width),
2369
- height: toMinimapH(b.height),
2370
- fill: ITEM_FILL,
2371
- stroke: ITEM_STROKE,
2372
- strokeWidth: 0.5,
2373
- rx: 1
2374
- },
2375
- it.id
2376
- );
2377
- }),
2378
- /* @__PURE__ */ jsx(
2379
- "rect",
2380
- {
2381
- x: vpMinimap.x,
2382
- y: vpMinimap.y,
2383
- width: vpMinimap.width,
2384
- height: vpMinimap.height,
2385
- fill: VIEWPORT_FILL,
2386
- stroke: VIEWPORT_STROKE,
2387
- strokeWidth: 1.5,
2388
- rx: 2
2389
- }
2390
- )
2391
- ] })
2392
- }
2393
- ) })
2970
+ renderAction("Recortar", onCut),
2971
+ renderAction("Copiar", onCopy),
2972
+ renderAction("Duplicar", onDuplicate),
2973
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle }),
2974
+ renderAction("Trazer para frente", onBringToFront),
2975
+ renderAction("Avancar uma camada", onBringForward),
2976
+ renderAction("Recuar uma camada", onSendBackward),
2977
+ renderAction("Enviar para tras", onSendToBack),
2978
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle }),
2979
+ renderAction(allSelectedLocked ? "Desbloquear" : "Bloquear", onToggleLock),
2980
+ renderAction("Apagar", onDelete, { danger: true })
2394
2981
  ]
2395
2982
  }
2396
2983
  );
2984
+ if (typeof document === "undefined") {
2985
+ return null;
2986
+ }
2987
+ return createPortal(menu, document.body);
2397
2988
  }
2398
- function noop() {
2989
+ var base = {
2990
+ width: 20,
2991
+ height: 20,
2992
+ viewBox: "0 0 24 24",
2993
+ fill: "none",
2994
+ stroke: "currentColor",
2995
+ strokeWidth: 2,
2996
+ strokeLinecap: "round",
2997
+ strokeLinejoin: "round"
2998
+ };
2999
+ function IconHand(props) {
3000
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3001
+ /* @__PURE__ */ jsx("path", { d: "M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0" }),
3002
+ /* @__PURE__ */ jsx("path", { d: "M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2" }),
3003
+ /* @__PURE__ */ jsx("path", { d: "M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8" }),
3004
+ /* @__PURE__ */ jsx("path", { d: "M18 8a2 2 0 1 1 4 0v5a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" })
3005
+ ] });
2399
3006
  }
2400
- function computeWorldBounds(items) {
2401
- if (items.length === 0) {
2402
- return { x: 0, y: 0, width: 0, height: 0 };
2403
- }
2404
- let minX = Infinity;
2405
- let minY = Infinity;
2406
- let maxX = -Infinity;
2407
- let maxY = -Infinity;
2408
- for (const it of items) {
2409
- const b = boundsAabbForRotatedItem(it);
2410
- minX = Math.min(minX, b.x);
2411
- minY = Math.min(minY, b.y);
2412
- maxX = Math.max(maxX, b.x + b.width);
2413
- maxY = Math.max(maxY, b.y + b.height);
2414
- }
2415
- const pad = Math.max((maxX - minX) * 0.1, (maxY - minY) * 0.1, 40);
2416
- return {
2417
- x: minX - pad,
2418
- y: minY - pad,
2419
- width: maxX - minX + pad * 2,
2420
- height: maxY - minY + pad * 2
2421
- };
3007
+ function IconSelect(props) {
3008
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3009
+ /* @__PURE__ */ jsx("path", { d: "M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" }),
3010
+ /* @__PURE__ */ jsx("path", { d: "M13 13l6 6" })
3011
+ ] });
2422
3012
  }
2423
-
2424
- // src/react/persistence/indexed-db-adapter.ts
2425
- init_shape_builders();
2426
- var DOCUMENT_STORE = "document";
2427
- var DOCUMENT_KEY = "main";
2428
- function openDocumentDb(dbName) {
2429
- return new Promise((resolve, reject) => {
2430
- const req = indexedDB.open(dbName, 1);
2431
- req.onupgradeneeded = () => {
2432
- const db = req.result;
2433
- if (!db.objectStoreNames.contains(DOCUMENT_STORE)) {
2434
- db.createObjectStore(DOCUMENT_STORE);
2435
- }
2436
- };
2437
- req.onsuccess = () => resolve(req.result);
2438
- req.onerror = () => reject(req.error);
2439
- });
3013
+ function IconRect(props) {
3014
+ return /* @__PURE__ */ jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2" }) });
2440
3015
  }
2441
- function createIndexedDbPersistenceAdapter(options = {}) {
2442
- const dbName = options.dbName ?? "canvu-document";
2443
- const imageStore = new IndexedDbImageStore();
2444
- let documentDbPromise = null;
2445
- function getDocDb() {
2446
- if (!documentDbPromise) {
2447
- documentDbPromise = openDocumentDb(dbName);
2448
- }
2449
- return documentDbPromise;
2450
- }
2451
- async function getFromDocDb(key) {
2452
- const db = await getDocDb();
2453
- return new Promise((resolve, reject) => {
2454
- const tx = db.transaction(DOCUMENT_STORE, "readonly");
2455
- const req = tx.objectStore(DOCUMENT_STORE).get(key);
2456
- req.onsuccess = () => resolve(req.result);
2457
- req.onerror = () => reject(req.error);
2458
- });
2459
- }
2460
- async function putInDocDb(key, value) {
2461
- const db = await getDocDb();
2462
- return new Promise((resolve, reject) => {
2463
- const tx = db.transaction(DOCUMENT_STORE, "readwrite");
2464
- const req = tx.objectStore(DOCUMENT_STORE).put(value, key);
2465
- req.onsuccess = () => resolve();
2466
- req.onerror = () => reject(req.error);
2467
- });
2468
- }
2469
- const revokedHrefs = /* @__PURE__ */ new Set();
2470
- function revokeItemBlob(item) {
2471
- if (item.imageRasterHref?.startsWith("blob:")) {
2472
- revokedHrefs.add(item.imageRasterHref);
2473
- }
2474
- if (item.imageThumbnailHref?.startsWith("blob:")) {
2475
- revokedHrefs.add(item.imageThumbnailHref);
2476
- }
2477
- const out = { ...item };
2478
- delete out.imageRasterHref;
2479
- delete out.imageThumbnailHref;
2480
- if (item.toolKind === "image") {
2481
- out.childrenSvg = "";
2482
- }
2483
- return out;
2484
- }
2485
- return {
2486
- async load() {
2487
- const raw = await getFromDocDb(DOCUMENT_KEY);
2488
- if (!raw?.items) return null;
2489
- const resolved = [];
2490
- for (const item of raw.items) {
2491
- if (item.toolKind === "image" && item.imageBlobId) {
2492
- const itemWithBlob = { ...item };
2493
- if (item.imageThumbnailBlobId) {
2494
- const thumbUrl = await createThumbnailBlobUrlFromStore(
2495
- imageStore,
2496
- item.imageThumbnailBlobId
2497
- );
2498
- if (thumbUrl) {
2499
- itemWithBlob.imageThumbnailHref = thumbUrl;
2500
- }
2501
- }
2502
- const fullUrl = await createBlobUrlFromStore(imageStore, item.imageBlobId);
2503
- if (fullUrl) {
2504
- itemWithBlob.imageRasterHref = fullUrl;
2505
- }
2506
- const rebuilt = rebuildItemSvg(itemWithBlob);
2507
- resolved.push(rebuilt);
2508
- } else {
2509
- resolved.push(item);
2510
- }
2511
- }
2512
- return { items: resolved, version: raw.version ?? 1 };
2513
- },
2514
- async save(snapshot) {
2515
- for (const href of revokedHrefs) {
2516
- URL.revokeObjectURL(href);
2517
- }
2518
- revokedHrefs.clear();
2519
- const cleanItems = snapshot.items.map(revokeItemBlob);
2520
- await putInDocDb(DOCUMENT_KEY, {
2521
- items: cleanItems,
2522
- version: snapshot.version ?? 1
2523
- });
2524
- }
2525
- };
3016
+ function IconEllipse(props) {
3017
+ return /* @__PURE__ */ jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6" }) });
2526
3018
  }
2527
-
2528
- // src/react/persistence/local-storage-adapter.ts
2529
- var DEFAULT_VECTOR_CANVAS_STORAGE_KEY = "trazo.vector-canvas.v1";
2530
- var SNAPSHOT_VERSION = 1;
2531
- function createLocalStoragePersistenceAdapter(options = {}) {
2532
- const key = options.key ?? DEFAULT_VECTOR_CANVAS_STORAGE_KEY;
2533
- const storage = options.storage ?? (typeof globalThis !== "undefined" && "localStorage" in globalThis && globalThis.localStorage ? globalThis.localStorage : null);
2534
- return {
2535
- load() {
2536
- if (!storage) return Promise.resolve(null);
2537
- try {
2538
- const raw = storage.getItem(key);
2539
- if (!raw) return Promise.resolve(null);
2540
- const parsed = JSON.parse(raw);
2541
- if (!parsed || typeof parsed !== "object" || !("items" in parsed) || !Array.isArray(parsed.items)) {
2542
- return Promise.resolve(null);
2543
- }
2544
- return Promise.resolve(parsed);
2545
- } catch {
2546
- return Promise.resolve(null);
2547
- }
2548
- },
2549
- save(snapshot) {
2550
- if (!storage) return Promise.resolve();
2551
- try {
2552
- storage.setItem(
2553
- key,
2554
- JSON.stringify({
2555
- ...snapshot,
2556
- version: snapshot.version ?? SNAPSHOT_VERSION
2557
- })
2558
- );
2559
- } catch {
2560
- }
2561
- return Promise.resolve();
2562
- }
2563
- };
3019
+ function IconLine(props) {
3020
+ return /* @__PURE__ */ jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }) });
2564
3021
  }
2565
- function createNoopPersistenceAdapter() {
2566
- return {
2567
- load: () => Promise.resolve(null),
2568
- save: () => Promise.resolve()
2569
- };
2570
- }
2571
- function useVectorCanvasDocument(options = {}) {
2572
- const {
2573
- persistenceKey = DEFAULT_VECTOR_CANVAS_STORAGE_KEY,
2574
- persistence,
2575
- debounceMs = 400,
2576
- remote
2577
- } = options;
2578
- const adapter = useMemo(() => {
2579
- if (persistence === false) return createNoopPersistenceAdapter();
2580
- if (persistence) return persistence;
2581
- return createLocalStoragePersistenceAdapter({ key: persistenceKey });
2582
- }, [persistence, persistenceKey]);
2583
- const [items, setItems] = useState([]);
2584
- const [isHydrated, setIsHydrated] = useState(false);
2585
- const saveTimerRef = useRef(null);
2586
- const adapterRef = useRef(adapter);
2587
- adapterRef.current = adapter;
2588
- useEffect(() => {
2589
- let cancelled = false;
2590
- adapter.load().then((snap) => {
2591
- if (cancelled) return;
2592
- if (snap?.items && snap.items.length > 0) {
2593
- setItems(snap.items);
2594
- }
2595
- setIsHydrated(true);
2596
- });
2597
- return () => {
2598
- cancelled = true;
2599
- };
2600
- }, [adapter]);
2601
- const persist = useCallback((next) => {
2602
- void adapterRef.current.save({ items: next, version: 1 });
2603
- }, []);
2604
- const onItemsChange = useCallback(
2605
- (next) => {
2606
- setItems(next);
2607
- remote?.send?.(next);
2608
- if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
2609
- saveTimerRef.current = setTimeout(() => {
2610
- persist(next);
2611
- saveTimerRef.current = null;
2612
- }, debounceMs);
2613
- },
2614
- [debounceMs, persist, remote]
2615
- );
2616
- useEffect(() => {
2617
- if (!remote || !isHydrated) return;
2618
- return remote.subscribe((serverItems) => {
2619
- setItems(serverItems);
2620
- persist(serverItems);
2621
- });
2622
- }, [remote, isHydrated, persist]);
2623
- useEffect(
2624
- () => () => {
2625
- if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
2626
- },
2627
- []
2628
- );
2629
- const clearPersistedDocument = useCallback(() => {
2630
- setItems([]);
2631
- void adapterRef.current.save({ items: [], version: 1 });
2632
- }, []);
2633
- return {
2634
- items,
2635
- onItemsChange,
2636
- setItems: onItemsChange,
2637
- isHydrated,
2638
- clearPersistedDocument
2639
- };
3022
+ function IconArrow(props) {
3023
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3024
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }),
3025
+ /* @__PURE__ */ jsx("polyline", { points: "12 5 19 5 19 12" })
3026
+ ] });
2640
3027
  }
2641
- var CanvuPluginContext = createContext(
2642
- null
2643
- );
2644
- function createCanvuPlugin(plugin) {
2645
- return plugin;
3028
+ function IconDraw(props) {
3029
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3030
+ /* @__PURE__ */ jsx("path", { d: "M12 19l7-7 3 3-7 7-3-3z" }),
3031
+ /* @__PURE__ */ jsx("path", { d: "M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" }),
3032
+ /* @__PURE__ */ jsx("path", { d: "M2 2l7.586 7.586" }),
3033
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "2" })
3034
+ ] });
2646
3035
  }
2647
- function useCanvuPluginContext() {
2648
- const ctx = useContext(CanvuPluginContext);
2649
- if (!ctx) {
2650
- throw new Error(
2651
- "useCanvuPluginContext must be used inside a VectorViewport plugin runtime."
2652
- );
2653
- }
2654
- return ctx;
3036
+ function IconText(props) {
3037
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3038
+ /* @__PURE__ */ jsx("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }),
3039
+ /* @__PURE__ */ jsx("path", { d: "M9 21h6" }),
3040
+ /* @__PURE__ */ jsx("path", { d: "M12 3v18" })
3041
+ ] });
2655
3042
  }
2656
- function useCanvuViewportContext() {
2657
- const { viewportRef, viewport } = useCanvuPluginContext();
2658
- return { viewportRef, viewport };
3043
+ function IconImage(props) {
3044
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3045
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
3046
+ /* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
3047
+ /* @__PURE__ */ jsx("polyline", { points: "21 15 16 10 5 21" })
3048
+ ] });
2659
3049
  }
2660
- function useCanvuDocumentContext() {
2661
- const { viewport } = useCanvuPluginContext();
2662
- return {
2663
- items: viewport.items,
2664
- onItemsChange: viewport.onItemsChange
2665
- };
3050
+ function IconLaser(props) {
3051
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
3052
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.5" }),
3053
+ /* @__PURE__ */ jsx("path", { d: "M12 4v4" }),
3054
+ /* @__PURE__ */ jsx("path", { d: "M12 16v4" }),
3055
+ /* @__PURE__ */ jsx("path", { d: "M4 12h4" }),
3056
+ /* @__PURE__ */ jsx("path", { d: "M16 12h4" })
3057
+ ] });
2666
3058
  }
2667
- function useCanvuResolvedTools() {
2668
- return useCanvuPluginContext().resolvedTools;
3059
+ var ic = { size: 20, strokeWidth: 2 };
3060
+ var DEFAULT_OVERFLOW_TOOL_IDS = [
3061
+ "rect",
3062
+ "ellipse",
3063
+ "line",
3064
+ "marker",
3065
+ "laser",
3066
+ "image"
3067
+ ];
3068
+ var DEFAULT_VECTOR_TOOLS = [
3069
+ {
3070
+ id: "hand",
3071
+ label: "Hand",
3072
+ icon: /* @__PURE__ */ jsx(Hand, { ...ic, "aria-hidden": true }),
3073
+ shortcutHint: "H"
3074
+ },
3075
+ {
3076
+ id: "select",
3077
+ label: "Select",
3078
+ icon: /* @__PURE__ */ jsx(MousePointer2, { ...ic, "aria-hidden": true }),
3079
+ shortcutHint: "V"
3080
+ },
3081
+ {
3082
+ id: "rect",
3083
+ label: "Rectangle",
3084
+ icon: /* @__PURE__ */ jsx(Square, { ...ic, "aria-hidden": true }),
3085
+ shortcutHint: "R"
3086
+ },
3087
+ {
3088
+ id: "ellipse",
3089
+ label: "Ellipse",
3090
+ icon: /* @__PURE__ */ jsx(Circle, { ...ic, "aria-hidden": true }),
3091
+ shortcutHint: "O"
3092
+ },
3093
+ {
3094
+ id: "line",
3095
+ label: "Line",
3096
+ icon: /* @__PURE__ */ jsx(Minus, { ...ic, "aria-hidden": true }),
3097
+ shortcutHint: "L"
3098
+ },
3099
+ {
3100
+ id: "arrow",
3101
+ label: "Arrow",
3102
+ icon: /* @__PURE__ */ jsx(ArrowUpRight, { ...ic, "aria-hidden": true }),
3103
+ shortcutHint: "A"
3104
+ },
3105
+ {
3106
+ id: "draw",
3107
+ label: "Desenhar",
3108
+ tooltipLabel: "Draw",
3109
+ icon: /* @__PURE__ */ jsx(PenLine, { ...ic, "aria-hidden": true }),
3110
+ shortcutHint: "D"
3111
+ },
3112
+ {
3113
+ id: "marker",
3114
+ label: "Realce",
3115
+ tooltipLabel: "Highlighter",
3116
+ icon: /* @__PURE__ */ jsx(Highlighter, { ...ic, "aria-hidden": true }),
3117
+ shortcutHint: "M"
3118
+ },
3119
+ {
3120
+ id: "laser",
3121
+ label: "Laser",
3122
+ icon: /* @__PURE__ */ jsx(IconLaser, { "aria-hidden": true }),
3123
+ shortcutHint: "K"
3124
+ },
3125
+ {
3126
+ id: "eraser",
3127
+ label: "Borracha",
3128
+ tooltipLabel: "Eraser",
3129
+ icon: /* @__PURE__ */ jsx(Eraser, { ...ic, "aria-hidden": true }),
3130
+ shortcutHint: "E"
3131
+ },
3132
+ {
3133
+ id: "text",
3134
+ label: "Text",
3135
+ icon: /* @__PURE__ */ jsx(Type, { ...ic, "aria-hidden": true }),
3136
+ shortcutHint: "T"
3137
+ },
3138
+ {
3139
+ id: "image",
3140
+ label: "File",
3141
+ icon: /* @__PURE__ */ jsx(Image$1, { ...ic, "aria-hidden": true }),
3142
+ shortcutHint: "I"
3143
+ }
3144
+ ];
3145
+ var shellLook = {
3146
+ display: "flex",
3147
+ flexDirection: "column",
3148
+ gap: 8,
3149
+ minWidth: 160,
3150
+ padding: "10px 12px",
3151
+ borderRadius: 8,
3152
+ border: "1px solid rgba(0,0,0,0.12)",
3153
+ background: "rgba(255,255,255,0.96)",
3154
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
3155
+ pointerEvents: "auto"
3156
+ };
3157
+ var labelStyle2 = {
3158
+ display: "flex",
3159
+ flexDirection: "column",
3160
+ gap: 4,
3161
+ fontSize: 11,
3162
+ fontWeight: 600,
3163
+ color: "#52525b"
3164
+ };
3165
+ function normalizeHex(stroke) {
3166
+ if (/^#[0-9A-Fa-f]{6}$/.test(stroke)) return stroke;
3167
+ return "#2563eb";
2669
3168
  }
2670
- function useCanvuPluginContribution(pluginId, contribution) {
2671
- const { registerContribution, unregisterContribution } = useCanvuPluginContext();
2672
- useLayoutEffect(() => {
2673
- registerContribution(pluginId, contribution);
2674
- return () => unregisterContribution(pluginId);
2675
- }, [contribution, pluginId, registerContribution, unregisterContribution]);
3169
+ function isStylableKind(tk) {
3170
+ return tk === "rect" || tk === "ellipse" || tk === "line" || tk === "arrow" || tk === "draw" || tk === "pencil" || tk === "brush" || tk === "marker" || tk === "text";
2676
3171
  }
2677
- function ToolPluginComponent({
2678
- pluginId,
2679
- tool,
2680
- toolTransform,
2681
- createItem
2682
- }) {
2683
- const contribution = useMemo(
2684
- () => ({
2685
- tools: [tool],
2686
- toolTransform,
2687
- customPlacements: createItem ? [
2688
- {
2689
- toolId: tool.id,
2690
- createItem
2691
- }
2692
- ] : void 0
2693
- }),
2694
- [createItem, tool, toolTransform]
2695
- );
2696
- useCanvuPluginContribution(pluginId, contribution);
2697
- return null;
2698
- }
2699
- function createToolPlugin(options) {
2700
- const { createItem, toolTransform, ...tool } = options;
2701
- const pluginId = `canvu.plugin.tool:${tool.id}`;
2702
- return createCanvuPlugin({
2703
- id: pluginId,
2704
- Component() {
2705
- return /* @__PURE__ */ jsx(
2706
- ToolPluginComponent,
2707
- {
2708
- pluginId,
2709
- tool,
2710
- toolTransform,
2711
- createItem
2712
- }
2713
- );
2714
- }
2715
- });
2716
- }
2717
- var menuStyle = {
2718
- position: "fixed",
2719
- zIndex: 1e4,
2720
- minWidth: 200,
2721
- padding: 4,
2722
- backgroundColor: "#ffffff",
2723
- border: "1px solid #e2e8f0",
2724
- borderRadius: 8,
2725
- boxShadow: "0 10px 40px rgba(15, 23, 42, 0.12)",
2726
- fontSize: 13,
2727
- fontFamily: "system-ui, sans-serif",
2728
- color: "#0f172a"
2729
- };
2730
- var itemStyle = {
2731
- display: "block",
2732
- width: "100%",
2733
- textAlign: "left",
2734
- padding: "8px 12px",
2735
- margin: 0,
2736
- border: "none",
2737
- borderRadius: 4,
2738
- background: "transparent",
2739
- cursor: "pointer"
2740
- };
2741
- var dividerStyle = {
2742
- height: 1,
2743
- margin: "4px 8px",
2744
- background: "#e2e8f0"
2745
- };
2746
- function ShapeContextMenu({
2747
- x,
2748
- y,
2749
- allSelectedLocked,
2750
- onClose,
2751
- onToggleLock,
2752
- onCut,
2753
- onCopy,
2754
- onBringToFront,
2755
- onBringForward,
2756
- onSendBackward,
2757
- onSendToBack,
2758
- onDuplicate,
2759
- onDelete
3172
+ function VectorSelectionInspector({
3173
+ items: itemsProp,
3174
+ activeToolStyle: activeToolStyleProp,
3175
+ onChange: onChangeProp,
3176
+ position = "top-left",
3177
+ inset = 12,
3178
+ zIndex = 24,
3179
+ className,
3180
+ style
2760
3181
  }) {
2761
- const rootRef = useRef(null);
2762
- useLayoutEffect(() => {
2763
- const el = rootRef.current;
2764
- if (!el) return;
2765
- const pad = 8;
2766
- const w = el.offsetWidth || 200;
2767
- const h = el.offsetHeight || 160;
2768
- let left = x;
2769
- let top = y;
2770
- if (left + w + pad > window.innerWidth) {
2771
- left = Math.max(pad, window.innerWidth - w - pad);
2772
- }
2773
- if (top + h + pad > window.innerHeight) {
2774
- top = Math.max(pad, window.innerHeight - h - pad);
2775
- }
2776
- el.style.left = `${left}px`;
2777
- el.style.top = `${top}px`;
2778
- }, [x, y]);
2779
- useEffect(() => {
2780
- const onKey = (e) => {
2781
- if (e.key === "Escape") {
2782
- onClose();
2783
- }
2784
- };
2785
- const onPointerDown = (e) => {
2786
- const t = e.target;
2787
- if (t && rootRef.current?.contains(t)) {
2788
- return;
2789
- }
2790
- onClose();
2791
- };
2792
- document.addEventListener("keydown", onKey);
2793
- document.addEventListener("pointerdown", onPointerDown, true);
2794
- return () => {
2795
- document.removeEventListener("keydown", onKey);
2796
- document.removeEventListener("pointerdown", onPointerDown, true);
2797
- };
2798
- }, [onClose]);
2799
- const run = (fn) => () => {
2800
- fn();
2801
- onClose();
3182
+ const ctx = useCanvuChromeContext();
3183
+ const items = itemsProp ?? ctx?.selectedItems ?? [];
3184
+ const activeToolStyle = activeToolStyleProp === void 0 ? ctx?.activeToolStyle ?? null : activeToolStyleProp;
3185
+ const onChange = onChangeProp ?? ctx?.onSelectionStyleChange ?? null;
3186
+ if (!onChange) return null;
3187
+ const shell = {
3188
+ ...getBoardPositionStyle(position, inset, zIndex),
3189
+ ...shellLook,
3190
+ ...style
2802
3191
  };
2803
- const renderAction = (label, onClick, options) => /* @__PURE__ */ jsx(
2804
- "button",
2805
- {
2806
- type: "button",
2807
- role: "menuitem",
2808
- style: {
2809
- ...itemStyle,
2810
- ...options?.danger ? { color: "#b91c1c" } : {}
2811
- },
2812
- onMouseEnter: (e) => {
2813
- e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
2814
- },
2815
- onMouseLeave: (e) => {
2816
- e.currentTarget.style.background = "transparent";
2817
- },
2818
- onClick: run(onClick),
2819
- children: label
2820
- }
3192
+ if (activeToolStyle) {
3193
+ const stroke2 = activeToolStyle.stroke;
3194
+ const strokeWidth2 = activeToolStyle.strokeWidth;
3195
+ const hex2 = normalizeHex(stroke2);
3196
+ const showMarkerOpacity2 = activeToolStyle.toolKind === "marker";
3197
+ const opacityPct2 = Math.round((activeToolStyle.strokeOpacity ?? 1) * 100);
3198
+ return /* @__PURE__ */ jsxs(
3199
+ "section",
3200
+ {
3201
+ "data-slot": "vector-selection-inspector",
3202
+ "data-position": position,
3203
+ className,
3204
+ "aria-label": activeToolStyle.label ?? "Estilo da ferramenta",
3205
+ style: shell,
3206
+ children: [
3207
+ /* @__PURE__ */ jsxs("label", { style: labelStyle2, children: [
3208
+ "Cor",
3209
+ /* @__PURE__ */ jsx(
3210
+ "input",
3211
+ {
3212
+ type: "color",
3213
+ value: hex2,
3214
+ onChange: (e) => onChange({
3215
+ stroke: e.target.value,
3216
+ strokeWidth: strokeWidth2,
3217
+ ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
3218
+ }),
3219
+ style: {
3220
+ width: "100%",
3221
+ height: 32,
3222
+ padding: 0,
3223
+ border: "none",
3224
+ cursor: "pointer",
3225
+ background: "transparent"
3226
+ }
3227
+ }
3228
+ )
3229
+ ] }),
3230
+ /* @__PURE__ */ jsxs("label", { style: labelStyle2, children: [
3231
+ "Grossura",
3232
+ /* @__PURE__ */ jsx(
3233
+ "input",
3234
+ {
3235
+ type: "range",
3236
+ min: 1,
3237
+ max: 48,
3238
+ value: strokeWidth2,
3239
+ onChange: (e) => onChange({
3240
+ stroke: hex2,
3241
+ strokeWidth: Number(e.target.value),
3242
+ ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
3243
+ })
3244
+ }
3245
+ )
3246
+ ] }),
3247
+ showMarkerOpacity2 && /* @__PURE__ */ jsxs("label", { style: labelStyle2, children: [
3248
+ "Opacidade (marcador)",
3249
+ /* @__PURE__ */ jsx(
3250
+ "input",
3251
+ {
3252
+ type: "range",
3253
+ min: 10,
3254
+ max: 100,
3255
+ value: opacityPct2,
3256
+ onChange: (e) => {
3257
+ const v = Number(e.target.value) / 100;
3258
+ onChange({
3259
+ stroke: hex2,
3260
+ strokeWidth: strokeWidth2,
3261
+ strokeOpacity: v
3262
+ });
3263
+ }
3264
+ }
3265
+ ),
3266
+ /* @__PURE__ */ jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
3267
+ opacityPct2,
3268
+ "%"
3269
+ ] })
3270
+ ] })
3271
+ ]
3272
+ }
3273
+ );
3274
+ }
3275
+ const stylable = items.filter(
3276
+ (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
2821
3277
  );
2822
- const menu = /* @__PURE__ */ jsxs(
2823
- "div",
3278
+ if (stylable.length === 0) return null;
3279
+ const first = stylable[0];
3280
+ if (!first) return null;
3281
+ const allSameStroke = stylable.every(
3282
+ (it) => (it.stroke ?? "#2563eb") === (first.stroke ?? "#2563eb")
3283
+ );
3284
+ const allSameWidth = stylable.every(
3285
+ (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
3286
+ );
3287
+ const stroke = first.stroke ?? "#2563eb";
3288
+ const strokeWidth = first.strokeWidth ?? 2;
3289
+ const hex = normalizeHex(stroke);
3290
+ const markers = stylable.filter((it) => it.toolKind === "marker");
3291
+ const showMarkerOpacity = markers.length > 0;
3292
+ const firstMarker = markers[0];
3293
+ const allSameMarkerOpacity = markers.length > 0 && markers.every(
3294
+ (it) => (it.strokeOpacity ?? 1) === (firstMarker?.strokeOpacity ?? 1)
3295
+ );
3296
+ const opacityPct = firstMarker ? Math.round((firstMarker.strokeOpacity ?? 1) * 100) : 100;
3297
+ return /* @__PURE__ */ jsxs(
3298
+ "section",
2824
3299
  {
2825
- ref: rootRef,
2826
- "data-slot": "shape-context-menu",
2827
- style: { ...menuStyle, left: x, top: y },
2828
- role: "menu",
3300
+ "data-slot": "vector-selection-inspector",
3301
+ "data-position": position,
3302
+ className,
3303
+ "aria-label": "Estilo da sele\xE7\xE3o",
3304
+ style: shell,
2829
3305
  children: [
2830
- renderAction("Recortar", onCut),
2831
- renderAction("Copiar", onCopy),
2832
- renderAction("Duplicar", onDuplicate),
2833
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle }),
2834
- renderAction("Trazer para frente", onBringToFront),
2835
- renderAction("Avancar uma camada", onBringForward),
2836
- renderAction("Recuar uma camada", onSendBackward),
2837
- renderAction("Enviar para tras", onSendToBack),
2838
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle }),
2839
- renderAction(allSelectedLocked ? "Desbloquear" : "Bloquear", onToggleLock),
2840
- renderAction("Apagar", onDelete, { danger: true })
3306
+ stylable.length > 1 && /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
3307
+ stylable.length,
3308
+ " objetos selecionados"
3309
+ ] }),
3310
+ /* @__PURE__ */ jsxs("label", { style: labelStyle2, children: [
3311
+ "Cor",
3312
+ !allSameStroke && /* @__PURE__ */ jsxs("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: [
3313
+ " ",
3314
+ "(valores misturados \u2014 novo valor aplica a todos)"
3315
+ ] }),
3316
+ /* @__PURE__ */ jsx(
3317
+ "input",
3318
+ {
3319
+ type: "color",
3320
+ value: hex,
3321
+ onChange: (e) => onChange({
3322
+ stroke: e.target.value,
3323
+ strokeWidth
3324
+ }),
3325
+ style: {
3326
+ width: "100%",
3327
+ height: 32,
3328
+ padding: 0,
3329
+ border: "none",
3330
+ cursor: "pointer",
3331
+ background: "transparent"
3332
+ }
3333
+ }
3334
+ )
3335
+ ] }),
3336
+ /* @__PURE__ */ jsxs("label", { style: labelStyle2, children: [
3337
+ "Grossura",
3338
+ !allSameWidth && /* @__PURE__ */ jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
3339
+ /* @__PURE__ */ jsx(
3340
+ "input",
3341
+ {
3342
+ type: "range",
3343
+ min: 1,
3344
+ max: 48,
3345
+ value: strokeWidth,
3346
+ onChange: (e) => onChange({
3347
+ stroke: hex,
3348
+ strokeWidth: Number(e.target.value)
3349
+ })
3350
+ }
3351
+ )
3352
+ ] }),
3353
+ showMarkerOpacity && firstMarker && /* @__PURE__ */ jsxs("label", { style: labelStyle2, children: [
3354
+ "Opacidade (marcador)",
3355
+ !allSameMarkerOpacity && markers.length > 1 && /* @__PURE__ */ jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
3356
+ /* @__PURE__ */ jsx(
3357
+ "input",
3358
+ {
3359
+ type: "range",
3360
+ min: 10,
3361
+ max: 100,
3362
+ value: opacityPct,
3363
+ onChange: (e) => {
3364
+ const v = Number(e.target.value) / 100;
3365
+ onChange({
3366
+ stroke: hex,
3367
+ strokeWidth,
3368
+ strokeOpacity: v
3369
+ });
3370
+ }
3371
+ }
3372
+ ),
3373
+ /* @__PURE__ */ jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
3374
+ opacityPct,
3375
+ "%"
3376
+ ] })
3377
+ ] })
2841
3378
  ]
2842
3379
  }
2843
3380
  );
2844
- if (typeof document === "undefined") {
2845
- return null;
2846
- }
2847
- return createPortal(menu, document.body);
2848
3381
  }
2849
- var base = {
2850
- width: 20,
2851
- height: 20,
2852
- viewBox: "0 0 24 24",
2853
- fill: "none",
2854
- stroke: "currentColor",
2855
- strokeWidth: 2,
2856
- strokeLinecap: "round",
2857
- strokeLinejoin: "round"
3382
+ var rootStyle = {
3383
+ display: "flex",
3384
+ flexDirection: "column",
3385
+ height: "100%",
3386
+ minHeight: 0,
3387
+ width: "100%"
2858
3388
  };
2859
- function IconHand(props) {
2860
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2861
- /* @__PURE__ */ jsx("path", { d: "M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0" }),
2862
- /* @__PURE__ */ jsx("path", { d: "M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2" }),
2863
- /* @__PURE__ */ jsx("path", { d: "M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8" }),
2864
- /* @__PURE__ */ jsx("path", { d: "M18 8a2 2 0 1 1 4 0v5a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" })
2865
- ] });
2866
- }
2867
- function IconSelect(props) {
2868
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2869
- /* @__PURE__ */ jsx("path", { d: "M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" }),
2870
- /* @__PURE__ */ jsx("path", { d: "M13 13l6 6" })
2871
- ] });
2872
- }
2873
- function IconRect(props) {
2874
- return /* @__PURE__ */ jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2" }) });
2875
- }
2876
- function IconEllipse(props) {
2877
- return /* @__PURE__ */ jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6" }) });
2878
- }
2879
- function IconLine(props) {
2880
- return /* @__PURE__ */ jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }) });
2881
- }
2882
- function IconArrow(props) {
2883
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2884
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }),
2885
- /* @__PURE__ */ jsx("polyline", { points: "12 5 19 5 19 12" })
2886
- ] });
2887
- }
2888
- function IconDraw(props) {
2889
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2890
- /* @__PURE__ */ jsx("path", { d: "M12 19l7-7 3 3-7 7-3-3z" }),
2891
- /* @__PURE__ */ jsx("path", { d: "M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" }),
2892
- /* @__PURE__ */ jsx("path", { d: "M2 2l7.586 7.586" }),
2893
- /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "2" })
2894
- ] });
3389
+ var headerStyle2 = {
3390
+ flexShrink: 0,
3391
+ display: "flex",
3392
+ flexWrap: "wrap",
3393
+ alignItems: "center",
3394
+ gap: "0.75rem",
3395
+ padding: "0.75rem 1rem",
3396
+ borderBottom: "1px solid #e4e4e7",
3397
+ background: "#fff",
3398
+ fontSize: "0.875rem"
3399
+ };
3400
+ var bodyStyle = {
3401
+ flex: 1,
3402
+ minHeight: 0,
3403
+ display: "flex",
3404
+ flexDirection: "column"
3405
+ };
3406
+ var mainStyle = {
3407
+ flex: 1,
3408
+ minHeight: 0,
3409
+ position: "relative",
3410
+ display: "flex",
3411
+ flexDirection: "column"
3412
+ };
3413
+ var viewportSurfaceStyle = {
3414
+ flex: 1,
3415
+ minHeight: 0,
3416
+ position: "relative",
3417
+ width: "100%",
3418
+ alignSelf: "stretch",
3419
+ background: "#fff",
3420
+ touchAction: "none"
3421
+ };
3422
+ function mergeStyle(base2, style) {
3423
+ return style ? { ...base2, ...style } : base2;
2895
3424
  }
2896
- function IconText(props) {
2897
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2898
- /* @__PURE__ */ jsx("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }),
2899
- /* @__PURE__ */ jsx("path", { d: "M9 21h6" }),
2900
- /* @__PURE__ */ jsx("path", { d: "M12 3v18" })
2901
- ] });
3425
+ function vectorCanvasSpaceStyle(position, inset, zIndex) {
3426
+ return {
3427
+ ...getBoardPositionStyle(position, inset, zIndex),
3428
+ pointerEvents: "none"
3429
+ };
2902
3430
  }
2903
- function IconImage(props) {
2904
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2905
- /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2906
- /* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2907
- /* @__PURE__ */ jsx("polyline", { points: "21 15 16 10 5 21" })
2908
- ] });
3431
+ function VectorCanvasRoot({
3432
+ children,
3433
+ className,
3434
+ style
3435
+ }) {
3436
+ return /* @__PURE__ */ jsx(
3437
+ "div",
3438
+ {
3439
+ "data-slot": "vector-canvas-root",
3440
+ className,
3441
+ style: mergeStyle(rootStyle, style),
3442
+ children
3443
+ }
3444
+ );
2909
3445
  }
2910
- function IconLaser(props) {
2911
- return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2912
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.5" }),
2913
- /* @__PURE__ */ jsx("path", { d: "M12 4v4" }),
2914
- /* @__PURE__ */ jsx("path", { d: "M12 16v4" }),
2915
- /* @__PURE__ */ jsx("path", { d: "M4 12h4" }),
2916
- /* @__PURE__ */ jsx("path", { d: "M16 12h4" })
2917
- ] });
3446
+ function VectorCanvasHeader({
3447
+ children,
3448
+ className,
3449
+ style
3450
+ }) {
3451
+ return /* @__PURE__ */ jsx(
3452
+ "header",
3453
+ {
3454
+ "data-slot": "vector-canvas-header",
3455
+ className,
3456
+ style: mergeStyle(headerStyle2, style),
3457
+ children
3458
+ }
3459
+ );
2918
3460
  }
2919
- var ic = { size: 20, strokeWidth: 2 };
2920
- var DEFAULT_OVERFLOW_TOOL_IDS = [
2921
- "rect",
2922
- "ellipse",
2923
- "line",
2924
- "marker",
2925
- "laser",
2926
- "image"
2927
- ];
2928
- var DEFAULT_VECTOR_TOOLS = [
2929
- {
2930
- id: "hand",
2931
- label: "Hand",
2932
- icon: /* @__PURE__ */ jsx(Hand, { ...ic, "aria-hidden": true }),
2933
- shortcutHint: "H"
2934
- },
2935
- {
2936
- id: "select",
2937
- label: "Select",
2938
- icon: /* @__PURE__ */ jsx(MousePointer2, { ...ic, "aria-hidden": true }),
2939
- shortcutHint: "V"
2940
- },
2941
- {
2942
- id: "rect",
2943
- label: "Rectangle",
2944
- icon: /* @__PURE__ */ jsx(Square, { ...ic, "aria-hidden": true }),
2945
- shortcutHint: "R"
2946
- },
2947
- {
2948
- id: "ellipse",
2949
- label: "Ellipse",
2950
- icon: /* @__PURE__ */ jsx(Circle, { ...ic, "aria-hidden": true }),
2951
- shortcutHint: "O"
2952
- },
2953
- {
2954
- id: "line",
2955
- label: "Line",
2956
- icon: /* @__PURE__ */ jsx(Minus, { ...ic, "aria-hidden": true }),
2957
- shortcutHint: "L"
2958
- },
2959
- {
2960
- id: "arrow",
2961
- label: "Arrow",
2962
- icon: /* @__PURE__ */ jsx(ArrowUpRight, { ...ic, "aria-hidden": true }),
2963
- shortcutHint: "A"
2964
- },
2965
- {
2966
- id: "draw",
2967
- label: "Desenhar",
2968
- tooltipLabel: "Draw",
2969
- icon: /* @__PURE__ */ jsx(PenLine, { ...ic, "aria-hidden": true }),
2970
- shortcutHint: "D"
2971
- },
2972
- {
2973
- id: "marker",
2974
- label: "Realce",
2975
- tooltipLabel: "Highlighter",
2976
- icon: /* @__PURE__ */ jsx(Highlighter, { ...ic, "aria-hidden": true }),
2977
- shortcutHint: "M"
2978
- },
2979
- {
2980
- id: "laser",
2981
- label: "Laser",
2982
- icon: /* @__PURE__ */ jsx(IconLaser, { "aria-hidden": true }),
2983
- shortcutHint: "K"
2984
- },
2985
- {
2986
- id: "eraser",
2987
- label: "Borracha",
2988
- tooltipLabel: "Eraser",
2989
- icon: /* @__PURE__ */ jsx(Eraser, { ...ic, "aria-hidden": true }),
2990
- shortcutHint: "E"
2991
- },
2992
- {
2993
- id: "text",
2994
- label: "Text",
2995
- icon: /* @__PURE__ */ jsx(Type, { ...ic, "aria-hidden": true }),
2996
- shortcutHint: "T"
2997
- },
2998
- {
2999
- id: "image",
3000
- label: "File",
3001
- icon: /* @__PURE__ */ jsx(Image$1, { ...ic, "aria-hidden": true }),
3002
- shortcutHint: "I"
3461
+ function VectorCanvasBody({
3462
+ children,
3463
+ className,
3464
+ style
3465
+ }) {
3466
+ return /* @__PURE__ */ jsx(
3467
+ "div",
3468
+ {
3469
+ "data-slot": "vector-canvas-body",
3470
+ className,
3471
+ style: mergeStyle(bodyStyle, style),
3472
+ children
3473
+ }
3474
+ );
3475
+ }
3476
+ function VectorCanvasMain({
3477
+ children,
3478
+ className,
3479
+ style
3480
+ }) {
3481
+ return /* @__PURE__ */ jsx(
3482
+ "div",
3483
+ {
3484
+ "data-slot": "vector-canvas-main",
3485
+ className,
3486
+ style: mergeStyle(mainStyle, style),
3487
+ children
3488
+ }
3489
+ );
3490
+ }
3491
+ function VectorCanvasViewportSurface({
3492
+ children,
3493
+ className,
3494
+ style
3495
+ }) {
3496
+ return /* @__PURE__ */ jsx(
3497
+ "div",
3498
+ {
3499
+ "data-slot": "vector-canvas-viewport-surface",
3500
+ className,
3501
+ style: mergeStyle(viewportSurfaceStyle, style),
3502
+ children
3503
+ }
3504
+ );
3505
+ }
3506
+ function VectorCanvasToolbar({
3507
+ children,
3508
+ className,
3509
+ style,
3510
+ position = "bottom-center",
3511
+ inset = 12,
3512
+ zIndex = 30
3513
+ }) {
3514
+ const base2 = {
3515
+ ...getBoardPositionStyle(position, inset, zIndex),
3516
+ display: "flex",
3517
+ justifyContent: "center",
3518
+ alignItems: "center",
3519
+ maxWidth: "calc(100% - 24px)",
3520
+ pointerEvents: "none"
3521
+ };
3522
+ return /* @__PURE__ */ jsx(
3523
+ "div",
3524
+ {
3525
+ "data-slot": "vector-canvas-toolbar",
3526
+ "data-position": position,
3527
+ className,
3528
+ style: mergeStyle(base2, style),
3529
+ children: /* @__PURE__ */ jsx("div", { style: { pointerEvents: "auto" }, children })
3530
+ }
3531
+ );
3532
+ }
3533
+ function VectorCanvasSpace({
3534
+ children,
3535
+ className,
3536
+ style,
3537
+ position = "top-right",
3538
+ inset = 12,
3539
+ zIndex = 40,
3540
+ contentPointerEvents = "auto"
3541
+ }) {
3542
+ return /* @__PURE__ */ jsx(
3543
+ "div",
3544
+ {
3545
+ "data-slot": `vector-canvas-space-${position}`,
3546
+ className,
3547
+ style: mergeStyle(vectorCanvasSpaceStyle(position, inset, zIndex), style),
3548
+ children: /* @__PURE__ */ jsx("div", { style: { pointerEvents: contentPointerEvents }, children })
3549
+ }
3550
+ );
3551
+ }
3552
+ var VectorCanvas = {
3553
+ Root: VectorCanvasRoot,
3554
+ Header: VectorCanvasHeader,
3555
+ Body: VectorCanvasBody,
3556
+ Main: VectorCanvasMain,
3557
+ ViewportSurface: VectorCanvasViewportSurface,
3558
+ Toolbar: VectorCanvasToolbar,
3559
+ Space: VectorCanvasSpace,
3560
+ NavMenu,
3561
+ SelectionInspector: VectorSelectionInspector
3562
+ };
3563
+ var VectorToolbarItemContext = createContext(
3564
+ null
3565
+ );
3566
+ function useVectorToolbarItemContext(componentName) {
3567
+ const ctx = useContext(VectorToolbarItemContext);
3568
+ if (!ctx) {
3569
+ throw new Error(`${componentName} must be used inside <VectorToolbar>`);
3003
3570
  }
3004
- ];
3571
+ return ctx;
3572
+ }
3005
3573
  var toolbarStyles = {
3006
3574
  display: "flex",
3007
3575
  gap: "4px",
@@ -3250,6 +3818,20 @@ var overflowGridCellButtonStyle = {
3250
3818
  outline: "none",
3251
3819
  WebkitTapHighlightColor: "transparent"
3252
3820
  };
3821
+ var activeToggleStyle = {
3822
+ background: "rgba(24,24,27,0.1)",
3823
+ borderColor: "rgba(24,24,27,0.28)",
3824
+ boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3825
+ };
3826
+ var inactiveToggleStyle = {
3827
+ borderColor: "transparent",
3828
+ boxShadow: "none"
3829
+ };
3830
+ function preventMouseDefault2(e) {
3831
+ if (e.pointerType === "mouse") {
3832
+ e.preventDefault();
3833
+ }
3834
+ }
3253
3835
  function ToolLockGlyph({ locked }) {
3254
3836
  const Icon = locked ? Lock : LockOpen;
3255
3837
  return /* @__PURE__ */ jsx(Icon, { size: 18, strokeWidth: 2, "aria-hidden": true });
@@ -3263,7 +3845,406 @@ function splitToolbarTools(tools, overflowIds) {
3263
3845
  const overflow = overflowIds.map((id) => tools.find((t) => t.id === id)).filter((t) => Boolean(t));
3264
3846
  return { primary, overflow };
3265
3847
  }
3266
- var icOverflow = { size: 18, strokeWidth: 2 };
3848
+ var icOverflow = { size: 18, strokeWidth: 2 };
3849
+ function useOverflowDropdown() {
3850
+ const [open, setOpen] = useState(false);
3851
+ const wrapRef = useRef(null);
3852
+ const triggerRef = useRef(null);
3853
+ const menuId = useId();
3854
+ const close = useCallback(() => {
3855
+ setOpen(false);
3856
+ }, []);
3857
+ const prevOpenRef = useRef(open);
3858
+ useEffect(() => {
3859
+ if (!open) {
3860
+ return;
3861
+ }
3862
+ const onDocPointerDown = (e) => {
3863
+ if (wrapRef.current?.contains(e.target)) {
3864
+ return;
3865
+ }
3866
+ close();
3867
+ };
3868
+ const onKeyDown = (e) => {
3869
+ if (e.key === "Escape") {
3870
+ e.stopPropagation();
3871
+ close();
3872
+ triggerRef.current?.focus();
3873
+ }
3874
+ };
3875
+ document.addEventListener("pointerdown", onDocPointerDown, true);
3876
+ document.addEventListener("keydown", onKeyDown, true);
3877
+ return () => {
3878
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
3879
+ document.removeEventListener("keydown", onKeyDown, true);
3880
+ };
3881
+ }, [open, close]);
3882
+ useEffect(() => {
3883
+ if (prevOpenRef.current && !open) {
3884
+ triggerRef.current?.focus({ preventScroll: true });
3885
+ }
3886
+ prevOpenRef.current = open;
3887
+ }, [open]);
3888
+ return { open, setOpen, close, wrapRef, triggerRef, menuId };
3889
+ }
3890
+ function renderInlineToolButton(tool, ctx) {
3891
+ const {
3892
+ selectedId,
3893
+ selectTool,
3894
+ density,
3895
+ buttonClassName,
3896
+ activeButtonClassName,
3897
+ renderToolButton
3898
+ } = ctx;
3899
+ const selected = tool.id === selectedId;
3900
+ const label = tool.ariaLabel ?? tool.label;
3901
+ if (renderToolButton) {
3902
+ return /* @__PURE__ */ jsx("div", { children: renderToolButton({
3903
+ tool,
3904
+ selected,
3905
+ onSelect: () => selectTool(tool.id)
3906
+ }) }, tool.id);
3907
+ }
3908
+ const tooltipName = tool.tooltipLabel ?? tool.label;
3909
+ const tooltipText = tool.shortcutHint !== void 0 ? `${tooltipName} \xB7 ${tool.shortcutHint}` : tooltipName;
3910
+ const labelOnly = tool.icon == null;
3911
+ const labelTextStyle = labelOnly ? {
3912
+ fontSize: "12px",
3913
+ fontWeight: 600,
3914
+ letterSpacing: "0.02em",
3915
+ lineHeight: 1.2,
3916
+ WebkitFontSmoothing: "antialiased",
3917
+ MozOsxFontSmoothing: "grayscale",
3918
+ textRendering: "geometricPrecision"
3919
+ } : {};
3920
+ return /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
3921
+ /* @__PURE__ */ jsxs(
3922
+ "button",
3923
+ {
3924
+ type: "button",
3925
+ "aria-pressed": selected,
3926
+ "aria-label": label,
3927
+ ...tool.shortcutHint !== void 0 ? { "aria-keyshortcuts": tool.shortcutHint } : {},
3928
+ className: `${buttonClassName} ${selected ? activeButtonClassName : ""}`.trim(),
3929
+ style: {
3930
+ ...defaultButtonStyle,
3931
+ ...density === "compact" ? {
3932
+ minHeight: "40px",
3933
+ minWidth: labelOnly ? "52px" : "40px",
3934
+ padding: "6px 8px"
3935
+ } : {},
3936
+ ...selected ? activeToggleStyle : inactiveToggleStyle
3937
+ },
3938
+ onPointerDown: preventMouseDefault2,
3939
+ onClick: () => selectTool(tool.id),
3940
+ children: [
3941
+ /* @__PURE__ */ jsx(
3942
+ "span",
3943
+ {
3944
+ style: {
3945
+ display: "flex",
3946
+ alignItems: "center",
3947
+ justifyContent: "center"
3948
+ },
3949
+ children: tool.icon ?? /* @__PURE__ */ jsx("span", { style: labelTextStyle, children: tool.label })
3950
+ }
3951
+ ),
3952
+ density === "comfortable" && tool.icon ? /* @__PURE__ */ jsx(
3953
+ "span",
3954
+ {
3955
+ style: {
3956
+ lineHeight: 1,
3957
+ maxWidth: "56px",
3958
+ overflow: "hidden",
3959
+ textOverflow: "ellipsis"
3960
+ },
3961
+ children: tool.label
3962
+ }
3963
+ ) : null
3964
+ ]
3965
+ }
3966
+ ),
3967
+ /* @__PURE__ */ jsx("span", { className: "vector-toolbar-tip", "aria-hidden": "true", children: tooltipText })
3968
+ ] }, tool.id);
3969
+ }
3970
+ function renderOverflowToolButton(tool, ctx) {
3971
+ const { selectedId, selectTool, closeMenu, renderToolButton, tabIndex } = ctx;
3972
+ const selected = tool.id === selectedId;
3973
+ const label = tool.ariaLabel ?? tool.label;
3974
+ const tooltipName = tool.tooltipLabel ?? tool.label;
3975
+ if (renderToolButton) {
3976
+ return /* @__PURE__ */ jsx(
3977
+ "div",
3978
+ {
3979
+ className: "vector-toolbar-overflow-cell",
3980
+ role: "presentation",
3981
+ children: renderToolButton({
3982
+ tool,
3983
+ selected,
3984
+ onSelect: () => {
3985
+ selectTool(tool.id);
3986
+ closeMenu();
3987
+ }
3988
+ })
3989
+ },
3990
+ tool.id
3991
+ );
3992
+ }
3993
+ return /* @__PURE__ */ jsx("div", { className: "vector-toolbar-overflow-cell", children: /* @__PURE__ */ jsxs(
3994
+ "button",
3995
+ {
3996
+ type: "button",
3997
+ role: "menuitemradio",
3998
+ "aria-checked": selected,
3999
+ tabIndex: tabIndex ?? -1,
4000
+ "aria-label": label,
4001
+ ...tool.shortcutHint !== void 0 ? { "aria-keyshortcuts": tool.shortcutHint } : {},
4002
+ style: overflowGridCellButtonStyle,
4003
+ onPointerDown: preventMouseDefault2,
4004
+ onClick: () => {
4005
+ selectTool(tool.id);
4006
+ closeMenu();
4007
+ },
4008
+ children: [
4009
+ /* @__PURE__ */ jsx(
4010
+ "span",
4011
+ {
4012
+ style: {
4013
+ display: "flex",
4014
+ alignItems: "center",
4015
+ justifyContent: "center",
4016
+ lineHeight: 0
4017
+ },
4018
+ children: tool.icon ?? /* @__PURE__ */ jsx(
4019
+ "span",
4020
+ {
4021
+ style: {
4022
+ fontSize: "11px",
4023
+ fontWeight: 700,
4024
+ lineHeight: 1.1,
4025
+ textAlign: "center",
4026
+ maxWidth: "34px",
4027
+ overflow: "hidden",
4028
+ textOverflow: "ellipsis"
4029
+ },
4030
+ children: tool.label.slice(0, 3)
4031
+ }
4032
+ )
4033
+ }
4034
+ ),
4035
+ /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-overflow-tip", "aria-hidden": "true", children: [
4036
+ /* @__PURE__ */ jsx("span", { children: tooltipName }),
4037
+ tool.shortcutHint !== void 0 ? /* @__PURE__ */ jsx("span", { className: "vector-toolbar-overflow-tip-shortcut", children: tool.shortcutHint }) : null
4038
+ ] })
4039
+ ]
4040
+ }
4041
+ ) }, tool.id);
4042
+ }
4043
+ function hasToolIdProp(value) {
4044
+ return typeof value === "function" && "toolId" in value && typeof value.toolId === "string";
4045
+ }
4046
+ function findActiveOverflowChild(children, selectedId) {
4047
+ let result = null;
4048
+ Children.forEach(children, (child) => {
4049
+ if (result || !isValidElement(child)) {
4050
+ return;
4051
+ }
4052
+ const childType = child.type;
4053
+ let toolId;
4054
+ let label = "";
4055
+ let icon = null;
4056
+ const childProps = child.props ?? {};
4057
+ if (hasToolIdProp(childType)) {
4058
+ toolId = childType.toolId;
4059
+ const def = DEFAULT_VECTOR_TOOLS.find((t) => t.id === toolId);
4060
+ if (def) {
4061
+ label = def.label;
4062
+ icon = def.icon ?? null;
4063
+ }
4064
+ } else {
4065
+ toolId = childProps.id;
4066
+ label = childProps.label ?? "";
4067
+ icon = childProps.icon ?? null;
4068
+ }
4069
+ if (childProps.icon !== void 0) {
4070
+ icon = childProps.icon;
4071
+ }
4072
+ if (childProps.label !== void 0 && childProps.label !== "") {
4073
+ label = childProps.label;
4074
+ }
4075
+ if (toolId !== void 0 && toolId === selectedId) {
4076
+ result = { id: toolId, label, icon };
4077
+ }
4078
+ });
4079
+ return result;
4080
+ }
4081
+ function VectorToolbarTool(props) {
4082
+ const ctx = useVectorToolbarItemContext("VectorToolbar.Tool");
4083
+ const tool = {
4084
+ id: props.id,
4085
+ label: props.label,
4086
+ tooltipLabel: props.tooltipLabel,
4087
+ icon: props.icon,
4088
+ shortcutHint: props.shortcutHint,
4089
+ ariaLabel: props.ariaLabel
4090
+ };
4091
+ if (ctx.insideOverflow) {
4092
+ return renderOverflowToolButton(tool, {
4093
+ selectedId: ctx.selectedId,
4094
+ selectTool: ctx.selectTool,
4095
+ closeMenu: ctx.closeOverflow,
4096
+ renderToolButton: ctx.renderToolButton
4097
+ });
4098
+ }
4099
+ return renderInlineToolButton(tool, {
4100
+ selectedId: ctx.selectedId,
4101
+ selectTool: ctx.selectTool,
4102
+ density: ctx.density,
4103
+ buttonClassName: ctx.buttonClassName,
4104
+ activeButtonClassName: ctx.activeButtonClassName,
4105
+ renderToolButton: ctx.renderToolButton
4106
+ });
4107
+ }
4108
+ function VectorToolbarToolLock({
4109
+ className,
4110
+ divider = true
4111
+ } = {}) {
4112
+ const ctx = useVectorToolbarItemContext("VectorToolbar.ToolLock");
4113
+ const {
4114
+ toolLocked,
4115
+ setToolLocked,
4116
+ buttonClassName,
4117
+ activeButtonClassName,
4118
+ density
4119
+ } = ctx;
4120
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4121
+ /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
4122
+ /* @__PURE__ */ jsx(
4123
+ "button",
4124
+ {
4125
+ type: "button",
4126
+ "aria-pressed": toolLocked,
4127
+ "aria-label": toolLocked ? "Unlock tool" : "Lock tool",
4128
+ className: `${buttonClassName} ${toolLocked ? activeButtonClassName : ""} ${className ?? ""}`.trim(),
4129
+ style: {
4130
+ ...defaultButtonStyle,
4131
+ ...density === "compact" ? {
4132
+ minHeight: "40px",
4133
+ minWidth: "40px",
4134
+ padding: "6px 8px"
4135
+ } : {},
4136
+ ...toolLocked ? activeToggleStyle : inactiveToggleStyle
4137
+ },
4138
+ onPointerDown: preventMouseDefault2,
4139
+ onClick: () => setToolLocked(!toolLocked),
4140
+ children: /* @__PURE__ */ jsx(ToolLockGlyph, { locked: toolLocked })
4141
+ }
4142
+ ),
4143
+ /* @__PURE__ */ jsx("span", { className: "vector-toolbar-tip", "aria-hidden": "true", children: toolLocked ? "Tool lock on" : "Tool lock off" })
4144
+ ] }),
4145
+ divider ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: toolLockDividerStyle }) : null
4146
+ ] });
4147
+ }
4148
+ function VectorToolbarOverflow({
4149
+ children,
4150
+ ariaLabel = "More tools"
4151
+ }) {
4152
+ const ctx = useVectorToolbarItemContext("VectorToolbar.Overflow");
4153
+ const { open, setOpen, close, wrapRef, triggerRef, menuId } = useOverflowDropdown();
4154
+ if (ctx.insideOverflow) {
4155
+ throw new Error("VectorToolbar.Overflow cannot be nested.");
4156
+ }
4157
+ const activeChild = findActiveOverflowChild(children, ctx.selectedId);
4158
+ const triggerHighlighted = open || activeChild !== null;
4159
+ const customRenderer = Boolean(ctx.renderToolButton);
4160
+ const nestedCtx = {
4161
+ ...ctx,
4162
+ insideOverflow: true,
4163
+ closeOverflow: close
4164
+ };
4165
+ return /* @__PURE__ */ jsxs("div", { ref: wrapRef, className: "vector-toolbar-overflow-anchor", children: [
4166
+ /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
4167
+ /* @__PURE__ */ jsxs(
4168
+ "button",
4169
+ {
4170
+ ref: triggerRef,
4171
+ type: "button",
4172
+ id: `${menuId}-trigger`,
4173
+ "aria-haspopup": customRenderer ? "true" : "menu",
4174
+ "aria-expanded": open,
4175
+ "aria-controls": open ? `${menuId}-menu` : void 0,
4176
+ "aria-label": activeChild ? `${ariaLabel}: ${activeChild.label}` : ariaLabel,
4177
+ className: `vector-toolbar-overflow-trigger ${ctx.buttonClassName} ${triggerHighlighted ? ctx.activeButtonClassName : ""}`.trim(),
4178
+ style: {
4179
+ ...defaultButtonStyle,
4180
+ flexDirection: "row",
4181
+ minWidth: "48px",
4182
+ minHeight: ctx.density === "compact" ? "40px" : "44px",
4183
+ padding: "6px 8px",
4184
+ gap: "4px",
4185
+ ...triggerHighlighted ? activeToggleStyle : inactiveToggleStyle
4186
+ },
4187
+ onPointerDown: preventMouseDefault2,
4188
+ onClick: () => setOpen((o) => !o),
4189
+ onKeyDown: (e) => {
4190
+ if (e.key === "ArrowDown" && !open) {
4191
+ e.preventDefault();
4192
+ setOpen(true);
4193
+ }
4194
+ },
4195
+ children: [
4196
+ /* @__PURE__ */ jsx(
4197
+ "span",
4198
+ {
4199
+ style: {
4200
+ display: "flex",
4201
+ alignItems: "center",
4202
+ justifyContent: "center",
4203
+ flex: 1,
4204
+ minWidth: 0
4205
+ },
4206
+ children: activeChild?.icon ?? /* @__PURE__ */ jsx(Shapes, { ...icOverflow, "aria-hidden": true, strokeWidth: 2 })
4207
+ }
4208
+ ),
4209
+ /* @__PURE__ */ jsx(
4210
+ ChevronDown,
4211
+ {
4212
+ size: 14,
4213
+ strokeWidth: 2,
4214
+ "aria-hidden": true,
4215
+ style: {
4216
+ opacity: 0.75,
4217
+ flexShrink: 0,
4218
+ transform: open ? "rotate(180deg)" : void 0,
4219
+ transition: "transform 0.15s ease"
4220
+ }
4221
+ }
4222
+ )
4223
+ ]
4224
+ }
4225
+ ),
4226
+ /* @__PURE__ */ jsx("span", { className: "vector-toolbar-tip", "aria-hidden": "true", children: ariaLabel })
4227
+ ] }),
4228
+ open ? customRenderer ? /* @__PURE__ */ jsx(
4229
+ "section",
4230
+ {
4231
+ id: `${menuId}-menu`,
4232
+ "aria-label": ariaLabel,
4233
+ className: "vector-toolbar-overflow-panel",
4234
+ children: /* @__PURE__ */ jsx(VectorToolbarItemContext.Provider, { value: nestedCtx, children })
4235
+ }
4236
+ ) : /* @__PURE__ */ jsx(
4237
+ "div",
4238
+ {
4239
+ id: `${menuId}-menu`,
4240
+ role: "menu",
4241
+ "aria-label": ariaLabel,
4242
+ className: "vector-toolbar-overflow-panel",
4243
+ children: /* @__PURE__ */ jsx(VectorToolbarItemContext.Provider, { value: nestedCtx, children })
4244
+ }
4245
+ ) : null
4246
+ ] });
4247
+ }
3267
4248
  function ToolbarOverflowMenu({
3268
4249
  tools,
3269
4250
  selectedId,
@@ -3274,46 +4255,9 @@ function ToolbarOverflowMenu({
3274
4255
  renderToolButton,
3275
4256
  menuAriaLabel
3276
4257
  }) {
3277
- const [open, setOpen] = useState(false);
3278
- const wrapRef = useRef(null);
3279
- const triggerRef = useRef(null);
3280
- const menuId = useId();
4258
+ const { open, setOpen, close, wrapRef, triggerRef, menuId } = useOverflowDropdown();
3281
4259
  const activeInOverflow = tools.some((t) => t.id === selectedId);
3282
4260
  const activeTool = tools.find((t) => t.id === selectedId);
3283
- const close = useCallback(() => {
3284
- setOpen(false);
3285
- }, []);
3286
- const prevOpenRef = useRef(open);
3287
- useEffect(() => {
3288
- if (!open) {
3289
- return;
3290
- }
3291
- const onDocPointerDown = (e) => {
3292
- if (wrapRef.current?.contains(e.target)) {
3293
- return;
3294
- }
3295
- close();
3296
- };
3297
- const onKeyDown = (e) => {
3298
- if (e.key === "Escape") {
3299
- e.stopPropagation();
3300
- close();
3301
- triggerRef.current?.focus();
3302
- }
3303
- };
3304
- document.addEventListener("pointerdown", onDocPointerDown, true);
3305
- document.addEventListener("keydown", onKeyDown, true);
3306
- return () => {
3307
- document.removeEventListener("pointerdown", onDocPointerDown, true);
3308
- document.removeEventListener("keydown", onKeyDown, true);
3309
- };
3310
- }, [open, close]);
3311
- useEffect(() => {
3312
- if (prevOpenRef.current && !open) {
3313
- triggerRef.current?.focus({ preventScroll: true });
3314
- }
3315
- prevOpenRef.current = open;
3316
- }, [open]);
3317
4261
  const triggerHighlighted = open || activeInOverflow;
3318
4262
  return /* @__PURE__ */ jsxs("div", { ref: wrapRef, className: "vector-toolbar-overflow-anchor", children: [
3319
4263
  /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
@@ -3335,20 +4279,9 @@ function ToolbarOverflowMenu({
3335
4279
  minHeight: density === "compact" ? "40px" : "44px",
3336
4280
  padding: "6px 8px",
3337
4281
  gap: "4px",
3338
- ...triggerHighlighted ? {
3339
- background: "rgba(24,24,27,0.1)",
3340
- borderColor: "rgba(24,24,27,0.28)",
3341
- boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3342
- } : {
3343
- borderColor: "transparent",
3344
- boxShadow: "none"
3345
- }
3346
- },
3347
- onPointerDown: (ev) => {
3348
- if (ev.pointerType === "mouse") {
3349
- ev.preventDefault();
3350
- }
4282
+ ...triggerHighlighted ? activeToggleStyle : inactiveToggleStyle
3351
4283
  },
4284
+ onPointerDown: preventMouseDefault2,
3352
4285
  onClick: () => setOpen((o) => !o),
3353
4286
  onKeyDown: (e) => {
3354
4287
  if (e.key === "ArrowDown" && !open) {
@@ -3395,25 +4328,14 @@ function ToolbarOverflowMenu({
3395
4328
  id: `${menuId}-menu`,
3396
4329
  "aria-label": menuAriaLabel,
3397
4330
  className: "vector-toolbar-overflow-panel",
3398
- children: tools.map((tool) => {
3399
- const selected = tool.id === selectedId;
3400
- return /* @__PURE__ */ jsx(
3401
- "div",
3402
- {
3403
- className: "vector-toolbar-overflow-cell",
3404
- role: "presentation",
3405
- children: renderToolButton({
3406
- tool,
3407
- selected,
3408
- onSelect: () => {
3409
- onSelect(tool.id);
3410
- close();
3411
- }
3412
- })
3413
- },
3414
- tool.id
3415
- );
3416
- })
4331
+ children: tools.map(
4332
+ (tool) => renderOverflowToolButton(tool, {
4333
+ selectedId,
4334
+ selectTool: onSelect,
4335
+ closeMenu: close,
4336
+ renderToolButton
4337
+ })
4338
+ )
3417
4339
  }
3418
4340
  ) : null,
3419
4341
  open && !renderToolButton ? /* @__PURE__ */ jsx(
@@ -3423,69 +4345,19 @@ function ToolbarOverflowMenu({
3423
4345
  role: "menu",
3424
4346
  "aria-label": menuAriaLabel,
3425
4347
  className: "vector-toolbar-overflow-panel",
3426
- children: tools.map((tool, index) => {
3427
- const selected = tool.id === selectedId;
3428
- const label = tool.ariaLabel ?? tool.label;
3429
- const tooltipName = tool.tooltipLabel ?? tool.label;
3430
- return /* @__PURE__ */ jsxs("div", { className: "vector-toolbar-overflow-cell", children: [
3431
- /* @__PURE__ */ jsx(
3432
- "button",
3433
- {
3434
- type: "button",
3435
- role: "menuitemradio",
3436
- "aria-checked": selected,
3437
- tabIndex: index === 0 ? 0 : -1,
3438
- "aria-label": label,
3439
- ...tool.shortcutHint !== void 0 ? { "aria-keyshortcuts": tool.shortcutHint } : {},
3440
- style: overflowGridCellButtonStyle,
3441
- onPointerDown: (ev) => {
3442
- if (ev.pointerType === "mouse") {
3443
- ev.preventDefault();
3444
- }
3445
- },
3446
- onClick: () => {
3447
- onSelect(tool.id);
3448
- close();
3449
- },
3450
- children: /* @__PURE__ */ jsx(
3451
- "span",
3452
- {
3453
- style: {
3454
- display: "flex",
3455
- alignItems: "center",
3456
- justifyContent: "center",
3457
- lineHeight: 0
3458
- },
3459
- children: tool.icon ?? /* @__PURE__ */ jsx(
3460
- "span",
3461
- {
3462
- style: {
3463
- fontSize: "11px",
3464
- fontWeight: 700,
3465
- lineHeight: 1.1,
3466
- textAlign: "center",
3467
- maxWidth: "34px",
3468
- overflow: "hidden",
3469
- textOverflow: "ellipsis"
3470
- },
3471
- children: tool.label.slice(0, 3)
3472
- }
3473
- )
3474
- }
3475
- )
3476
- }
3477
- ),
3478
- /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-overflow-tip", "aria-hidden": "true", children: [
3479
- /* @__PURE__ */ jsx("span", { children: tooltipName }),
3480
- tool.shortcutHint !== void 0 ? /* @__PURE__ */ jsx("span", { className: "vector-toolbar-overflow-tip-shortcut", children: tool.shortcutHint }) : null
3481
- ] })
3482
- ] }, tool.id);
3483
- })
4348
+ children: tools.map(
4349
+ (tool, index) => renderOverflowToolButton(tool, {
4350
+ selectedId,
4351
+ selectTool: onSelect,
4352
+ closeMenu: close,
4353
+ tabIndex: index === 0 ? 0 : -1
4354
+ })
4355
+ )
3484
4356
  }
3485
4357
  ) : null
3486
4358
  ] });
3487
4359
  }
3488
- function VectorToolbar({
4360
+ function VectorToolbarComponent({
3489
4361
  value,
3490
4362
  onChange,
3491
4363
  tools,
@@ -3507,23 +4379,63 @@ function VectorToolbar({
3507
4379
  const pluginContext = useContext(CanvuPluginContext);
3508
4380
  const runtimeTools = pluginContext?.resolvedTools;
3509
4381
  const resolvedTools = tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS;
3510
- const ctx = {
3511
- tools: resolvedTools,
3512
- selectedId: value,
3513
- selectTool: onChange,
3514
- toolLocked,
3515
- setToolLocked: (locked) => onToolLockedChange?.(locked)
3516
- };
3517
- if (children) {
4382
+ if (typeof children === "function") {
4383
+ const ctx = {
4384
+ tools: resolvedTools,
4385
+ selectedId: value,
4386
+ selectTool: onChange,
4387
+ toolLocked,
4388
+ setToolLocked: (locked) => onToolLockedChange?.(locked)
4389
+ };
3518
4390
  return /* @__PURE__ */ jsx("div", { className, style, children: children(ctx) });
3519
4391
  }
4392
+ const flexDir = orientation === "vertical" ? "column" : "row";
4393
+ const toolbarClass = className.trim() === "" ? "vector-toolbar" : `vector-toolbar ${className.trim()}`;
4394
+ if (children !== void 0 && children !== null) {
4395
+ const itemCtx = {
4396
+ selectedId: value,
4397
+ selectTool: onChange,
4398
+ toolLocked,
4399
+ setToolLocked: (locked) => onToolLockedChange?.(locked),
4400
+ density,
4401
+ buttonClassName,
4402
+ activeButtonClassName,
4403
+ renderToolButton,
4404
+ insideOverflow: false,
4405
+ closeOverflow: noop2,
4406
+ orientation
4407
+ };
4408
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4409
+ /* @__PURE__ */ jsx("style", { children: TOOLBAR_TOOLTIP_CSS }),
4410
+ /* @__PURE__ */ jsx("style", { children: OVERFLOW_MENU_CSS }),
4411
+ /* @__PURE__ */ jsx("style", { children: TOOLBAR_MOBILE_CSS }),
4412
+ /* @__PURE__ */ jsx(
4413
+ "div",
4414
+ {
4415
+ role: "toolbar",
4416
+ "data-orientation": orientation,
4417
+ "aria-label": ariaLabel,
4418
+ "aria-orientation": orientation === "vertical" ? "vertical" : "horizontal",
4419
+ className: toolbarClass,
4420
+ style: { ...toolbarStyles, flexDirection: flexDir, ...style },
4421
+ children: /* @__PURE__ */ jsx(VectorToolbarItemContext.Provider, { value: itemCtx, children })
4422
+ }
4423
+ )
4424
+ ] });
4425
+ }
3520
4426
  const { primary: primaryTools, overflow: overflowTools } = splitToolbarTools(
3521
4427
  resolvedTools,
3522
4428
  overflowToolIds
3523
4429
  );
3524
- const flexDir = orientation === "vertical" ? "column" : "row";
3525
4430
  const showOverflowMenu = overflowTools.length > 0;
3526
- const toolbarClass = className.trim() === "" ? "vector-toolbar" : `vector-toolbar ${className.trim()}`;
4431
+ const inlineCtx = {
4432
+ selectedId: value,
4433
+ selectTool: onChange,
4434
+ density,
4435
+ buttonClassName,
4436
+ activeButtonClassName,
4437
+ renderToolButton
4438
+ };
3527
4439
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3528
4440
  /* @__PURE__ */ jsx("style", { children: TOOLBAR_TOOLTIP_CSS }),
3529
4441
  /* @__PURE__ */ jsx("style", { children: OVERFLOW_MENU_CSS }),
@@ -3554,20 +4466,9 @@ function VectorToolbar({
3554
4466
  minWidth: "40px",
3555
4467
  padding: "6px 8px"
3556
4468
  } : {},
3557
- ...toolLocked ? {
3558
- background: "rgba(24,24,27,0.1)",
3559
- borderColor: "rgba(24,24,27,0.28)",
3560
- boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3561
- } : {
3562
- borderColor: "transparent",
3563
- boxShadow: "none"
3564
- }
3565
- },
3566
- onPointerDown: (ev) => {
3567
- if (ev.pointerType === "mouse") {
3568
- ev.preventDefault();
3569
- }
4469
+ ...toolLocked ? activeToggleStyle : inactiveToggleStyle
3570
4470
  },
4471
+ onPointerDown: preventMouseDefault2,
3571
4472
  onClick: () => onToolLockedChange?.(!toolLocked),
3572
4473
  children: /* @__PURE__ */ jsx(ToolLockGlyph, { locked: toolLocked })
3573
4474
  }
@@ -3576,89 +4477,7 @@ function VectorToolbar({
3576
4477
  ] }),
3577
4478
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: toolLockDividerStyle })
3578
4479
  ] }) : null,
3579
- primaryTools.map((tool) => {
3580
- const selected = tool.id === value;
3581
- const label = tool.ariaLabel ?? tool.label;
3582
- if (renderToolButton) {
3583
- return /* @__PURE__ */ jsx("div", { children: renderToolButton({
3584
- tool,
3585
- selected,
3586
- onSelect: () => onChange(tool.id)
3587
- }) }, tool.id);
3588
- }
3589
- const tooltipName = tool.tooltipLabel ?? tool.label;
3590
- const tooltipText = tool.shortcutHint !== void 0 ? `${tooltipName} \xB7 ${tool.shortcutHint}` : tooltipName;
3591
- const labelOnly = tool.icon == null;
3592
- const labelTextStyle = labelOnly ? {
3593
- fontSize: "12px",
3594
- fontWeight: 600,
3595
- letterSpacing: "0.02em",
3596
- lineHeight: 1.2,
3597
- WebkitFontSmoothing: "antialiased",
3598
- MozOsxFontSmoothing: "grayscale",
3599
- textRendering: "geometricPrecision"
3600
- } : {};
3601
- return /* @__PURE__ */ jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
3602
- /* @__PURE__ */ jsxs(
3603
- "button",
3604
- {
3605
- type: "button",
3606
- "aria-pressed": selected,
3607
- "aria-label": label,
3608
- ...tool.shortcutHint !== void 0 ? { "aria-keyshortcuts": tool.shortcutHint } : {},
3609
- className: `${buttonClassName} ${selected ? activeButtonClassName : ""}`.trim(),
3610
- style: {
3611
- ...defaultButtonStyle,
3612
- ...density === "compact" ? {
3613
- minHeight: "40px",
3614
- minWidth: labelOnly ? "52px" : "40px",
3615
- padding: "6px 8px"
3616
- } : {},
3617
- ...selected ? {
3618
- background: "rgba(24,24,27,0.1)",
3619
- borderColor: "rgba(24,24,27,0.28)",
3620
- boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3621
- } : {
3622
- borderColor: "transparent",
3623
- boxShadow: "none"
3624
- }
3625
- },
3626
- onPointerDown: (ev) => {
3627
- if (ev.pointerType === "mouse") {
3628
- ev.preventDefault();
3629
- }
3630
- },
3631
- onClick: () => onChange(tool.id),
3632
- children: [
3633
- /* @__PURE__ */ jsx(
3634
- "span",
3635
- {
3636
- style: {
3637
- display: "flex",
3638
- alignItems: "center",
3639
- justifyContent: "center"
3640
- },
3641
- children: tool.icon ?? /* @__PURE__ */ jsx("span", { style: labelTextStyle, children: tool.label })
3642
- }
3643
- ),
3644
- density === "comfortable" && tool.icon ? /* @__PURE__ */ jsx(
3645
- "span",
3646
- {
3647
- style: {
3648
- lineHeight: 1,
3649
- maxWidth: "56px",
3650
- overflow: "hidden",
3651
- textOverflow: "ellipsis"
3652
- },
3653
- children: tool.label
3654
- }
3655
- ) : null
3656
- ]
3657
- }
3658
- ),
3659
- /* @__PURE__ */ jsx("span", { className: "vector-toolbar-tip", "aria-hidden": "true", children: tooltipText })
3660
- ] }, tool.id);
3661
- }),
4480
+ primaryTools.map((tool) => renderInlineToolButton(tool, inlineCtx)),
3662
4481
  showOverflowMenu && orientation === "horizontal" ? /* @__PURE__ */ jsx(
3663
4482
  "span",
3664
4483
  {
@@ -3685,6 +4504,51 @@ function VectorToolbar({
3685
4504
  )
3686
4505
  ] });
3687
4506
  }
4507
+ function noop2() {
4508
+ }
4509
+ function makeToolBinding(id) {
4510
+ const def = DEFAULT_VECTOR_TOOLS.find((t) => t.id === id);
4511
+ if (!def) {
4512
+ throw new Error(`No default tool with id "${id}"`);
4513
+ }
4514
+ const Component = ((props) => /* @__PURE__ */ jsx(VectorToolbarTool, { ...def, className: props?.className }));
4515
+ Component.toolId = id;
4516
+ Component.displayName = `VectorToolbar.${def.label}`;
4517
+ return Component;
4518
+ }
4519
+ var HandBinding = makeToolBinding("hand");
4520
+ var SelectBinding = makeToolBinding("select");
4521
+ var RectBinding = makeToolBinding("rect");
4522
+ var EllipseBinding = makeToolBinding("ellipse");
4523
+ var LineBinding = makeToolBinding("line");
4524
+ var ArrowBinding = makeToolBinding("arrow");
4525
+ var DrawBinding = makeToolBinding("draw");
4526
+ var MarkerBinding = makeToolBinding("marker");
4527
+ var LaserBinding = makeToolBinding("laser");
4528
+ var EraserBinding = makeToolBinding("eraser");
4529
+ var TextBinding = makeToolBinding("text");
4530
+ var ImageBinding = makeToolBinding("image");
4531
+ VectorToolbarComponent.displayName = "VectorToolbar";
4532
+ var VectorToolbar = Object.assign(
4533
+ VectorToolbarComponent,
4534
+ {
4535
+ Tool: VectorToolbarTool,
4536
+ ToolLock: VectorToolbarToolLock,
4537
+ Overflow: VectorToolbarOverflow,
4538
+ Hand: HandBinding,
4539
+ Select: SelectBinding,
4540
+ Rect: RectBinding,
4541
+ Ellipse: EllipseBinding,
4542
+ Line: LineBinding,
4543
+ Arrow: ArrowBinding,
4544
+ Draw: DrawBinding,
4545
+ Marker: MarkerBinding,
4546
+ Laser: LaserBinding,
4547
+ Eraser: EraserBinding,
4548
+ Text: TextBinding,
4549
+ Image: ImageBinding
4550
+ }
4551
+ );
3688
4552
 
3689
4553
  // src/camera/camera.ts
3690
4554
  init_rect();
@@ -5054,30 +5918,6 @@ var SvgVectorRenderer = class {
5054
5918
  }
5055
5919
  };
5056
5920
 
5057
- // src/scene/clone-item.ts
5058
- init_shape_builders();
5059
- function cloneVectorSceneItemWithNewId(item) {
5060
- const id = createShapeId();
5061
- const copy = JSON.parse(JSON.stringify(item));
5062
- let next = { ...copy, id };
5063
- if (next.toolKind === "arrow" && next.line) {
5064
- next = {
5065
- ...next,
5066
- childrenSvg: buildArrowSvg(id, next.line, resolveStrokeStyle(next))
5067
- };
5068
- }
5069
- if (next.toolKind === "text" && next.text !== void 0) {
5070
- return rebuildItemSvg(next);
5071
- }
5072
- if (next.toolKind === "custom" && next.customInnerSvg && next.customIntrinsicSize) {
5073
- return rebuildItemSvg(next);
5074
- }
5075
- return next;
5076
- }
5077
- function cloneVectorSceneItemsWithNewIds(items) {
5078
- return items.map(cloneVectorSceneItemWithNewId);
5079
- }
5080
-
5081
5921
  // src/scene/scene.ts
5082
5922
  var VectorScene = class {
5083
5923
  items = [];
@@ -7673,9 +8513,7 @@ var VectorViewport = forwardRef(
7673
8513
  return;
7674
8514
  }
7675
8515
  if (!e.shiftKey && cur.length > 0) {
7676
- const selectedUnlockedItems = cur.map((id) => resolved.find((it) => it.id === id)).filter(
7677
- (it) => it != null && !it.locked
7678
- );
8516
+ const selectedUnlockedItems = cur.map((id) => resolved.find((it) => it.id === id)).filter((it) => it != null && !it.locked);
7679
8517
  if (selectedUnlockedItems.some(
7680
8518
  (it) => pointInSelectedItemBounds(it, worldX, worldY)
7681
8519
  )) {
@@ -8757,110 +9595,7 @@ var VectorViewport = forwardRef(
8757
9595
  }
8758
9596
  );
8759
9597
  VectorViewport.displayName = "VectorViewport";
8760
- var DEFAULT_SHELL_STYLE = {
8761
- position: "absolute",
8762
- left: 12,
8763
- bottom: 140,
8764
- zIndex: 22
8765
- };
8766
- var layoutStyle = {
8767
- display: "flex",
8768
- flexDirection: "column",
8769
- gap: 4,
8770
- /* Let clicks pass through the flex gaps; buttons opt in below. */
8771
- pointerEvents: "none"
8772
- };
8773
- var btnStyle2 = {
8774
- pointerEvents: "auto",
8775
- width: 36,
8776
- height: 36,
8777
- display: "inline-flex",
8778
- alignItems: "center",
8779
- justifyContent: "center",
8780
- borderRadius: 8,
8781
- border: "1px solid rgba(0,0,0,0.12)",
8782
- background: "rgba(255,255,255,0.96)",
8783
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
8784
- cursor: "pointer",
8785
- fontSize: 18,
8786
- lineHeight: 1,
8787
- fontWeight: 600,
8788
- color: "#18181b",
8789
- padding: 0,
8790
- outline: "none",
8791
- WebkitTapHighlightColor: "transparent"
8792
- };
8793
- var labelStyle3 = {
8794
- pointerEvents: "none",
8795
- fontSize: 10,
8796
- fontWeight: 600,
8797
- color: "#52525b",
8798
- textAlign: "center",
8799
- userSelect: "none",
8800
- padding: "2px 0"
8801
- };
8802
- var fieldsetReset = {
8803
- border: "none",
8804
- margin: 0,
8805
- padding: 0,
8806
- minWidth: 0
8807
- };
8808
- function ViewportZoomControls({
8809
- zoomPercent,
8810
- onZoomIn,
8811
- onZoomOut,
8812
- className = "",
8813
- position,
8814
- inset = 12,
8815
- zIndex = 22,
8816
- style
8817
- }) {
8818
- const anchorStyle = position !== void 0 ? getBoardPositionStyle(position, inset, zIndex) : DEFAULT_SHELL_STYLE;
8819
- return /* @__PURE__ */ jsxs(
8820
- "fieldset",
8821
- {
8822
- className,
8823
- "data-position": position ?? "default",
8824
- "aria-label": "Zoom controls",
8825
- style: { ...anchorStyle, ...layoutStyle, ...fieldsetReset, ...style },
8826
- children: [
8827
- /* @__PURE__ */ jsx(
8828
- "button",
8829
- {
8830
- type: "button",
8831
- style: btnStyle2,
8832
- "aria-label": "Zoom in",
8833
- title: "Zoom in",
8834
- onPointerDown: (e) => {
8835
- if (e.pointerType === "mouse") e.preventDefault();
8836
- },
8837
- onClick: onZoomIn,
8838
- children: "+"
8839
- }
8840
- ),
8841
- /* @__PURE__ */ jsx(
8842
- "button",
8843
- {
8844
- type: "button",
8845
- style: btnStyle2,
8846
- "aria-label": "Zoom out",
8847
- title: "Zoom out",
8848
- onPointerDown: (e) => {
8849
- if (e.pointerType === "mouse") e.preventDefault();
8850
- },
8851
- onClick: onZoomOut,
8852
- children: "\u2212"
8853
- }
8854
- ),
8855
- /* @__PURE__ */ jsxs("span", { style: labelStyle3, "aria-hidden": true, children: [
8856
- zoomPercent,
8857
- "%"
8858
- ] })
8859
- ]
8860
- }
8861
- );
8862
- }
8863
9598
 
8864
- export { CanvuChromeContext, CanvuPluginContext, DEFAULT_OVERFLOW_TOOL_IDS, DEFAULT_VECTOR_CANVAS_STORAGE_KEY, DEFAULT_VECTOR_TOOLS, IconArrow, IconDraw, IconEllipse, IconHand, IconImage, IconLaser, IconLine, IconRect, IconSelect, IconText, NavMenu, ShapeContextMenu, VectorCanvas, VectorCanvasBody, VectorCanvasHeader, VectorCanvasMain, VectorCanvasRoot, VectorCanvasToolbar, VectorCanvasViewportSurface, VectorSelectionInspector, VectorToolbar, VectorViewport, ViewportZoomControls, createCanvuPlugin, createIndexedDbPersistenceAdapter, createLocalStoragePersistenceAdapter, createNoopPersistenceAdapter, createToolPlugin, cursorForVectorToolId, getBoardPositionStyle, hydrateSceneItemsWithAssets, ingestAssetFilesToSceneItems, useCanvuChromeContext, useCanvuDocumentContext, useCanvuPluginContext, useCanvuPluginContribution, useCanvuResolvedTools, useCanvuViewportContext, useVectorCanvasDocument };
9599
+ export { CanvuChromeContext, CanvuPluginContext, DEFAULT_OVERFLOW_TOOL_IDS, DEFAULT_VECTOR_CANVAS_STORAGE_KEY, DEFAULT_VECTOR_TOOLS, IconArrow, IconDraw, IconEllipse, IconHand, IconImage, IconLaser, IconLine, IconRect, IconSelect, IconText, ImagesMenu, NavMenu, ShapeContextMenu, VectorCanvas, VectorCanvasBody, VectorCanvasHeader, VectorCanvasMain, VectorCanvasRoot, VectorCanvasToolbar, VectorCanvasViewportSurface, VectorSelectionInspector, VectorToolbar, VectorViewport, createCanvuPlugin, createIndexedDbPersistenceAdapter, createLocalStoragePersistenceAdapter, createNoopPersistenceAdapter, createToolPlugin, cursorForVectorToolId, getBoardPositionStyle, hydrateSceneItemsWithAssets, ingestAssetFilesToSceneItems, useCanvuChromeContext, useCanvuDocumentContext, useCanvuPluginContext, useCanvuPluginContribution, useCanvuResolvedTools, useCanvuViewportContext, useVectorCanvasDocument };
8865
9600
  //# sourceMappingURL=react.js.map
8866
9601
  //# sourceMappingURL=react.js.map