canvu-react 0.3.16 → 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,1452 +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
- ),
1677
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1678
- strokeWidth2,
1679
- "px"
1680
- ] })
1681
- ] }),
1682
- showMarkerOpacity2 && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1683
- "Opacidade (marcador)",
1684
- /* @__PURE__ */ jsxRuntime.jsx(
1685
- "input",
1686
- {
1687
- type: "range",
1688
- min: 10,
1689
- max: 100,
1690
- value: opacityPct2,
1691
- onChange: (e) => {
1692
- const v = Number(e.target.value) / 100;
1693
- onChange({
1694
- stroke: hex2,
1695
- strokeWidth: strokeWidth2,
1696
- strokeOpacity: v
1697
- });
1698
- }
1699
- }
1700
- ),
1701
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1702
- opacityPct2,
1703
- "%"
1704
- ] })
2352
+ )
1705
2353
  ] })
1706
- ]
1707
- }
1708
- );
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 };
1709
2363
  }
1710
- const stylable = items.filter(
1711
- (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
1712
- );
1713
- if (stylable.length === 0) return null;
1714
- const first = stylable[0];
1715
- if (!first) return null;
1716
- const allSameStroke = stylable.every(
1717
- (it) => (it.stroke ?? "#2563eb") === (first.stroke ?? "#2563eb")
1718
- );
1719
- const allSameWidth = stylable.every(
1720
- (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
1721
- );
1722
- const stroke = first.stroke ?? "#2563eb";
1723
- const strokeWidth = first.strokeWidth ?? 2;
1724
- const hex = normalizeHex(stroke);
1725
- const markers = stylable.filter((it) => it.toolKind === "marker");
1726
- const showMarkerOpacity = markers.length > 0;
1727
- const firstMarker = markers[0];
1728
- const allSameMarkerOpacity = markers.length > 0 && markers.every(
1729
- (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
+ }
1730
2430
  );
1731
- 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;
1732
2443
  return /* @__PURE__ */ jsxRuntime.jsxs(
1733
- "section",
2444
+ "span",
1734
2445
  {
1735
- "data-slot": "vector-selection-inspector",
1736
- "data-position": position,
1737
2446
  className,
1738
- "aria-label": "Estilo da sele\xE7\xE3o",
1739
- style: shell,
2447
+ style: { display: "inline-flex", alignItems: "center", ...style },
1740
2448
  children: [
1741
- stylable.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
1742
- stylable.length,
1743
- " 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
+ "%"
1744
2464
  ] }),
1745
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1746
- "Cor",
1747
- !allSameStroke && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: [
1748
- " ",
1749
- "(valores misturados \u2014 novo valor aplica a todos)"
1750
- ] }),
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: [
1751
2521
  /* @__PURE__ */ jsxRuntime.jsx(
1752
- "input",
2522
+ NavMenuZoomControls,
1753
2523
  {
1754
- type: "color",
1755
- value: hex,
1756
- onChange: (e) => onChange({
1757
- stroke: e.target.value,
1758
- strokeWidth
1759
- }),
1760
- style: {
1761
- width: "100%",
1762
- height: 32,
1763
- padding: 0,
1764
- border: "none",
1765
- cursor: "pointer",
1766
- background: "transparent"
1767
- }
2524
+ zoomPercent: zoomPercentProp,
2525
+ onZoomIn: onZoomInProp,
2526
+ onZoomOut: onZoomOutProp
1768
2527
  }
1769
- )
1770
- ] }),
1771
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1772
- "Grossura",
1773
- !allSameWidth && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
2528
+ ),
1774
2529
  /* @__PURE__ */ jsxRuntime.jsx(
1775
- "input",
2530
+ NavMenuUndoRedo,
1776
2531
  {
1777
- type: "range",
1778
- min: 1,
1779
- max: 48,
1780
- value: strokeWidth,
1781
- onChange: (e) => onChange({
1782
- stroke: hex,
1783
- strokeWidth: Number(e.target.value)
1784
- })
2532
+ onUndo: onUndoProp,
2533
+ onRedo: onRedoProp,
2534
+ canUndo: canUndoProp,
2535
+ canRedo: canRedoProp
1785
2536
  }
1786
2537
  ),
1787
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1788
- strokeWidth,
1789
- "px"
1790
- ] })
1791
- ] }),
1792
- showMarkerOpacity && firstMarker && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1793
- "Opacidade (marcador)",
1794
- !allSameMarkerOpacity && markers.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
1795
2538
  /* @__PURE__ */ jsxRuntime.jsx(
1796
- "input",
2539
+ NavMenuMinimap,
1797
2540
  {
1798
- type: "range",
1799
- min: 10,
1800
- max: 100,
1801
- value: opacityPct,
1802
- onChange: (e) => {
1803
- const v = Number(e.target.value) / 100;
1804
- onChange({
1805
- stroke: hex,
1806
- strokeWidth,
1807
- strokeOpacity: v
1808
- });
1809
- }
2541
+ camera: cameraProp,
2542
+ viewportWidth: viewportWidthProp,
2543
+ viewportHeight: viewportHeightProp,
2544
+ items: itemsProp,
2545
+ onRequestRender: onRequestRenderProp
1810
2546
  }
1811
- ),
1812
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1813
- opacityPct,
1814
- "%"
1815
- ] })
1816
- ] })
1817
- ]
2547
+ )
2548
+ ] }) }),
2549
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setSlotEl, style: minimapSlotStyle })
2550
+ ] })
1818
2551
  }
1819
2552
  );
1820
2553
  }
1821
- function getBoardPositionStyle(position, inset = 12, zIndex = 40) {
1822
- const base2 = { position: "absolute", zIndex };
1823
- switch (position) {
1824
- case "fill":
1825
- return { ...base2, inset };
1826
- case "top-left":
1827
- case "left-top":
1828
- return { ...base2, top: inset, left: inset };
1829
- case "top-center":
1830
- return {
1831
- ...base2,
1832
- top: inset,
1833
- left: "50%",
1834
- transform: "translateX(-50%)"
1835
- };
1836
- case "top-right":
1837
- case "right-top":
1838
- return { ...base2, top: inset, right: inset };
1839
- case "bottom-left":
1840
- case "left-bottom":
1841
- return { ...base2, bottom: inset, left: inset };
1842
- case "bottom-center":
1843
- return {
1844
- ...base2,
1845
- bottom: inset,
1846
- left: "50%",
1847
- transform: "translateX(-50%)"
1848
- };
1849
- case "bottom-right":
1850
- case "right-bottom":
1851
- return { ...base2, bottom: inset, right: inset };
1852
- case "left-center":
1853
- return {
1854
- ...base2,
1855
- left: inset,
1856
- top: "50%",
1857
- transform: "translateY(-50%)"
1858
- };
1859
- case "right-center":
1860
- return {
1861
- ...base2,
1862
- right: inset,
1863
- top: "50%",
1864
- transform: "translateY(-50%)"
1865
- };
1866
- case "center":
1867
- return {
1868
- ...base2,
1869
- top: "50%",
1870
- left: "50%",
1871
- transform: "translate(-50%, -50%)"
1872
- };
1873
- default:
1874
- return base2;
1875
- }
1876
- }
1877
- var rootStyle = {
1878
- display: "flex",
1879
- flexDirection: "column",
1880
- height: "100%",
1881
- minHeight: 0,
1882
- width: "100%"
1883
- };
1884
- var headerStyle = {
1885
- flexShrink: 0,
1886
- display: "flex",
1887
- flexWrap: "wrap",
1888
- alignItems: "center",
1889
- gap: "0.75rem",
1890
- padding: "0.75rem 1rem",
1891
- borderBottom: "1px solid #e4e4e7",
1892
- background: "#fff",
1893
- fontSize: "0.875rem"
1894
- };
1895
- var bodyStyle = {
1896
- flex: 1,
1897
- minHeight: 0,
1898
- display: "flex",
1899
- flexDirection: "column"
1900
- };
1901
- var mainStyle = {
1902
- flex: 1,
1903
- minHeight: 0,
1904
- position: "relative",
1905
- display: "flex",
1906
- flexDirection: "column"
1907
- };
1908
- var viewportSurfaceStyle = {
1909
- flex: 1,
1910
- minHeight: 0,
1911
- position: "relative",
1912
- width: "100%",
1913
- alignSelf: "stretch",
1914
- background: "#fff",
1915
- touchAction: "none"
1916
- };
1917
- function mergeStyle(base2, style) {
1918
- 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
+ });
1919
2577
  }
1920
- 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
+ }
1921
2628
  return {
1922
- ...getBoardPositionStyle(position, inset, zIndex),
1923
- 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
+ }
1924
2672
  };
1925
2673
  }
1926
- function VectorCanvasRoot({
1927
- children,
1928
- className,
1929
- style
1930
- }) {
1931
- return /* @__PURE__ */ jsxRuntime.jsx(
1932
- "div",
1933
- {
1934
- "data-slot": "vector-canvas-root",
1935
- className,
1936
- style: mergeStyle(rootStyle, style),
1937
- 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();
1938
2709
  }
1939
- );
2710
+ };
1940
2711
  }
