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