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