1941
- function VectorCanvasHeader({
1942
- children,
1943
- className,
1944
- style
1945
- }) {
1946
- return /* @__PURE__ */ jsxRuntime.jsx(
1947
- "header",
1948
- {
1949
- "data-slot": "vector-canvas-header",
1950
- className,
1951
- style: mergeStyle(headerStyle, style),
1952
- children
1953
- }
1954
- );
2712
+ function createNoopPersistenceAdapter() {
2713
+ return {
2714
+ load: () => Promise.resolve(null),
2715
+ save: () => Promise.resolve()
2716
+ };
1955
2717
  }
1956
- function VectorCanvasBody({
1957
- children,
1958
- className,
1959
- style
1960
- }) {
1961
- return /* @__PURE__ */ jsxRuntime.jsx(
1962
- "div",
1963
- {
1964
- "data-slot": "vector-canvas-body",
1965
- className,
1966
- style: mergeStyle(bodyStyle, style),
1967
- children
1968
- }
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]
1969
2762
  );
1970
- }
1971
- function VectorCanvasMain({
1972
- children,
1973
- className,
1974
- style
1975
- }) {
1976
- return /* @__PURE__ */ jsxRuntime.jsx(
1977
- "div",
1978
- {
1979
- "data-slot": "vector-canvas-main",
1980
- className,
1981
- style: mergeStyle(mainStyle, style),
1982
- children
1983
- }
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
+ []
1984
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
+ };
1985
2787
  }
1986
- function VectorCanvasViewportSurface({
1987
- children,
1988
- className,
1989
- style
1990
- }) {
1991
- return /* @__PURE__ */ jsxRuntime.jsx(
1992
- "div",
1993
- {
1994
- "data-slot": "vector-canvas-viewport-surface",
1995
- className,
1996
- style: mergeStyle(viewportSurfaceStyle, style),
1997
- children
1998
- }
1999
- );
2788
+ var CanvuPluginContext = react.createContext(
2789
+ null
2790
+ );
2791
+ function createCanvuPlugin(plugin) {
2792
+ return plugin;
2000
2793
  }
2001
- function VectorCanvasToolbar({
2002
- children,
2003
- className,
2004
- style,
2005
- position = "bottom-center",
2006
- inset = 12,
2007
- zIndex = 30
2008
- }) {
2009
- const base2 = {
2010
- ...getBoardPositionStyle(position, inset, zIndex),
2011
- display: "flex",
2012
- justifyContent: "center",
2013
- alignItems: "center",
2014
- maxWidth: "calc(100% - 24px)",
2015
- pointerEvents: "none"
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
2016
2812
  };
2017
- return /* @__PURE__ */ jsxRuntime.jsx(
2018
- "div",
2019
- {
2020
- "data-slot": "vector-canvas-toolbar",
2021
- "data-position": position,
2022
- className,
2023
- style: mergeStyle(base2, style),
2024
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: "auto" }, children })
2025
- }
2026
- );
2027
2813
  }
2028
- function VectorCanvasSpace({
2029
- children,
2030
- className,
2031
- style,
2032
- position = "top-right",
2033
- inset = 12,
2034
- zIndex = 40,
2035
- 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
2036
2829
  }) {
2037
- return /* @__PURE__ */ jsxRuntime.jsx(
2038
- "div",
2039
- {
2040
- "data-slot": `vector-canvas-space-${position}`,
2041
- className,
2042
- style: mergeStyle(vectorCanvasSpaceStyle(position, inset, zIndex), style),
2043
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: contentPointerEvents }, children })
2044
- }
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]
2045
2842
  );
2843
+ useCanvuPluginContribution(pluginId, contribution);
2844
+ return null;
2046
2845
  }
2047
- var VectorCanvas = {
2048
- Root: VectorCanvasRoot,
2049
- Header: VectorCanvasHeader,
2050
- Body: VectorCanvasBody,
2051
- Main: VectorCanvasMain,
2052
- ViewportSurface: VectorCanvasViewportSurface,
2053
- Toolbar: VectorCanvasToolbar,
2054
- Space: VectorCanvasSpace,
2055
- NavMenu,
2056
- SelectionInspector: VectorSelectionInspector
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"
2057
2876
  };
2058
- var MINIMAP_W = 180;
2059
- var MINIMAP_H = 120;
2060
- var PADDING = 12;
2061
- var BORDER_RADIUS = 8;
2062
- var BG = "#ffffff";
2063
- var BORDER_COLOR = "rgba(0,0,0,0.12)";
2064
- var ITEM_FILL = "rgba(0,0,0,0.08)";
2065
- var ITEM_STROKE = "rgba(0,0,0,0.15)";
2066
- var VIEWPORT_FILL = "rgba(59,130,246,0.08)";
2067
- var VIEWPORT_STROKE = "#3b82f6";
2068
- var btnStyle = {
2069
- pointerEvents: "auto",
2070
- width: 36,
2071
- height: 36,
2072
- display: "inline-flex",
2073
- alignItems: "center",
2074
- justifyContent: "center",
2075
- cursor: "pointer",
2076
- fontSize: 18,
2077
- lineHeight: 1,
2078
- color: "#18181b",
2079
- padding: 0,
2080
- border: "none",
2081
- outline: "none",
2082
- background: "none",
2083
- WebkitTapHighlightColor: "transparent"
2084
- };
2085
- var chevronBtnStyle = {
2086
- pointerEvents: "auto",
2087
- width: 24,
2088
- height: 36,
2089
- display: "inline-flex",
2090
- alignItems: "center",
2091
- justifyContent: "center",
2092
- cursor: "pointer",
2093
- fontSize: 12,
2094
- lineHeight: 1,
2095
- color: "#52525b",
2096
- padding: 0,
2877
+ var itemStyle = {
2878
+ display: "block",
2879
+ width: "100%",
2880
+ textAlign: "left",
2881
+ padding: "8px 12px",
2882
+ margin: 0,
2097
2883
  border: "none",
2098
- outline: "none",
2099
- background: "none",
2100
- WebkitTapHighlightColor: "transparent"
2101
- };
2102
- var undoBtnStyle = {
2103
- ...chevronBtnStyle,
2104
- width: 30,
2105
- opacity: 1,
2106
- color: "#18181b"
2107
- };
2108
- var labelStyle2 = {
2109
- fontSize: 10,
2110
- fontWeight: 600,
2111
- color: "#52525b",
2112
- textAlign: "center",
2113
- userSelect: "none",
2114
- padding: "2px 0"
2115
- };
2116
- var panelLayoutStyle = {
2117
- display: "flex",
2118
- flexDirection: "column",
2119
- alignItems: "flex-start",
2120
- gap: 4,
2121
- touchAction: "none",
2122
- pointerEvents: "none",
2123
- userSelect: "none"
2884
+ borderRadius: 4,
2885
+ background: "transparent",
2886
+ cursor: "pointer"
2124
2887
  };
2125
- var innerStyle = {
2126
- pointerEvents: "auto",
2127
- display: "flex",
2128
- flexDirection: "row",
2129
- alignItems: "center",
2130
- gap: 4,
2131
- background: BG,
2132
- borderRadius: BORDER_RADIUS,
2133
- border: `1px solid ${BORDER_COLOR}`,
2134
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
2135
- padding: 4
2888
+ var dividerStyle = {
2889
+ height: 1,
2890
+ margin: "4px 8px",
2891
+ background: "#e2e8f0"
2136
2892
  };
2137
- function NavMenu({
2138
- camera: cameraProp,
2139
- viewportWidth: viewportWidthProp,
2140
- viewportHeight: viewportHeightProp,
2141
- items: itemsProp,
2142
- zoomPercent: zoomPercentProp,
2143
- onZoomIn: onZoomInProp,
2144
- onZoomOut: onZoomOutProp,
2145
- onUndo: onUndoProp,
2146
- onRedo: onRedoProp,
2147
- canUndo: canUndoProp,
2148
- canRedo: canRedoProp,
2149
- onRequestRender: onRequestRenderProp,
2150
- position = "bottom-left",
2151
- inset = 12,
2152
- zIndex = 23,
2153
- className,
2154
- 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
2155
2907
  }) {
2156
- const ctx = useCanvuChromeContext();
2157
- const camera = cameraProp ?? ctx?.camera ?? null;
2158
- const viewportWidth = viewportWidthProp ?? ctx?.viewportWidth ?? 0;
2159
- const viewportHeight = viewportHeightProp ?? ctx?.viewportHeight ?? 0;
2160
- const items = itemsProp ?? ctx?.items ?? [];
2161
- const zoomPercent = zoomPercentProp ?? ctx?.zoomPercent ?? 100;
2162
- const onZoomIn = onZoomInProp ?? ctx?.onZoomIn ?? noop;
2163
- const onZoomOut = onZoomOutProp ?? ctx?.onZoomOut ?? noop;
2164
- const onUndo = onUndoProp ?? ctx?.onUndo ?? noop;
2165
- const onRedo = onRedoProp ?? ctx?.onRedo ?? noop;
2166
- const canUndo = canUndoProp ?? ctx?.canUndo ?? false;
2167
- const canRedo = canRedoProp ?? ctx?.canRedo ?? false;
2168
- const onRequestRender = onRequestRenderProp ?? ctx?.onRequestRender ?? noop;
2169
- const [expanded, setExpanded] = react.useState(false);
2170
- const svgRef = react.useRef(null);
2171
- const [dragging, setDragging] = react.useState(false);
2172
- const [mouseDown, setMouseDown] = react.useState(false);
2173
- const worldBounds = computeWorldBounds(items);
2174
- const isEmpty = worldBounds.width <= 0 || worldBounds.height <= 0;
2175
- const scale = Math.min(
2176
- (MINIMAP_W - PADDING * 2) / Math.max(worldBounds.width, 1),
2177
- (MINIMAP_H - PADDING * 2) / Math.max(worldBounds.height, 1)
2178
- );
2179
- const originX = worldBounds.x;
2180
- const originY = worldBounds.y;
2181
- const toMinimapX = (wx) => (wx - originX) * scale + PADDING;
2182
- const toMinimapY = (wy) => (wy - originY) * scale + PADDING;
2183
- const toMinimapW = (ww) => ww * scale;
2184
- const toMinimapH = (wh) => wh * scale;
2185
- const viewportWorld = camera ? camera.getVisibleWorldRect(viewportWidth, viewportHeight) : { x: 0, y: 0, width: 0, height: 0 };
2186
- const vpMinimap = {
2187
- x: toMinimapX(viewportWorld.x),
2188
- y: toMinimapY(viewportWorld.y),
2189
- width: toMinimapW(viewportWorld.width),
2190
- 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();
2191
2949
  };
2192
- const worldFromMinimap = react.useCallback(
2193
- (mx, my) => ({
2194
- worldX: (mx - PADDING) / scale + originX,
2195
- worldY: (my - PADDING) / scale + originY
2196
- }),
2197
- [scale, originX, originY]
2198
- );
2199
- const panTo = react.useCallback(
2200
- (mx, my) => {
2201
- if (!camera) return;
2202
- const { worldX, worldY } = worldFromMinimap(mx, my);
2203
- camera.x = viewportWidth / 2 - worldX * camera.zoom;
2204
- camera.y = viewportHeight / 2 - worldY * camera.zoom;
2205
- onRequestRender();
2206
- },
2207
- [worldFromMinimap, camera, viewportWidth, viewportHeight, onRequestRender]
2208
- );
2209
- const handlePointerDown = react.useCallback(
2210
- (e) => {
2211
- if (isEmpty) return;
2212
- setDragging(true);
2213
- setMouseDown(true);
2214
- const rect = svgRef.current?.getBoundingClientRect();
2215
- if (!rect) return;
2216
- panTo(e.clientX - rect.left, e.clientY - rect.top);
2217
- svgRef.current?.setPointerCapture(e.pointerId);
2218
- },
2219
- [isEmpty, panTo]
2220
- );
2221
- const handlePointerMove = react.useCallback(
2222
- (e) => {
2223
- if (!mouseDown || isEmpty) return;
2224
- const rect = svgRef.current?.getBoundingClientRect();
2225
- if (!rect) return;
2226
- panTo(e.clientX - rect.left, e.clientY - rect.top);
2227
- },
2228
- [mouseDown, isEmpty, panTo]
2229
- );
2230
- const handlePointerUp = react.useCallback(() => {
2231
- setDragging(false);
2232
- setMouseDown(false);
2233
- }, []);
2234
- const toggleExpanded = react.useCallback(() => {
2235
- setExpanded((v) => !v);
2236
- }, []);
2237
- const anchorStyle = getBoardPositionStyle(position, inset, zIndex);
2238
- return /* @__PURE__ */ jsxRuntime.jsxs(
2239
- "fieldset",
2950
+ const renderAction = (label, onClick, options) => /* @__PURE__ */ jsxRuntime.jsx(
2951
+ "button",
2240
2952
  {
2241
- "data-slot": "canvu-nav-menu",
2242
- "data-position": position,
2243
- className,
2953
+ type: "button",
2954
+ role: "menuitem",
2244
2955
  style: {
2245
- ...anchorStyle,
2246
- ...panelLayoutStyle,
2247
- border: "none",
2248
- margin: 0,
2249
- padding: 0,
2250
- minWidth: 0,
2251
- ...style
2956
+ ...itemStyle,
2957
+ ...options?.danger ? { color: "#b91c1c" } : {}
2252
2958
  },
2253
- "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",
2254
2976
  children: [
2255
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: innerStyle, children: [
2256
- /* @__PURE__ */ jsxRuntime.jsx(
2257
- "button",
2258
- {
2259
- type: "button",
2260
- style: btnStyle,
2261
- "aria-label": "Zoom out",
2262
- title: "Zoom out",
2263
- onPointerDown: (e) => {
2264
- if (e.pointerType === "mouse") e.preventDefault();
2265
- },
2266
- onClick: onZoomOut,
2267
- children: "\u2212"
2268
- }
2269
- ),
2270
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: labelStyle2, "aria-hidden": true, children: [
2271
- zoomPercent,
2272
- "%"
2273
- ] }),
2274
- /* @__PURE__ */ jsxRuntime.jsx(
2275
- "button",
2276
- {
2277
- type: "button",
2278
- style: btnStyle,
2279
- "aria-label": "Zoom in",
2280
- title: "Zoom in",
2281
- onPointerDown: (e) => {
2282
- if (e.pointerType === "mouse") e.preventDefault();
2283
- },
2284
- onClick: onZoomIn,
2285
- children: "+"
2286
- }
2287
- ),
2288
- /* @__PURE__ */ jsxRuntime.jsx(
2289
- "button",
2290
- {
2291
- type: "button",
2292
- style: undoBtnStyle,
2293
- "aria-label": "Undo",
2294
- title: "Undo",
2295
- disabled: !canUndo,
2296
- onPointerDown: (e) => {
2297
- if (e.pointerType === "mouse") e.preventDefault();
2298
- },
2299
- onClick: onUndo,
2300
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16 })
2301
- }
2302
- ),
2303
- /* @__PURE__ */ jsxRuntime.jsx(
2304
- "button",
2305
- {
2306
- type: "button",
2307
- style: undoBtnStyle,
2308
- "aria-label": "Redo",
2309
- title: "Redo",
2310
- disabled: !canRedo,
2311
- onPointerDown: (e) => {
2312
- if (e.pointerType === "mouse") e.preventDefault();
2313
- },
2314
- onClick: onRedo,
2315
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16 })
2316
- }
2317
- ),
2318
- /* @__PURE__ */ jsxRuntime.jsx(
2319
- "button",
2320
- {
2321
- type: "button",
2322
- style: chevronBtnStyle,
2323
- "aria-label": expanded ? "Hide minimap" : "Show minimap",
2324
- title: expanded ? "Hide minimap" : "Show minimap",
2325
- onPointerDown: (e) => {
2326
- if (e.pointerType === "mouse") e.preventDefault();
2327
- },
2328
- onClick: toggleExpanded,
2329
- children: /* @__PURE__ */ jsxRuntime.jsx(
2330
- "svg",
2331
- {
2332
- width: "12",
2333
- height: "12",
2334
- viewBox: "0 0 12 12",
2335
- style: {
2336
- transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
2337
- transition: "transform 0.15s ease"
2338
- },
2339
- "aria-hidden": true,
2340
- children: /* @__PURE__ */ jsxRuntime.jsx(
2341
- "path",
2342
- {
2343
- d: "M2 4 L6 8 L10 4",
2344
- fill: "none",
2345
- stroke: "currentColor",
2346
- strokeWidth: "1.5",
2347
- strokeLinecap: "round",
2348
- strokeLinejoin: "round"
2349
- }
2350
- )
2351
- }
2352
- )
2353
- }
2354
- )
2355
- ] }),
2356
- expanded && /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Minimap", children: /* @__PURE__ */ jsxRuntime.jsx(
2357
- "svg",
2358
- {
2359
- ref: svgRef,
2360
- style: {
2361
- width: MINIMAP_W,
2362
- height: MINIMAP_H,
2363
- borderRadius: BORDER_RADIUS,
2364
- border: `1px solid ${BORDER_COLOR}`,
2365
- background: BG,
2366
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
2367
- cursor: isEmpty ? "default" : dragging ? "grabbing" : "grab",
2368
- display: "block",
2369
- pointerEvents: "auto"
2370
- },
2371
- onPointerDown: handlePointerDown,
2372
- onPointerMove: handlePointerMove,
2373
- onPointerUp: handlePointerUp,
2374
- onPointerCancel: handlePointerUp,
2375
- children: isEmpty ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2376
- items.map((it) => {
2377
- const b = normalizeRect(boundsAabbForRotatedItem(it));
2378
- return /* @__PURE__ */ jsxRuntime.jsx(
2379
- "rect",
2380
- {
2381
- x: toMinimapX(b.x),
2382
- y: toMinimapY(b.y),
2383
- width: toMinimapW(b.width),
2384
- height: toMinimapH(b.height),
2385
- fill: ITEM_FILL,
2386
- stroke: ITEM_STROKE,
2387
- strokeWidth: 0.5,
2388
- rx: 1
2389
- },
2390
- it.id
2391
- );
2392
- }),
2393
- /* @__PURE__ */ jsxRuntime.jsx(
2394
- "rect",
2395
- {
2396
- x: vpMinimap.x,
2397
- y: vpMinimap.y,
2398
- width: vpMinimap.width,
2399
- height: vpMinimap.height,
2400
- fill: VIEWPORT_FILL,
2401
- stroke: VIEWPORT_STROKE,
2402
- strokeWidth: 1.5,
2403
- rx: 2
2404
- }
2405
- )
2406
- ] })
2407
- }
2408
- ) })
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 })
2409
2988
  ]
2410
2989
  }
2411
2990
  );
2991
+ if (typeof document === "undefined") {
2992
+ return null;
2993
+ }
2994
+ return reactDom.createPortal(menu, document.body);
2412
2995
  }
2413
- 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
+ ] });
2414
3013
  }
2415
- function computeWorldBounds(items) {
2416
- if (items.length === 0) {
2417
- return { x: 0, y: 0, width: 0, height: 0 };
2418
- }
2419
- let minX = Infinity;
2420
- let minY = Infinity;
2421
- let maxX = -Infinity;
2422
- let maxY = -Infinity;
2423
- for (const it of items) {
2424
- const b = boundsAabbForRotatedItem(it);
2425
- minX = Math.min(minX, b.x);
2426
- minY = Math.min(minY, b.y);
2427
- maxX = Math.max(maxX, b.x + b.width);
2428
- maxY = Math.max(maxY, b.y + b.height);
2429
- }
2430
- const pad = Math.max((maxX - minX) * 0.1, (maxY - minY) * 0.1, 40);
2431
- return {
2432
- x: minX - pad,
2433
- y: minY - pad,
2434
- width: maxX - minX + pad * 2,
2435
- height: maxY - minY + pad * 2
2436
- };
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
+ ] });
2437
3019
  }
2438
-
2439
- // src/react/persistence/indexed-db-adapter.ts
2440
- init_shape_builders();
2441
- var DOCUMENT_STORE = "document";
2442
- var DOCUMENT_KEY = "main";
2443
- function openDocumentDb(dbName) {
2444
- return new Promise((resolve, reject) => {
2445
- const req = indexedDB.open(dbName, 1);
2446
- req.onupgradeneeded = () => {
2447
- const db = req.result;
2448
- if (!db.objectStoreNames.contains(DOCUMENT_STORE)) {
2449
- db.createObjectStore(DOCUMENT_STORE);
2450
- }
2451
- };
2452
- req.onsuccess = () => resolve(req.result);
2453
- req.onerror = () => reject(req.error);
2454
- });
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" }) });
2455
3022
  }
2456
- function createIndexedDbPersistenceAdapter(options = {}) {
2457
- const dbName = options.dbName ?? "canvu-document";
2458
- const imageStore = new IndexedDbImageStore();
2459
- let documentDbPromise = null;
2460
- function getDocDb() {
2461
- if (!documentDbPromise) {
2462
- documentDbPromise = openDocumentDb(dbName);
2463
- }
2464
- return documentDbPromise;
2465
- }
2466
- async function getFromDocDb(key) {
2467
- const db = await getDocDb();
2468
- return new Promise((resolve, reject) => {
2469
- const tx = db.transaction(DOCUMENT_STORE, "readonly");
2470
- const req = tx.objectStore(DOCUMENT_STORE).get(key);
2471
- req.onsuccess = () => resolve(req.result);
2472
- req.onerror = () => reject(req.error);
2473
- });
2474
- }
2475
- async function putInDocDb(key, value) {
2476
- const db = await getDocDb();
2477
- return new Promise((resolve, reject) => {
2478
- const tx = db.transaction(DOCUMENT_STORE, "readwrite");
2479
- const req = tx.objectStore(DOCUMENT_STORE).put(value, key);
2480
- req.onsuccess = () => resolve();
2481
- req.onerror = () => reject(req.error);
2482
- });
2483
- }
2484
- const revokedHrefs = /* @__PURE__ */ new Set();
2485
- function revokeItemBlob(item) {
2486
- if (item.imageRasterHref?.startsWith("blob:")) {
2487
- revokedHrefs.add(item.imageRasterHref);
2488
- }
2489
- if (item.imageThumbnailHref?.startsWith("blob:")) {
2490
- revokedHrefs.add(item.imageThumbnailHref);
2491
- }
2492
- const out = { ...item };
2493
- delete out.imageRasterHref;
2494
- delete out.imageThumbnailHref;
2495
- if (item.toolKind === "image") {
2496
- out.childrenSvg = "";
2497
- }
2498
- return out;
2499
- }
2500
- return {
2501
- async load() {
2502
- const raw = await getFromDocDb(DOCUMENT_KEY);
2503
- if (!raw?.items) return null;
2504
- const resolved = [];
2505
- for (const item of raw.items) {
2506
- if (item.toolKind === "image" && item.imageBlobId) {
2507
- const itemWithBlob = { ...item };
2508
- if (item.imageThumbnailBlobId) {
2509
- const thumbUrl = await createThumbnailBlobUrlFromStore(
2510
- imageStore,
2511
- item.imageThumbnailBlobId
2512
- );
2513
- if (thumbUrl) {
2514
- itemWithBlob.imageThumbnailHref = thumbUrl;
2515
- }
2516
- }
2517
- const fullUrl = await createBlobUrlFromStore(imageStore, item.imageBlobId);
2518
- if (fullUrl) {
2519
- itemWithBlob.imageRasterHref = fullUrl;
2520
- }
2521
- const rebuilt = rebuildItemSvg(itemWithBlob);
2522
- resolved.push(rebuilt);
2523
- } else {
2524
- resolved.push(item);
2525
- }
2526
- }
2527
- return { items: resolved, version: raw.version ?? 1 };
2528
- },
2529
- async save(snapshot) {
2530
- for (const href of revokedHrefs) {
2531
- URL.revokeObjectURL(href);
2532
- }
2533
- revokedHrefs.clear();
2534
- const cleanItems = snapshot.items.map(revokeItemBlob);
2535
- await putInDocDb(DOCUMENT_KEY, {
2536
- items: cleanItems,
2537
- version: snapshot.version ?? 1
2538
- });
2539
- }
2540
- };
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" }) });
2541
3025
  }
2542
-
2543
- // src/react/persistence/local-storage-adapter.ts
2544
- var DEFAULT_VECTOR_CANVAS_STORAGE_KEY = "trazo.vector-canvas.v1";
2545
- var SNAPSHOT_VERSION = 1;
2546
- function createLocalStoragePersistenceAdapter(options = {}) {
2547
- const key = options.key ?? DEFAULT_VECTOR_CANVAS_STORAGE_KEY;
2548
- const storage = options.storage ?? (typeof globalThis !== "undefined" && "localStorage" in globalThis && globalThis.localStorage ? globalThis.localStorage : null);
2549
- return {
2550
- load() {
2551
- if (!storage) return Promise.resolve(null);
2552
- try {
2553
- const raw = storage.getItem(key);
2554
- if (!raw) return Promise.resolve(null);
2555
- const parsed = JSON.parse(raw);
2556
- if (!parsed || typeof parsed !== "object" || !("items" in parsed) || !Array.isArray(parsed.items)) {
2557
- return Promise.resolve(null);
2558
- }
2559
- return Promise.resolve(parsed);
2560
- } catch {
2561
- return Promise.resolve(null);
2562
- }
2563
- },
2564
- save(snapshot) {
2565
- if (!storage) return Promise.resolve();
2566
- try {
2567
- storage.setItem(
2568
- key,
2569
- JSON.stringify({
2570
- ...snapshot,
2571
- version: snapshot.version ?? SNAPSHOT_VERSION
2572
- })
2573
- );
2574
- } catch {
2575
- }
2576
- return Promise.resolve();
2577
- }
2578
- };
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" }) });
2579
3028
  }
2580
- function createNoopPersistenceAdapter() {
2581
- return {
2582
- load: () => Promise.resolve(null),
2583
- save: () => Promise.resolve()
2584
- };
2585
- }
2586
- function useVectorCanvasDocument(options = {}) {
2587
- const {
2588
- persistenceKey = DEFAULT_VECTOR_CANVAS_STORAGE_KEY,
2589
- persistence,
2590
- debounceMs = 400,
2591
- remote
2592
- } = options;
2593
- const adapter = react.useMemo(() => {
2594
- if (persistence === false) return createNoopPersistenceAdapter();
2595
- if (persistence) return persistence;
2596
- return createLocalStoragePersistenceAdapter({ key: persistenceKey });
2597
- }, [persistence, persistenceKey]);
2598
- const [items, setItems] = react.useState([]);
2599
- const [isHydrated, setIsHydrated] = react.useState(false);
2600
- const saveTimerRef = react.useRef(null);
2601
- const adapterRef = react.useRef(adapter);
2602
- adapterRef.current = adapter;
2603
- react.useEffect(() => {
2604
- let cancelled = false;
2605
- adapter.load().then((snap) => {
2606
- if (cancelled) return;
2607
- if (snap?.items && snap.items.length > 0) {
2608
- setItems(snap.items);
2609
- }
2610
- setIsHydrated(true);
2611
- });
2612
- return () => {
2613
- cancelled = true;
2614
- };
2615
- }, [adapter]);
2616
- const persist = react.useCallback((next) => {
2617
- void adapterRef.current.save({ items: next, version: 1 });
2618
- }, []);
2619
- const onItemsChange = react.useCallback(
2620
- (next) => {
2621
- setItems(next);
2622
- remote?.send?.(next);
2623
- if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
2624
- saveTimerRef.current = setTimeout(() => {
2625
- persist(next);
2626
- saveTimerRef.current = null;
2627
- }, debounceMs);
2628
- },
2629
- [debounceMs, persist, remote]
2630
- );
2631
- react.useEffect(() => {
2632
- if (!remote || !isHydrated) return;
2633
- return remote.subscribe((serverItems) => {
2634
- setItems(serverItems);
2635
- persist(serverItems);
2636
- });
2637
- }, [remote, isHydrated, persist]);
2638
- react.useEffect(
2639
- () => () => {
2640
- if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
2641
- },
2642
- []
2643
- );
2644
- const clearPersistedDocument = react.useCallback(() => {
2645
- setItems([]);
2646
- void adapterRef.current.save({ items: [], version: 1 });
2647
- }, []);
2648
- return {
2649
- items,
2650
- onItemsChange,
2651
- setItems: onItemsChange,
2652
- isHydrated,
2653
- clearPersistedDocument
2654
- };
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
+ ] });
2655
3034
  }
2656
- var CanvuPluginContext = react.createContext(
2657
- null
2658
- );
2659
- function createCanvuPlugin(plugin) {
2660
- 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
+ ] });
2661
3042
  }
2662
- function useCanvuPluginContext() {
2663
- const ctx = react.useContext(CanvuPluginContext);
2664
- if (!ctx) {
2665
- throw new Error(
2666
- "useCanvuPluginContext must be used inside a VectorViewport plugin runtime."
2667
- );
2668
- }
2669
- 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
+ ] });
2670
3049
  }
2671
- function useCanvuViewportContext() {
2672
- const { viewportRef, viewport } = useCanvuPluginContext();
2673
- 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
+ ] });
2674
3056
  }
2675
- function useCanvuDocumentContext() {
2676
- const { viewport } = useCanvuPluginContext();
2677
- return {
2678
- items: viewport.items,
2679
- onItemsChange: viewport.onItemsChange
2680
- };
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
+ ] });
2681
3065
  }
2682
- function useCanvuResolvedTools() {
2683
- 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";
2684
3175
  }
2685
- function useCanvuPluginContribution(pluginId, contribution) {
2686
- const { registerContribution, unregisterContribution } = useCanvuPluginContext();
2687
- react.useLayoutEffect(() => {
2688
- registerContribution(pluginId, contribution);
2689
- return () => unregisterContribution(pluginId);
2690
- }, [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";
2691
3178
  }
2692
- function ToolPluginComponent({
2693
- pluginId,
2694
- tool,
2695
- toolTransform,
2696
- createItem
2697
- }) {
2698
- const contribution = react.useMemo(
2699
- () => ({
2700
- tools: [tool],
2701
- toolTransform,
2702
- customPlacements: createItem ? [
2703
- {
2704
- toolId: tool.id,
2705
- createItem
2706
- }
2707
- ] : void 0
2708
- }),
2709
- [createItem, tool, toolTransform]
2710
- );
2711
- useCanvuPluginContribution(pluginId, contribution);
2712
- return null;
2713
- }
2714
- function createToolPlugin(options) {
2715
- const { createItem, toolTransform, ...tool } = options;
2716
- const pluginId = `canvu.plugin.tool:${tool.id}`;
2717
- return createCanvuPlugin({
2718
- id: pluginId,
2719
- Component() {
2720
- return /* @__PURE__ */ jsxRuntime.jsx(
2721
- ToolPluginComponent,
2722
- {
2723
- pluginId,
2724
- tool,
2725
- toolTransform,
2726
- createItem
2727
- }
2728
- );
2729
- }
2730
- });
2731
- }
2732
- var menuStyle = {
2733
- position: "fixed",
2734
- zIndex: 1e4,
2735
- minWidth: 200,
2736
- padding: 4,
2737
- backgroundColor: "#ffffff",
2738
- border: "1px solid #e2e8f0",
2739
- borderRadius: 8,
2740
- boxShadow: "0 10px 40px rgba(15, 23, 42, 0.12)",
2741
- fontSize: 13,
2742
- fontFamily: "system-ui, sans-serif",
2743
- color: "#0f172a"
2744
- };
2745
- var itemStyle = {
2746
- display: "block",
2747
- width: "100%",
2748
- textAlign: "left",
2749
- padding: "8px 12px",
2750
- margin: 0,
2751
- border: "none",
2752
- borderRadius: 4,
2753
- background: "transparent",
2754
- cursor: "pointer"
2755
- };
2756
- var dividerStyle = {
2757
- height: 1,
2758
- margin: "4px 8px",
2759
- background: "#e2e8f0"
2760
- };
2761
- function ShapeContextMenu({
2762
- x,
2763
- y,
2764
- allSelectedLocked,
2765
- onClose,
2766
- onToggleLock,
2767
- onCut,
2768
- onCopy,
2769
- onBringToFront,
2770
- onBringForward,
2771
- onSendBackward,
2772
- onSendToBack,
2773
- onDuplicate,
2774
- 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
2775
3188
  }) {
2776
- const rootRef = react.useRef(null);
2777
- react.useLayoutEffect(() => {
2778
- const el = rootRef.current;
2779
- if (!el) return;
2780
- const pad = 8;
2781
- const w = el.offsetWidth || 200;
2782
- const h = el.offsetHeight || 160;
2783
- let left = x;
2784
- let top = y;
2785
- if (left + w + pad > window.innerWidth) {
2786
- left = Math.max(pad, window.innerWidth - w - pad);
2787
- }
2788
- if (top + h + pad > window.innerHeight) {
2789
- top = Math.max(pad, window.innerHeight - h - pad);
2790
- }
2791
- el.style.left = `${left}px`;
2792
- el.style.top = `${top}px`;
2793
- }, [x, y]);
2794
- react.useEffect(() => {
2795
- const onKey = (e) => {
2796
- if (e.key === "Escape") {
2797
- onClose();
2798
- }
2799
- };
2800
- const onPointerDown = (e) => {
2801
- const t = e.target;
2802
- if (t && rootRef.current?.contains(t)) {
2803
- return;
2804
- }
2805
- onClose();
2806
- };
2807
- document.addEventListener("keydown", onKey);
2808
- document.addEventListener("pointerdown", onPointerDown, true);
2809
- return () => {
2810
- document.removeEventListener("keydown", onKey);
2811
- document.removeEventListener("pointerdown", onPointerDown, true);
2812
- };
2813
- }, [onClose]);
2814
- const run = (fn) => () => {
2815
- fn();
2816
- 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
2817
3198
  };
2818
- const renderAction = (label, onClick, options) => /* @__PURE__ */ jsxRuntime.jsx(
2819
- "button",
2820
- {
2821
- type: "button",
2822
- role: "menuitem",
2823
- style: {
2824
- ...itemStyle,
2825
- ...options?.danger ? { color: "#b91c1c" } : {}
2826
- },
2827
- onMouseEnter: (e) => {
2828
- e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
2829
- },
2830
- onMouseLeave: (e) => {
2831
- e.currentTarget.style.background = "transparent";
2832
- },
2833
- onClick: run(onClick),
2834
- children: label
2835
- }
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)
2836
3284
  );
2837
- const menu = /* @__PURE__ */ jsxRuntime.jsxs(
2838
- "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",
2839
3306
  {
2840
- ref: rootRef,
2841
- "data-slot": "shape-context-menu",
2842
- style: { ...menuStyle, left: x, top: y },
2843
- 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,
2844
3312
  children: [
2845
- renderAction("Recortar", onCut),
2846
- renderAction("Copiar", onCopy),
2847
- renderAction("Duplicar", onDuplicate),
2848
- /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: dividerStyle }),
2849
- renderAction("Trazer para frente", onBringToFront),
2850
- renderAction("Avancar uma camada", onBringForward),
2851
- renderAction("Recuar uma camada", onSendBackward),
2852
- renderAction("Enviar para tras", onSendToBack),
2853
- /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: dividerStyle }),
2854
- renderAction(allSelectedLocked ? "Desbloquear" : "Bloquear", onToggleLock),
2855
- 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
+ ] })
2856
3385
  ]
2857
3386
  }
2858
3387
  );
2859
- if (typeof document === "undefined") {
2860
- return null;
2861
- }
2862
- return reactDom.createPortal(menu, document.body);
2863
3388
  }
2864
- var base = {
2865
- width: 20,
2866
- height: 20,
2867
- viewBox: "0 0 24 24",
2868
- fill: "none",
2869
- stroke: "currentColor",
2870
- strokeWidth: 2,
2871
- strokeLinecap: "round",
2872
- strokeLinejoin: "round"
3389
+ var rootStyle = {
3390
+ display: "flex",
3391
+ flexDirection: "column",
3392
+ height: "100%",
3393
+ minHeight: 0,
3394
+ width: "100%"
2873
3395
  };
2874
- function IconHand(props) {
2875
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2876
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0" }),
2877
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2" }),
2878
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8" }),
2879
- /* @__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" })
2880
- ] });
2881
- }
2882
- function IconSelect(props) {
2883
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2884
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" }),
2885
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M13 13l6 6" })
2886
- ] });
2887
- }
2888
- function IconRect(props) {
2889
- 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" }) });
2890
- }
2891
- function IconEllipse(props) {
2892
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6" }) });
2893
- }
2894
- function IconLine(props) {
2895
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }) });
2896
- }
2897
- function IconArrow(props) {
2898
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2899
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }),
2900
- /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "12 5 19 5 19 12" })
2901
- ] });
2902
- }
2903
- function IconDraw(props) {
2904
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2905
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19l7-7 3 3-7 7-3-3z" }),
2906
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z" }),
2907
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 2l7.586 7.586" }),
2908
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "2" })
2909
- ] });
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;
2910
3431
  }
2911
- function IconText(props) {
2912
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2913
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }),
2914
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 21h6" }),
2915
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3v18" })
2916
- ] });
3432
+ function vectorCanvasSpaceStyle(position, inset, zIndex) {
3433
+ return {
3434
+ ...getBoardPositionStyle(position, inset, zIndex),
3435
+ pointerEvents: "none"
3436
+ };
2917
3437
  }
2918
- function IconImage(props) {
2919
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2920
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2921
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2922
- /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "21 15 16 10 5 21" })
2923
- ] });
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
+ );
2924
3452
  }
2925
- function IconLaser(props) {
2926
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
2927
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "2.5" }),
2928
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4v4" }),
2929
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 16v4" }),
2930
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 12h4" }),
2931
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 12h4" })
2932
- ] });
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
+ );
2933
3467
  }
2934
- var ic = { size: 20, strokeWidth: 2 };
2935
- var DEFAULT_OVERFLOW_TOOL_IDS = [
2936
- "rect",
2937
- "ellipse",
2938
- "line",
2939
- "marker",
2940
- "laser",
2941
- "image"
2942
- ];
2943
- var DEFAULT_VECTOR_TOOLS = [
2944
- {
2945
- id: "hand",
2946
- label: "Hand",
2947
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Hand, { ...ic, "aria-hidden": true }),
2948
- shortcutHint: "H"
2949
- },
2950
- {
2951
- id: "select",
2952
- label: "Select",
2953
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointer2, { ...ic, "aria-hidden": true }),
2954
- shortcutHint: "V"
2955
- },
2956
- {
2957
- id: "rect",
2958
- label: "Rectangle",
2959
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { ...ic, "aria-hidden": true }),
2960
- shortcutHint: "R"
2961
- },
2962
- {
2963
- id: "ellipse",
2964
- label: "Ellipse",
2965
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Circle, { ...ic, "aria-hidden": true }),
2966
- shortcutHint: "O"
2967
- },
2968
- {
2969
- id: "line",
2970
- label: "Line",
2971
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minus, { ...ic, "aria-hidden": true }),
2972
- shortcutHint: "L"
2973
- },
2974
- {
2975
- id: "arrow",
2976
- label: "Arrow",
2977
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUpRight, { ...ic, "aria-hidden": true }),
2978
- shortcutHint: "A"
2979
- },
2980
- {
2981
- id: "draw",
2982
- label: "Desenhar",
2983
- tooltipLabel: "Draw",
2984
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PenLine, { ...ic, "aria-hidden": true }),
2985
- shortcutHint: "D"
2986
- },
2987
- {
2988
- id: "marker",
2989
- label: "Realce",
2990
- tooltipLabel: "Highlighter",
2991
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Highlighter, { ...ic, "aria-hidden": true }),
2992
- shortcutHint: "M"
2993
- },
2994
- {
2995
- id: "laser",
2996
- label: "Laser",
2997
- icon: /* @__PURE__ */ jsxRuntime.jsx(IconLaser, { "aria-hidden": true }),
2998
- shortcutHint: "K"
2999
- },
3000
- {
3001
- id: "eraser",
3002
- label: "Borracha",
3003
- tooltipLabel: "Eraser",
3004
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eraser, { ...ic, "aria-hidden": true }),
3005
- shortcutHint: "E"
3006
- },
3007
- {
3008
- id: "text",
3009
- label: "Text",
3010
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Type, { ...ic, "aria-hidden": true }),
3011
- shortcutHint: "T"
3012
- },
3013
- {
3014
- id: "image",
3015
- label: "File",
3016
- icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { ...ic, "aria-hidden": true }),
3017
- 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>`);
3018
3577
  }
3019
- ];
3578
+ return ctx;
3579
+ }
3020
3580
  var toolbarStyles = {
3021
3581
  display: "flex",
3022
3582
  gap: "4px",
@@ -3265,6 +3825,20 @@ var overflowGridCellButtonStyle = {
3265
3825
  outline: "none",
3266
3826
  WebkitTapHighlightColor: "transparent"
3267
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
+ }
3268
3842
  function ToolLockGlyph({ locked }) {
3269
3843
  const Icon = locked ? lucideReact.Lock : lucideReact.LockOpen;
3270
3844
  return /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 18, strokeWidth: 2, "aria-hidden": true });
@@ -3278,7 +3852,406 @@ function splitToolbarTools(tools, overflowIds) {
3278
3852
  const overflow = overflowIds.map((id) => tools.find((t) => t.id === id)).filter((t) => Boolean(t));
3279
3853
  return { primary, overflow };
3280
3854
  }
3281
- 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
+ }
3282
4255
  function ToolbarOverflowMenu({
3283
4256
  tools,
3284
4257
  selectedId,
@@ -3289,46 +4262,9 @@ function ToolbarOverflowMenu({
3289
4262
  renderToolButton,
3290
4263
  menuAriaLabel
3291
4264
  }) {
3292
- const [open, setOpen] = react.useState(false);
3293
- const wrapRef = react.useRef(null);
3294
- const triggerRef = react.useRef(null);
3295
- const menuId = react.useId();
4265
+ const { open, setOpen, close, wrapRef, triggerRef, menuId } = useOverflowDropdown();
3296
4266
  const activeInOverflow = tools.some((t) => t.id === selectedId);
3297
4267
  const activeTool = tools.find((t) => t.id === selectedId);
3298
- const close = react.useCallback(() => {
3299
- setOpen(false);
3300
- }, []);
3301
- const prevOpenRef = react.useRef(open);
3302
- react.useEffect(() => {
3303
- if (!open) {
3304
- return;
3305
- }
3306
- const onDocPointerDown = (e) => {
3307
- if (wrapRef.current?.contains(e.target)) {
3308
- return;
3309
- }
3310
- close();
3311
- };
3312
- const onKeyDown = (e) => {
3313
- if (e.key === "Escape") {
3314
- e.stopPropagation();
3315
- close();
3316
- triggerRef.current?.focus();
3317
- }
3318
- };
3319
- document.addEventListener("pointerdown", onDocPointerDown, true);
3320
- document.addEventListener("keydown", onKeyDown, true);
3321
- return () => {
3322
- document.removeEventListener("pointerdown", onDocPointerDown, true);
3323
- document.removeEventListener("keydown", onKeyDown, true);
3324
- };
3325
- }, [open, close]);
3326
- react.useEffect(() => {
3327
- if (prevOpenRef.current && !open) {
3328
- triggerRef.current?.focus({ preventScroll: true });
3329
- }
3330
- prevOpenRef.current = open;
3331
- }, [open]);
3332
4268
  const triggerHighlighted = open || activeInOverflow;
3333
4269
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrapRef, className: "vector-toolbar-overflow-anchor", children: [
3334
4270
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
@@ -3350,20 +4286,9 @@ function ToolbarOverflowMenu({
3350
4286
  minHeight: density === "compact" ? "40px" : "44px",
3351
4287
  padding: "6px 8px",
3352
4288
  gap: "4px",
3353
- ...triggerHighlighted ? {
3354
- background: "rgba(24,24,27,0.1)",
3355
- borderColor: "rgba(24,24,27,0.28)",
3356
- boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3357
- } : {
3358
- borderColor: "transparent",
3359
- boxShadow: "none"
3360
- }
3361
- },
3362
- onPointerDown: (ev) => {
3363
- if (ev.pointerType === "mouse") {
3364
- ev.preventDefault();
3365
- }
4289
+ ...triggerHighlighted ? activeToggleStyle : inactiveToggleStyle
3366
4290
  },
4291
+ onPointerDown: preventMouseDefault2,
3367
4292
  onClick: () => setOpen((o) => !o),
3368
4293
  onKeyDown: (e) => {
3369
4294
  if (e.key === "ArrowDown" && !open) {
@@ -3410,25 +4335,14 @@ function ToolbarOverflowMenu({
3410
4335
  id: `${menuId}-menu`,
3411
4336
  "aria-label": menuAriaLabel,
3412
4337
  className: "vector-toolbar-overflow-panel",
3413
- children: tools.map((tool) => {
3414
- const selected = tool.id === selectedId;
3415
- return /* @__PURE__ */ jsxRuntime.jsx(
3416
- "div",
3417
- {
3418
- className: "vector-toolbar-overflow-cell",
3419
- role: "presentation",
3420
- children: renderToolButton({
3421
- tool,
3422
- selected,
3423
- onSelect: () => {
3424
- onSelect(tool.id);
3425
- close();
3426
- }
3427
- })
3428
- },
3429
- tool.id
3430
- );
3431
- })
4338
+ children: tools.map(
4339
+ (tool) => renderOverflowToolButton(tool, {
4340
+ selectedId,
4341
+ selectTool: onSelect,
4342
+ closeMenu: close,
4343
+ renderToolButton
4344
+ })
4345
+ )
3432
4346
  }
3433
4347
  ) : null,
3434
4348
  open && !renderToolButton ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3438,69 +4352,19 @@ function ToolbarOverflowMenu({
3438
4352
  role: "menu",
3439
4353
  "aria-label": menuAriaLabel,
3440
4354
  className: "vector-toolbar-overflow-panel",
3441
- children: tools.map((tool, index) => {
3442
- const selected = tool.id === selectedId;
3443
- const label = tool.ariaLabel ?? tool.label;
3444
- const tooltipName = tool.tooltipLabel ?? tool.label;
3445
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "vector-toolbar-overflow-cell", children: [
3446
- /* @__PURE__ */ jsxRuntime.jsx(
3447
- "button",
3448
- {
3449
- type: "button",
3450
- role: "menuitemradio",
3451
- "aria-checked": selected,
3452
- tabIndex: index === 0 ? 0 : -1,
3453
- "aria-label": label,
3454
- ...tool.shortcutHint !== void 0 ? { "aria-keyshortcuts": tool.shortcutHint } : {},
3455
- style: overflowGridCellButtonStyle,
3456
- onPointerDown: (ev) => {
3457
- if (ev.pointerType === "mouse") {
3458
- ev.preventDefault();
3459
- }
3460
- },
3461
- onClick: () => {
3462
- onSelect(tool.id);
3463
- close();
3464
- },
3465
- children: /* @__PURE__ */ jsxRuntime.jsx(
3466
- "span",
3467
- {
3468
- style: {
3469
- display: "flex",
3470
- alignItems: "center",
3471
- justifyContent: "center",
3472
- lineHeight: 0
3473
- },
3474
- children: tool.icon ?? /* @__PURE__ */ jsxRuntime.jsx(
3475
- "span",
3476
- {
3477
- style: {
3478
- fontSize: "11px",
3479
- fontWeight: 700,
3480
- lineHeight: 1.1,
3481
- textAlign: "center",
3482
- maxWidth: "34px",
3483
- overflow: "hidden",
3484
- textOverflow: "ellipsis"
3485
- },
3486
- children: tool.label.slice(0, 3)
3487
- }
3488
- )
3489
- }
3490
- )
3491
- }
3492
- ),
3493
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "vector-toolbar-overflow-tip", "aria-hidden": "true", children: [
3494
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: tooltipName }),
3495
- tool.shortcutHint !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "vector-toolbar-overflow-tip-shortcut", children: tool.shortcutHint }) : null
3496
- ] })
3497
- ] }, tool.id);
3498
- })
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
+ )
3499
4363
  }
3500
4364
  ) : null
3501
4365
  ] });
3502
4366
  }
3503
- function VectorToolbar({
4367
+ function VectorToolbarComponent({
3504
4368
  value,
3505
4369
  onChange,
3506
4370
  tools,
@@ -3522,23 +4386,63 @@ function VectorToolbar({
3522
4386
  const pluginContext = react.useContext(CanvuPluginContext);
3523
4387
  const runtimeTools = pluginContext?.resolvedTools;
3524
4388
  const resolvedTools = tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS;
3525
- const ctx = {
3526
- tools: resolvedTools,
3527
- selectedId: value,
3528
- selectTool: onChange,
3529
- toolLocked,
3530
- setToolLocked: (locked) => onToolLockedChange?.(locked)
3531
- };
3532
- 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
+ };
3533
4397
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, children: children(ctx) });
3534
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
+ }
3535
4433
  const { primary: primaryTools, overflow: overflowTools } = splitToolbarTools(
3536
4434
  resolvedTools,
3537
4435
  overflowToolIds
3538
4436
  );
3539
- const flexDir = orientation === "vertical" ? "column" : "row";
3540
4437
  const showOverflowMenu = overflowTools.length > 0;
3541
- 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
+ };
3542
4446
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3543
4447
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: TOOLBAR_TOOLTIP_CSS }),
3544
4448
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: OVERFLOW_MENU_CSS }),
@@ -3569,20 +4473,9 @@ function VectorToolbar({
3569
4473
  minWidth: "40px",
3570
4474
  padding: "6px 8px"
3571
4475
  } : {},
3572
- ...toolLocked ? {
3573
- background: "rgba(24,24,27,0.1)",
3574
- borderColor: "rgba(24,24,27,0.28)",
3575
- boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3576
- } : {
3577
- borderColor: "transparent",
3578
- boxShadow: "none"
3579
- }
3580
- },
3581
- onPointerDown: (ev) => {
3582
- if (ev.pointerType === "mouse") {
3583
- ev.preventDefault();
3584
- }
4476
+ ...toolLocked ? activeToggleStyle : inactiveToggleStyle
3585
4477
  },
4478
+ onPointerDown: preventMouseDefault2,
3586
4479
  onClick: () => onToolLockedChange?.(!toolLocked),
3587
4480
  children: /* @__PURE__ */ jsxRuntime.jsx(ToolLockGlyph, { locked: toolLocked })
3588
4481
  }
@@ -3591,89 +4484,7 @@ function VectorToolbar({
3591
4484
  ] }),
3592
4485
  /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, style: toolLockDividerStyle })
3593
4486
  ] }) : null,
3594
- primaryTools.map((tool) => {
3595
- const selected = tool.id === value;
3596
- const label = tool.ariaLabel ?? tool.label;
3597
- if (renderToolButton) {
3598
- return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderToolButton({
3599
- tool,
3600
- selected,
3601
- onSelect: () => onChange(tool.id)
3602
- }) }, tool.id);
3603
- }
3604
- const tooltipName = tool.tooltipLabel ?? tool.label;
3605
- const tooltipText = tool.shortcutHint !== void 0 ? `${tooltipName} \xB7 ${tool.shortcutHint}` : tooltipName;
3606
- const labelOnly = tool.icon == null;
3607
- const labelTextStyle = labelOnly ? {
3608
- fontSize: "12px",
3609
- fontWeight: 600,
3610
- letterSpacing: "0.02em",
3611
- lineHeight: 1.2,
3612
- WebkitFontSmoothing: "antialiased",
3613
- MozOsxFontSmoothing: "grayscale",
3614
- textRendering: "geometricPrecision"
3615
- } : {};
3616
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "vector-toolbar-tool-wrap", children: [
3617
- /* @__PURE__ */ jsxRuntime.jsxs(
3618
- "button",
3619
- {
3620
- type: "button",
3621
- "aria-pressed": selected,
3622
- "aria-label": label,
3623
- ...tool.shortcutHint !== void 0 ? { "aria-keyshortcuts": tool.shortcutHint } : {},
3624
- className: `${buttonClassName} ${selected ? activeButtonClassName : ""}`.trim(),
3625
- style: {
3626
- ...defaultButtonStyle,
3627
- ...density === "compact" ? {
3628
- minHeight: "40px",
3629
- minWidth: labelOnly ? "52px" : "40px",
3630
- padding: "6px 8px"
3631
- } : {},
3632
- ...selected ? {
3633
- background: "rgba(24,24,27,0.1)",
3634
- borderColor: "rgba(24,24,27,0.28)",
3635
- boxShadow: "inset 0 0 0 1px rgba(24,24,27,0.08)"
3636
- } : {
3637
- borderColor: "transparent",
3638
- boxShadow: "none"
3639
- }
3640
- },
3641
- onPointerDown: (ev) => {
3642
- if (ev.pointerType === "mouse") {
3643
- ev.preventDefault();
3644
- }
3645
- },
3646
- onClick: () => onChange(tool.id),
3647
- children: [
3648
- /* @__PURE__ */ jsxRuntime.jsx(
3649
- "span",
3650
- {
3651
- style: {
3652
- display: "flex",
3653
- alignItems: "center",
3654
- justifyContent: "center"
3655
- },
3656
- children: tool.icon ?? /* @__PURE__ */ jsxRuntime.jsx("span", { style: labelTextStyle, children: tool.label })
3657
- }
3658
- ),
3659
- density === "comfortable" && tool.icon ? /* @__PURE__ */ jsxRuntime.jsx(
3660
- "span",
3661
- {
3662
- style: {
3663
- lineHeight: 1,
3664
- maxWidth: "56px",
3665
- overflow: "hidden",
3666
- textOverflow: "ellipsis"
3667
- },
3668
- children: tool.label
3669
- }
3670
- ) : null
3671
- ]
3672
- }
3673
- ),
3674
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "vector-toolbar-tip", "aria-hidden": "true", children: tooltipText })
3675
- ] }, tool.id);
3676
- }),
4487
+ primaryTools.map((tool) => renderInlineToolButton(tool, inlineCtx)),
3677
4488
  showOverflowMenu && orientation === "horizontal" ? /* @__PURE__ */ jsxRuntime.jsx(
3678
4489
  "span",
3679
4490
  {
@@ -3700,6 +4511,51 @@ function VectorToolbar({
3700
4511
  )
3701
4512
  ] });
3702
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
+ );
3703
4559
 
3704
4560
  // src/camera/camera.ts
3705
4561
  init_rect();
@@ -5069,30 +5925,6 @@ var SvgVectorRenderer = class {
5069
5925
  }
5070
5926
  };
5071
5927
 
5072
- // src/scene/clone-item.ts
5073
- init_shape_builders();
5074
- function cloneVectorSceneItemWithNewId(item) {
5075
- const id = createShapeId();
5076
- const copy = JSON.parse(JSON.stringify(item));
5077
- let next = { ...copy, id };
5078
- if (next.toolKind === "arrow" && next.line) {
5079
- next = {
5080
- ...next,
5081
- childrenSvg: buildArrowSvg(id, next.line, resolveStrokeStyle(next))
5082
- };
5083
- }
5084
- if (next.toolKind === "text" && next.text !== void 0) {
5085
- return rebuildItemSvg(next);
5086
- }
5087
- if (next.toolKind === "custom" && next.customInnerSvg && next.customIntrinsicSize) {
5088
- return rebuildItemSvg(next);
5089
- }
5090
- return next;
5091
- }
5092
- function cloneVectorSceneItemsWithNewIds(items) {
5093
- return items.map(cloneVectorSceneItemWithNewId);
5094
- }
5095
-
5096
5928
  // src/scene/scene.ts
5097
5929
  var VectorScene = class {
5098
5930
  items = [];
@@ -7688,10 +8520,8 @@ var VectorViewport = react.forwardRef(
7688
8520
  return;
7689
8521
  }
7690
8522
  if (!e.shiftKey && cur.length > 0) {
7691
- const selectedArrowOrLine = cur.map((id) => resolved.find((it) => it.id === id)).filter(
7692
- (it) => it != null && !it.locked && (it.toolKind === "line" || it.toolKind === "arrow")
7693
- );
7694
- if (selectedArrowOrLine.some(
8523
+ const selectedUnlockedItems = cur.map((id) => resolved.find((it) => it.id === id)).filter((it) => it != null && !it.locked);
8524
+ if (selectedUnlockedItems.some(
7695
8525
  (it) => pointInSelectedItemBounds(it, worldX, worldY)
7696
8526
  )) {
7697
8527
  const snapshots = {};
@@ -8772,109 +9602,6 @@ var VectorViewport = react.forwardRef(
8772
9602
  }
8773
9603
  );
8774
9604
  VectorViewport.displayName = "VectorViewport";
8775
- var DEFAULT_SHELL_STYLE = {
8776
- position: "absolute",
8777
- left: 12,
8778
- bottom: 140,
8779
- zIndex: 22
8780
- };
8781
- var layoutStyle = {
8782
- display: "flex",
8783
- flexDirection: "column",
8784
- gap: 4,
8785
- /* Let clicks pass through the flex gaps; buttons opt in below. */
8786
- pointerEvents: "none"
8787
- };
8788
- var btnStyle2 = {
8789
- pointerEvents: "auto",
8790
- width: 36,
8791
- height: 36,
8792
- display: "inline-flex",
8793
- alignItems: "center",
8794
- justifyContent: "center",
8795
- borderRadius: 8,
8796
- border: "1px solid rgba(0,0,0,0.12)",
8797
- background: "rgba(255,255,255,0.96)",
8798
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
8799
- cursor: "pointer",
8800
- fontSize: 18,
8801
- lineHeight: 1,
8802
- fontWeight: 600,
8803
- color: "#18181b",
8804
- padding: 0,
8805
- outline: "none",
8806
- WebkitTapHighlightColor: "transparent"
8807
- };
8808
- var labelStyle3 = {
8809
- pointerEvents: "none",
8810
- fontSize: 10,
8811
- fontWeight: 600,
8812
- color: "#52525b",
8813
- textAlign: "center",
8814
- userSelect: "none",
8815
- padding: "2px 0"
8816
- };
8817
- var fieldsetReset = {
8818
- border: "none",
8819
- margin: 0,
8820
- padding: 0,
8821
- minWidth: 0
8822
- };
8823
- function ViewportZoomControls({
8824
- zoomPercent,
8825
- onZoomIn,
8826
- onZoomOut,
8827
- className = "",
8828
- position,
8829
- inset = 12,
8830
- zIndex = 22,
8831
- style
8832
- }) {
8833
- const anchorStyle = position !== void 0 ? getBoardPositionStyle(position, inset, zIndex) : DEFAULT_SHELL_STYLE;
8834
- return /* @__PURE__ */ jsxRuntime.jsxs(
8835
- "fieldset",
8836
- {
8837
- className,
8838
- "data-position": position ?? "default",
8839
- "aria-label": "Zoom controls",
8840
- style: { ...anchorStyle, ...layoutStyle, ...fieldsetReset, ...style },
8841
- children: [
8842
- /* @__PURE__ */ jsxRuntime.jsx(
8843
- "button",
8844
- {
8845
- type: "button",
8846
- style: btnStyle2,
8847
- "aria-label": "Zoom in",
8848
- title: "Zoom in",
8849
- onPointerDown: (e) => {
8850
- if (e.pointerType === "mouse") e.preventDefault();
8851
- },
8852
- onClick: onZoomIn,
8853
- children: "+"
8854
- }
8855
- ),
8856
- /* @__PURE__ */ jsxRuntime.jsx(
8857
- "button",
8858
- {
8859
- type: "button",
8860
- style: btnStyle2,
8861
- "aria-label": "Zoom out",
8862
- title: "Zoom out",
8863
- onPointerDown: (e) => {
8864
- if (e.pointerType === "mouse") e.preventDefault();
8865
- },
8866
- onClick: onZoomOut,
8867
- children: "\u2212"
8868
- }
8869
- ),
8870
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: labelStyle3, "aria-hidden": true, children: [
8871
- zoomPercent,
8872
- "%"
8873
- ] })
8874
- ]
8875
- }
8876
- );
8877
- }
8878
9605
 
8879
9606
  exports.CanvuChromeContext = CanvuChromeContext;
8880
9607
  exports.CanvuPluginContext = CanvuPluginContext;
@@ -8891,6 +9618,7 @@ exports.IconLine = IconLine;
8891
9618
  exports.IconRect = IconRect;
8892
9619
  exports.IconSelect = IconSelect;
8893
9620
  exports.IconText = IconText;
9621
+ exports.ImagesMenu = ImagesMenu;
8894
9622
  exports.NavMenu = NavMenu;
8895
9623
  exports.ShapeContextMenu = ShapeContextMenu;
8896
9624
  exports.VectorCanvas = VectorCanvas;
@@ -8903,7 +9631,6 @@ exports.VectorCanvasViewportSurface = VectorCanvasViewportSurface;
8903
9631
  exports.VectorSelectionInspector = VectorSelectionInspector;
8904
9632
  exports.VectorToolbar = VectorToolbar;
8905
9633
  exports.VectorViewport = VectorViewport;
8906
- exports.ViewportZoomControls = ViewportZoomControls;
8907
9634
  exports.createCanvuPlugin = createCanvuPlugin;
8908
9635
  exports.createIndexedDbPersistenceAdapter = createIndexedDbPersistenceAdapter;
8909
9636
  exports.createLocalStoragePersistenceAdapter = createLocalStoragePersistenceAdapter;