@pixerate/editor-react 0.75.0 → 0.76.0

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.
@@ -44,7 +44,7 @@ function useVectorCommands(registry, controller, options = {}) {
44
44
  }
45
45
 
46
46
  // src/vector/VectorCanvas.tsx
47
- import React2, { useEffect as useEffect2, useId, useRef as useRef3 } from "react";
47
+ import React2, { useEffect as useEffect2, useId, useRef as useRef3, useState as useState2 } from "react";
48
48
  import {
49
49
  VECTOR_OVERLAY_THEME as THEME2,
50
50
  getVectorOverlayRenderData,
@@ -227,48 +227,19 @@ function VectorTextElement({ node }) {
227
227
  )) });
228
228
  }
229
229
  function VectorSceneNodes({ nodes }) {
230
- return /* @__PURE__ */ jsx2(Fragment, { children: nodes.map(
231
- (node) => node.kind === "text" ? /* @__PURE__ */ jsx2(VectorTextElement, { node }, node.id) : node.kind === "group" ? /* @__PURE__ */ jsx2(
232
- "g",
233
- {
234
- "data-vector-id": node.id,
235
- "data-vector-mask": node.mask?.type,
236
- transform: node.transform,
237
- opacity: node.opacity < 1 ? node.opacity : void 0,
238
- filter: node.filter,
239
- style: blendStyle(node.blendMode),
240
- children: node.mask ? /* @__PURE__ */ jsx2(VectorMaskedNodes, { mask: node.mask, nodes: node.children }) : /* @__PURE__ */ jsx2(VectorSceneNodes, { nodes: node.children })
241
- },
242
- node.id
243
- ) : node.layers ? (
244
- // Fill and stroke stacks: each layer in order, bottom to top.
245
- /* @__PURE__ */ jsx2(
246
- "g",
247
- {
248
- "data-vector-id": node.id,
249
- transform: node.transform,
250
- opacity: node.opacity < 1 ? node.opacity : void 0,
251
- filter: node.filter,
252
- style: blendStyle(node.blendMode),
253
- children: node.layers.map((layer) => /* @__PURE__ */ jsx2(VectorPaintPath, { paint: layer, d: node.d, fillRule: node.fillRule, layer: `${layer.kind}-${layer.index}` }, `${layer.kind}-${layer.index}`))
254
- },
255
- node.id
256
- )
257
- ) : /* @__PURE__ */ jsx2(
258
- VectorPaintPath,
259
- {
260
- paint: node,
261
- d: node.d,
262
- fillRule: node.fillRule,
263
- id: node.id,
264
- transform: node.transform,
265
- filter: node.filter,
266
- blendMode: node.blendMode
267
- },
268
- node.id
269
- )
270
- ) });
230
+ return /* @__PURE__ */ jsx2(Fragment, { children: nodes.map((node) => /* @__PURE__ */ jsx2(VectorSceneNode, { node }, node.id)) });
271
231
  }
232
+ var VectorSceneNode = React2.memo(function VectorSceneNode2({ node }) {
233
+ if (node.kind === "text") return /* @__PURE__ */ jsx2(VectorTextElement, { node });
234
+ if (node.kind === "group")
235
+ return /* @__PURE__ */ jsx2("g", { "data-vector-id": node.id, "data-vector-mask": node.mask?.type, transform: node.transform, opacity: node.opacity < 1 ? node.opacity : void 0, filter: node.filter, style: blendStyle(node.blendMode), children: node.mask ? /* @__PURE__ */ jsx2(VectorMaskedNodes, { mask: node.mask, nodes: node.children }) : /* @__PURE__ */ jsx2(VectorSceneNodes, { nodes: node.children }) });
236
+ if (node.layers)
237
+ return (
238
+ // Fill and stroke stacks: each layer in order, bottom to top.
239
+ /* @__PURE__ */ jsx2("g", { "data-vector-id": node.id, transform: node.transform, opacity: node.opacity < 1 ? node.opacity : void 0, filter: node.filter, style: blendStyle(node.blendMode), children: node.layers.map((layer) => /* @__PURE__ */ jsx2(VectorPaintPath, { paint: layer, d: node.d, fillRule: node.fillRule, layer: `${layer.kind}-${layer.index}` }, `${layer.kind}-${layer.index}`)) })
240
+ );
241
+ return /* @__PURE__ */ jsx2(VectorPaintPath, { paint: node, d: node.d, fillRule: node.fillRule, id: node.id, transform: node.transform, filter: node.filter, blendMode: node.blendMode });
242
+ });
272
243
  function VectorMaskedNodes({ mask, nodes }) {
273
244
  const b = mask.bounds;
274
245
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
@@ -363,7 +334,8 @@ function VectorCanvas({
363
334
  const editingText = state.textEdit?.nodeId ?? null;
364
335
  const fitted = useRef3(!fitOnMount);
365
336
  const uid = useId().replace(/:/g, "");
366
- const scene = getVectorSceneRenderData(state, { idPrefix: `ve${uid}-` });
337
+ const [size, setSize] = useState2(null);
338
+ const scene = getVectorSceneRenderData(state, { idPrefix: `ve${uid}-`, ...size ? { viewportSize: size } : {} });
367
339
  const overlay = getVectorOverlayRenderData(state);
368
340
  const host = getVectorSymbolEditBackground(state, { idPrefix: `ve${uid}-host-` });
369
341
  useEffect2(() => {
@@ -381,6 +353,18 @@ function VectorCanvas({
381
353
  observer.observe(el);
382
354
  return () => observer.disconnect();
383
355
  }, [controller]);
356
+ useEffect2(() => {
357
+ const el = svgRef.current;
358
+ if (!el || typeof ResizeObserver === "undefined") return;
359
+ const measure = () => {
360
+ const r = el.getBoundingClientRect();
361
+ if (r.width > 0 && r.height > 0) setSize((s) => s && s.width === r.width && s.height === r.height ? s : { width: r.width, height: r.height });
362
+ };
363
+ measure();
364
+ const observer = new ResizeObserver(measure);
365
+ observer.observe(el);
366
+ return () => observer.disconnect();
367
+ }, []);
384
368
  useEffect2(() => {
385
369
  if (!editingText) return;
386
370
  const t = setTimeout(() => textRef.current?.focus({ preventScroll: true }), 0);
@@ -516,8 +500,8 @@ function VectorCanvas({
516
500
  /* @__PURE__ */ jsx2("circle", { cx: h.x, cy: h.y, r: THEME2.handleRadius + (h.hovered || h.active ? 1 : 0), fill: h.active ? accentColor : THEME2.handleFill, stroke: accentColor })
517
501
  ] }, h.index)),
518
502
  overlay.envelope.anchors.map((a) => {
519
- const size = as + (a.hovered || a.active ? 2 : 0);
520
- return /* @__PURE__ */ jsx2("rect", { "data-vector-envelope-anchor": a.index, x: a.x - size / 2, y: a.y - size / 2, width: size, height: size, fill: a.active ? accentColor : THEME2.handleFill, stroke: accentColor }, a.index);
503
+ const size2 = as + (a.hovered || a.active ? 2 : 0);
504
+ return /* @__PURE__ */ jsx2("rect", { "data-vector-envelope-anchor": a.index, x: a.x - size2 / 2, y: a.y - size2 / 2, width: size2, height: size2, fill: a.active ? accentColor : THEME2.handleFill, stroke: accentColor }, a.index);
521
505
  })
522
506
  ] }),
523
507
  overlay.handles.map((h) => /* @__PURE__ */ jsxs2("g", { "data-vector-handle": h.ref.handle, children: [
@@ -525,10 +509,10 @@ function VectorCanvas({
525
509
  /* @__PURE__ */ jsx2("circle", { cx: h.x, cy: h.y, r: THEME2.handleRadius + (h.hovered ? 1 : 0), fill: THEME2.handleFill, stroke: accentColor })
526
510
  ] }, `${h.ref.nodeId}:${h.ref.subpath}:${h.ref.index}:${h.ref.handle}`)),
527
511
  overlay.anchors.map((a) => {
528
- const size = as + (a.hovered ? 2 : 0);
512
+ const size2 = as + (a.hovered ? 2 : 0);
529
513
  const fill = a.selected ? accentColor : THEME2.handleFill;
530
514
  const key = `${a.ref.nodeId}:${a.ref.subpath}:${a.ref.index}`;
531
- return a.type === "corner" ? /* @__PURE__ */ jsx2("rect", { "data-vector-anchor": a.ref.index, x: a.x - size / 2, y: a.y - size / 2, width: size, height: size, fill, stroke: accentColor }, key) : /* @__PURE__ */ jsx2("circle", { "data-vector-anchor": a.ref.index, cx: a.x, cy: a.y, r: size / 2, fill, stroke: accentColor }, key);
515
+ return a.type === "corner" ? /* @__PURE__ */ jsx2("rect", { "data-vector-anchor": a.ref.index, x: a.x - size2 / 2, y: a.y - size2 / 2, width: size2, height: size2, fill, stroke: accentColor }, key) : /* @__PURE__ */ jsx2("circle", { "data-vector-anchor": a.ref.index, cx: a.x, cy: a.y, r: size2 / 2, fill, stroke: accentColor }, key);
532
516
  }),
533
517
  overlay.cornerWidgets.map((w) => /* @__PURE__ */ jsxs2("g", { "data-vector-corner-widget": w.ref.index, "data-corner-kind": w.kind, children: [
534
518
  /* @__PURE__ */ jsx2("circle", { cx: w.x, cy: w.y, r: THEME2.cornerWidgetRadius + (w.hovered ? 1 : 0), fill: THEME2.handleFill, stroke: accentColor }),
@@ -649,6 +633,7 @@ function VectorCanvas({
649
633
  sh.lines.map((d, i) => /* @__PURE__ */ jsx2("path", { d, stroke: accentColor, strokeWidth: 1, opacity: 0.7 }, i)),
650
634
  sh.points.map((p) => /* @__PURE__ */ jsx2("circle", { "data-vector-shading-point": p.index, "data-selected": p.selected || void 0, cx: p.x, cy: p.y, r: p.selected ? 6 : 4.5, fill: p.color, stroke: p.selected ? accentColor : "#ffffff", strokeWidth: p.selected ? 2 : 1.5 }, p.index))
651
635
  ] }, `shading:${sh.nodeId}`)),
636
+ overlay.symmetry.map((sy) => /* @__PURE__ */ jsx2("g", { "data-vector-symmetry-axes": sy.nodeId, stroke: accentColor, strokeDasharray: sy.drawing ? void 0 : "6 4", opacity: sy.drawing ? 0.9 : 0.6, children: sy.lines.map((l, i) => /* @__PURE__ */ jsx2("line", { x1: l.x1, y1: l.y1, x2: l.x2, y2: l.y2 }, i)) }, `sym:${sy.nodeId}`)),
652
637
  overlay.trackingPoints.map((t, i) => /* @__PURE__ */ jsx2("path", { "data-vector-tracking-point": "", d: `M${t.x - 5} ${t.y}H${t.x + 5}M${t.x} ${t.y - 5}V${t.y + 5}`, stroke: guideColor, strokeWidth: 1.5 }, `track:${i}`)),
653
638
  overlay.typedEntry && /* @__PURE__ */ jsxs2("g", { "data-vector-typed-entry": "", transform: `translate(${overlay.typedEntry.x + 14} ${overlay.typedEntry.y + 18})`, children: [
654
639
  /* @__PURE__ */ jsx2("rect", { x: -3, y: -12, width: overlay.typedEntry.text.length * 7 + 8, height: 17, rx: 3, fill: THEME2.handleFill, stroke: accentColor }),
@@ -807,7 +792,7 @@ import {
807
792
  isVectorMaskGroup,
808
793
  widthProfilePreset,
809
794
  widthProfilePresetId,
810
- vectorPaintToHex as vectorPaintToHex5,
795
+ vectorPaintToHex as vectorPaintToHex6,
811
796
  vectorToolLabel,
812
797
  vectorPolarAngles,
813
798
  vectorPolarIncrementOf
@@ -816,7 +801,8 @@ import {
816
801
  // src/vector/VectorEffectsPanel.tsx
817
802
  import {
818
803
  VECTOR_EFFECT_MENU,
819
- getVectorEffectsPanel
804
+ getVectorEffectsPanel,
805
+ vectorPaintToHex
820
806
  } from "@pixerate/editor/vector";
821
807
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
822
808
  var buttonClass = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
@@ -904,6 +890,15 @@ function VectorEffectsPanel({ controller, className }) {
904
890
  /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1 truncate", children: spec.label }),
905
891
  /* @__PURE__ */ jsx3("input", { type: "checkbox", "aria-label": `${e.label} ${spec.label}`, checked: !!e.params[spec.key], onChange: (ev) => set(e, spec.key, ev.target.checked, false) })
906
892
  ] }, spec.key);
893
+ if (spec.kind === "color") {
894
+ const c = e.params[spec.key];
895
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
896
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
897
+ /* @__PURE__ */ jsx3("input", { type: "checkbox", "aria-label": `${e.label} ${spec.label} on`, checked: c !== null, onChange: (ev) => set(e, spec.key, ev.target.checked ? "#ffffff" : null, false) }),
898
+ /* @__PURE__ */ jsx3("input", { type: "color", "aria-label": `${e.label} ${spec.label}`, className: "h-6 w-8 cursor-pointer rounded border border-border bg-background disabled:opacity-40", disabled: c === null, value: vectorPaintToHex(c ?? "#ffffff") ?? "#ffffff", onChange: (ev) => set(e, spec.key, ev.target.value, true), onBlur: end })
899
+ ] }, spec.key);
900
+ }
901
+ if (spec.kind !== "select") return null;
907
902
  return /* @__PURE__ */ jsxs3("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
908
903
  /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
909
904
  /* @__PURE__ */ jsx3("select", { "aria-label": `${e.label} ${spec.label}`, className: selectClass, value: String(e.params[spec.key] ?? ""), onChange: (ev) => set(e, spec.key, ev.target.value, false), children: spec.options.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: o.label }, o.value)) })
@@ -1584,8 +1579,8 @@ function VectorTextPanel({ controller, className }) {
1584
1579
  }
1585
1580
 
1586
1581
  // src/vector/VectorSymbolsPanel.tsx
1587
- import { useId as useId3, useState as useState2 } from "react";
1588
- import { getVectorSymbolsPanel, vectorPaintToHex } from "@pixerate/editor/vector";
1582
+ import { useId as useId3, useState as useState3 } from "react";
1583
+ import { getVectorSymbolsPanel, vectorPaintToHex as vectorPaintToHex2 } from "@pixerate/editor/vector";
1589
1584
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1590
1585
  var buttonClass6 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
1591
1586
  var selectClass5 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
@@ -1594,7 +1589,7 @@ function VectorSymbolsPanel({ controller, className }) {
1594
1589
  const state = useVectorEditorState(controller);
1595
1590
  const uid = useId3().replace(/:/g, "");
1596
1591
  const panel = getVectorSymbolsPanel(state, { idPrefix: `vsym${uid}-` });
1597
- const [error, setError] = useState2(null);
1592
+ const [error, setError] = useState3(null);
1598
1593
  const report = (r) => setError(r.ok ? null : r.message ?? null);
1599
1594
  const inst = panel.instance;
1600
1595
  return /* @__PURE__ */ jsxs8("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-symbols-panel": "", "aria-label": "Symbols", children: [
@@ -1639,7 +1634,7 @@ function VectorSymbolsPanel({ controller, className }) {
1639
1634
  type: "color",
1640
1635
  "aria-label": "Instance fill",
1641
1636
  className: "h-6 w-8 rounded border border-border",
1642
- value: vectorPaintToHex(inst.fill ?? void 0) ?? "#000000",
1637
+ value: vectorPaintToHex2(inst.fill ?? void 0) ?? "#000000",
1643
1638
  onChange: (e) => report(controller.setInstanceOptions({ fill: e.target.value }, { ids: [inst.id] }))
1644
1639
  }
1645
1640
  )
@@ -1652,7 +1647,7 @@ function VectorSymbolsPanel({ controller, className }) {
1652
1647
  }
1653
1648
 
1654
1649
  // src/vector/VectorConstraintsPanel.tsx
1655
- import { useState as useState3 } from "react";
1650
+ import { useState as useState4 } from "react";
1656
1651
  import { getVectorConstraintPanel } from "@pixerate/editor/vector";
1657
1652
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1658
1653
  var buttonClass7 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
@@ -1661,8 +1656,8 @@ var labelClass6 = "w-16 text-[11px] font-medium uppercase tracking-wide text-mut
1661
1656
  function VectorConstraintsPanel({ controller, className }) {
1662
1657
  const state = useVectorEditorState(controller);
1663
1658
  const panel = getVectorConstraintPanel(state);
1664
- const [error, setError] = useState3(null);
1665
- const [param, setParam] = useState3({ name: "", value: "" });
1659
+ const [error, setError] = useState4(null);
1660
+ const [param, setParam] = useState4({ name: "", value: "" });
1666
1661
  const report = (r) => setError(r.ok ? null : r.message ?? null);
1667
1662
  const add = (a) => report(controller.addConstraint({ kind: a.kind, refs: a.refs }));
1668
1663
  if (!panel.actions.length && !panel.constraints.length && !state.document.constraints?.length && state.tool !== "direct") return null;
@@ -1745,7 +1740,7 @@ import {
1745
1740
  VECTOR_VERTICAL_RESIZING,
1746
1741
  VECTOR_VERTICAL_RESIZING_LABELS,
1747
1742
  getVectorFramePanel,
1748
- vectorPaintToHex as vectorPaintToHex2
1743
+ vectorPaintToHex as vectorPaintToHex3
1749
1744
  } from "@pixerate/editor/vector";
1750
1745
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1751
1746
  var buttonClass8 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
@@ -1756,7 +1751,7 @@ function VectorFramePanel({ controller, className }) {
1756
1751
  const panel = getVectorFramePanel(state);
1757
1752
  const f = panel.frame;
1758
1753
  const ids = f ? [f.id] : void 0;
1759
- const fillHex = f ? vectorPaintToHex2(f.frame.fill ?? null) : null;
1754
+ const fillHex = f ? vectorPaintToHex3(f.frame.fill ?? null) : null;
1760
1755
  const number = (key, label) => /* @__PURE__ */ jsxs10("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1761
1756
  /* @__PURE__ */ jsx10("span", { className: "w-4 text-muted-foreground", children: label }),
1762
1757
  /* @__PURE__ */ jsx10(
@@ -1930,15 +1925,15 @@ function VectorConnectorPanel({ controller, className }) {
1930
1925
  }
1931
1926
 
1932
1927
  // src/vector/VectorShadingPanel.tsx
1933
- import { useState as useState4 } from "react";
1934
- import { isVectorFreeformGradient, isVectorMeshGradient, vectorPaintToHex as vectorPaintToHex3 } from "@pixerate/editor/vector";
1928
+ import { useState as useState5 } from "react";
1929
+ import { isVectorFreeformGradient, isVectorMeshGradient, vectorPaintToHex as vectorPaintToHex4 } from "@pixerate/editor/vector";
1935
1930
  import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1936
1931
  var buttonClass10 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
1937
1932
  var numberClass6 = "h-7 w-12 rounded border border-border bg-background px-1.5 text-xs";
1938
1933
  function VectorShadingPanel({ controller, className }) {
1939
1934
  const state = useVectorEditorState(controller);
1940
- const [rows, setRows] = useState4(2);
1941
- const [columns, setColumns] = useState4(2);
1935
+ const [rows, setRows] = useState5(2);
1936
+ const [columns, setColumns] = useState5(2);
1942
1937
  const path = state.selection.map((id) => controller.getNode(id)).find((n) => n?.type === "path");
1943
1938
  if (!path) return null;
1944
1939
  const fill = path.fill;
@@ -1971,7 +1966,7 @@ function VectorShadingPanel({ controller, className }) {
1971
1966
  "Point ",
1972
1967
  sel.index
1973
1968
  ] }),
1974
- /* @__PURE__ */ jsx12("input", { type: "color", "aria-label": "Point colour", className: "h-7 w-10 cursor-pointer rounded border border-border bg-background", value: vectorPaintToHex3(color ?? "#000000") ?? "#000000", onChange: (e) => edit({ color: e.target.value }, "shading-color") }),
1969
+ /* @__PURE__ */ jsx12("input", { type: "color", "aria-label": "Point colour", className: "h-7 w-10 cursor-pointer rounded border border-border bg-background", value: vectorPaintToHex4(color ?? "#000000") ?? "#000000", onChange: (e) => edit({ color: e.target.value }, "shading-color") }),
1975
1970
  /* @__PURE__ */ jsx12("input", { type: "range", min: 0, max: 1, step: 0.05, "aria-label": "Point opacity", title: "Opacity", className: "h-4 w-20 accent-primary", value: opacity, onChange: (e) => edit({ opacity: parseFloat(e.target.value) }, "shading-opacity") }),
1976
1971
  free && /* @__PURE__ */ jsxs12(Fragment3, { children: [
1977
1972
  /* @__PURE__ */ jsx12("input", { type: "range", min: 0, max: 1, step: 0.05, "aria-label": "Point spread", title: "Spread: pure colour round the point", className: "h-4 w-20 accent-primary", value: spread, onChange: (e) => edit({ spread: parseFloat(e.target.value) }, "shading-spread") }),
@@ -1982,7 +1977,7 @@ function VectorShadingPanel({ controller, className }) {
1982
1977
  }
1983
1978
 
1984
1979
  // src/vector/VectorTracePanel.tsx
1985
- import { useRef as useRef4, useState as useState5 } from "react";
1980
+ import { useRef as useRef4, useState as useState6 } from "react";
1986
1981
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1987
1982
  var buttonClass11 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
1988
1983
  var selectClass8 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
@@ -2001,9 +1996,9 @@ async function filePixels(file, max = 1e6) {
2001
1996
  }
2002
1997
  function VectorTracePanel({ controller, className }) {
2003
1998
  const input = useRef4(null);
2004
- const [mode, setMode] = useState5("threshold");
2005
- const [colors, setColors] = useState5(6);
2006
- const [message, setMessage] = useState5(null);
1999
+ const [mode, setMode] = useState6("threshold");
2000
+ const [colors, setColors] = useState6(6);
2001
+ const [message, setMessage] = useState6(null);
2007
2002
  const trace = async (file) => {
2008
2003
  try {
2009
2004
  const pixels = await filePixels(file);
@@ -2043,6 +2038,43 @@ function VectorTracePanel({ controller, className }) {
2043
2038
  ] });
2044
2039
  }
2045
2040
 
2041
+ // src/vector/VectorSymmetryPanel.tsx
2042
+ import { VECTOR_SYMMETRY_MODES, VECTOR_SYMMETRY_MODE_LABELS, isVectorSymmetryGroup } from "@pixerate/editor/vector";
2043
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2044
+ var buttonClass12 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
2045
+ var selectClass9 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
2046
+ var numberClass8 = "h-7 w-12 rounded border border-border bg-background px-1.5 text-xs";
2047
+ function VectorSymmetryPanel({ controller, className }) {
2048
+ const state = useVectorEditorState(controller);
2049
+ const group = state.selection.map((id) => controller.getNode(id)).find(isVectorSymmetryGroup) ?? null;
2050
+ const s = group?.symmetry;
2051
+ const ids = group ? [group.id] : void 0;
2052
+ const drawing = !!group && state.symmetryTarget === group.id;
2053
+ return /* @__PURE__ */ jsxs14("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-symmetry-panel": "", "aria-label": "Symmetry", children: [
2054
+ /* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap items-center gap-1", children: [
2055
+ /* @__PURE__ */ jsx14("span", { className: "w-16 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Symmetry" }),
2056
+ !group && /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, "data-vector-symmetry-action": "make", title: "Make Symmetry: the selection (or nothing yet) mirrored live across a vertical axis", onClick: () => controller.makeSymmetry({ mode: "vertical" }), children: "Mirror" }),
2057
+ !group && /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, "data-vector-symmetry-action": "radial", title: "Make Symmetry: the selection repeated round its centre", onClick: () => controller.makeSymmetry({ mode: "radial", copies: 6 }), children: "Radial" }),
2058
+ group && s && /* @__PURE__ */ jsxs14(Fragment4, { children: [
2059
+ /* @__PURE__ */ jsx14("select", { "aria-label": "Symmetry mode", className: selectClass9, value: s.mode, onChange: (e) => controller.setSymmetry({ mode: e.target.value }, { ids }), children: VECTOR_SYMMETRY_MODES.map((m) => /* @__PURE__ */ jsx14("option", { value: m, children: VECTOR_SYMMETRY_MODE_LABELS[m] }, m)) }),
2060
+ s.mode === "radial" && /* @__PURE__ */ jsxs14(Fragment4, { children: [
2061
+ /* @__PURE__ */ jsx14("input", { type: "number", min: 2, max: 64, "aria-label": "Symmetry copies", title: "Copies round the circle", className: numberClass8, value: s.copies ?? 6, onChange: (e) => controller.setSymmetry({ copies: Math.max(2, Math.min(64, parseInt(e.target.value, 10) || 2)) }, { ids }) }),
2062
+ /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1 text-xs text-foreground", title: "Mirror each copy too (kaleidoscope)", children: [
2063
+ /* @__PURE__ */ jsx14("input", { type: "checkbox", "aria-label": "Mirror copies", checked: !!s.mirror, onChange: (e) => controller.setSymmetry({ mirror: e.target.checked }, { ids }) }),
2064
+ "Mirror"
2065
+ ] })
2066
+ ] })
2067
+ ] })
2068
+ ] }),
2069
+ group && /* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap items-center gap-1", children: [
2070
+ /* @__PURE__ */ jsx14("span", { className: "w-16" }),
2071
+ /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, "aria-pressed": drawing, "data-vector-symmetry-action": "draw", title: "Draw inside: new pen, pencil, shape and text art goes into the group, mirrored as you draw (Escape stops)", onClick: () => drawing ? controller.exitSymmetry() : controller.enterSymmetry(group.id), children: "Draw inside" }),
2072
+ /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, title: "Expand: keep the copies as real objects", onClick: () => controller.releaseSymmetry({ ids, expand: true }), children: "Expand" }),
2073
+ /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, title: "Release: stop repeating (the copies go)", onClick: () => controller.releaseSymmetry({ ids }), children: "Release" })
2074
+ ] })
2075
+ ] });
2076
+ }
2077
+
2046
2078
  // src/vector/VectorGradientEditor.tsx
2047
2079
  import {
2048
2080
  VECTOR_GRADIENT_TYPES,
@@ -2054,12 +2086,12 @@ import {
2054
2086
  VECTOR_GRADIENT_INTERPOLATION_LABELS,
2055
2087
  VECTOR_STROKE_GRADIENT_MODE_LABELS,
2056
2088
  getVectorGradientEditor,
2057
- vectorPaintToHex as vectorPaintToHex4
2089
+ vectorPaintToHex as vectorPaintToHex5
2058
2090
  } from "@pixerate/editor/vector";
2059
- import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2060
- var selectClass9 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
2061
- var numberClass8 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
2062
- var buttonClass12 = "inline-flex h-7 min-w-7 items-center justify-center rounded border border-border bg-background px-1.5 text-xs text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
2091
+ import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
2092
+ var selectClass10 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
2093
+ var numberClass9 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
2094
+ var buttonClass13 = "inline-flex h-7 min-w-7 items-center justify-center rounded border border-border bg-background px-1.5 text-xs text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
2063
2095
  var percent = (v) => Math.round(v * 1e3) / 10;
2064
2096
  function VectorGradientEditor({ controller, className }) {
2065
2097
  const state = useVectorEditorState(controller);
@@ -2071,26 +2103,26 @@ function VectorGradientEditor({ controller, className }) {
2071
2103
  const setType = (value) => {
2072
2104
  if (value === "solid") {
2073
2105
  const first = ed.stops[0];
2074
- controller.setStyle({ [ed.target]: vectorPaintToHex4(first?.color) ?? first?.color ?? "#000000" }, void 0, { label: "Solid Colour" });
2106
+ controller.setStyle({ [ed.target]: vectorPaintToHex5(first?.color) ?? first?.color ?? "#000000" }, void 0, { label: "Solid Colour" });
2075
2107
  } else controller.setGradient({ type: value });
2076
2108
  };
2077
- return /* @__PURE__ */ jsxs14("div", { className: `space-y-1.5 ${className ?? ""}`, "data-vector-gradient-editor": ed.target, "aria-label": "Gradient", children: [
2078
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1.5", children: [
2079
- /* @__PURE__ */ jsx14("span", { className: "w-16 text-xs text-foreground", children: "Gradient" }),
2080
- ["fill", "stroke"].map((t) => /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, "aria-pressed": ed.target === t, onClick: () => controller.setGradientTarget(t), children: t === "fill" ? "Fill" : "Stroke" }, t)),
2081
- ed.mixed && /* @__PURE__ */ jsx14("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
2109
+ return /* @__PURE__ */ jsxs15("div", { className: `space-y-1.5 ${className ?? ""}`, "data-vector-gradient-editor": ed.target, "aria-label": "Gradient", children: [
2110
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1.5", children: [
2111
+ /* @__PURE__ */ jsx15("span", { className: "w-16 text-xs text-foreground", children: "Gradient" }),
2112
+ ["fill", "stroke"].map((t) => /* @__PURE__ */ jsx15("button", { type: "button", className: buttonClass13, "aria-pressed": ed.target === t, onClick: () => controller.setGradientTarget(t), children: t === "fill" ? "Fill" : "Stroke" }, t)),
2113
+ ed.mixed && /* @__PURE__ */ jsx15("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
2082
2114
  ] }),
2083
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1.5", children: [
2084
- /* @__PURE__ */ jsx14("span", { className: "w-16 text-xs text-foreground", children: "Type" }),
2085
- /* @__PURE__ */ jsxs14("select", { "aria-label": "Gradient type", className: selectClass9, value: ed.type ?? "solid", onChange: (e) => setType(e.target.value), children: [
2086
- /* @__PURE__ */ jsx14("option", { value: "solid", children: "Solid" }),
2087
- VECTOR_GRADIENT_TYPES.map((t) => /* @__PURE__ */ jsx14("option", { value: t, children: VECTOR_GRADIENT_TYPE_LABELS[t] }, t))
2115
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1.5", children: [
2116
+ /* @__PURE__ */ jsx15("span", { className: "w-16 text-xs text-foreground", children: "Type" }),
2117
+ /* @__PURE__ */ jsxs15("select", { "aria-label": "Gradient type", className: selectClass10, value: ed.type ?? "solid", onChange: (e) => setType(e.target.value), children: [
2118
+ /* @__PURE__ */ jsx15("option", { value: "solid", children: "Solid" }),
2119
+ VECTOR_GRADIENT_TYPES.map((t) => /* @__PURE__ */ jsx15("option", { value: t, children: VECTOR_GRADIENT_TYPE_LABELS[t] }, t))
2088
2120
  ] }),
2089
- /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
2121
+ /* @__PURE__ */ jsx15("button", { type: "button", className: buttonClass13, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
2090
2122
  ] }),
2091
- g && /* @__PURE__ */ jsxs14(Fragment4, { children: [
2092
- /* @__PURE__ */ jsxs14("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
2093
- /* @__PURE__ */ jsx14(
2123
+ g && /* @__PURE__ */ jsxs15(Fragment5, { children: [
2124
+ /* @__PURE__ */ jsxs15("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
2125
+ /* @__PURE__ */ jsx15(
2094
2126
  "div",
2095
2127
  {
2096
2128
  role: "button",
@@ -2104,7 +2136,7 @@ function VectorGradientEditor({ controller, className }) {
2104
2136
  }
2105
2137
  }
2106
2138
  ),
2107
- ed.stops.map((s) => /* @__PURE__ */ jsx14(
2139
+ ed.stops.map((s) => /* @__PURE__ */ jsx15(
2108
2140
  "button",
2109
2141
  {
2110
2142
  type: "button",
@@ -2118,21 +2150,21 @@ function VectorGradientEditor({ controller, className }) {
2118
2150
  s.index
2119
2151
  ))
2120
2152
  ] }),
2121
- sel && /* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap items-center gap-1.5", "data-vector-gradient-selected-stop": sel.index, children: [
2122
- /* @__PURE__ */ jsx14(
2153
+ sel && /* @__PURE__ */ jsxs15("div", { className: "flex flex-wrap items-center gap-1.5", "data-vector-gradient-selected-stop": sel.index, children: [
2154
+ /* @__PURE__ */ jsx15(
2123
2155
  "input",
2124
2156
  {
2125
2157
  type: "color",
2126
2158
  "aria-label": "Stop colour",
2127
2159
  className: "h-7 w-9 cursor-pointer rounded border border-border bg-background",
2128
- value: vectorPaintToHex4(sel.color) ?? "#000000",
2160
+ value: vectorPaintToHex5(sel.color) ?? "#000000",
2129
2161
  onChange: (e) => controller.setGradientStop(sel.index, { color: e.target.value }, { merge: `gradient-stop-color-${sel.index}` }),
2130
2162
  onBlur: end
2131
2163
  }
2132
2164
  ),
2133
- /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
2165
+ /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
2134
2166
  "Opacity",
2135
- /* @__PURE__ */ jsx14(
2167
+ /* @__PURE__ */ jsx15(
2136
2168
  "input",
2137
2169
  {
2138
2170
  type: "number",
@@ -2140,7 +2172,7 @@ function VectorGradientEditor({ controller, className }) {
2140
2172
  max: 100,
2141
2173
  step: 5,
2142
2174
  "aria-label": "Stop opacity",
2143
- className: numberClass8,
2175
+ className: numberClass9,
2144
2176
  value: percent(sel.opacity ?? 1),
2145
2177
  onChange: (e) => {
2146
2178
  const n = parseFloat(e.target.value);
@@ -2149,9 +2181,9 @@ function VectorGradientEditor({ controller, className }) {
2149
2181
  }
2150
2182
  )
2151
2183
  ] }),
2152
- /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
2184
+ /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
2153
2185
  "At",
2154
- /* @__PURE__ */ jsx14(
2186
+ /* @__PURE__ */ jsx15(
2155
2187
  "input",
2156
2188
  {
2157
2189
  type: "number",
@@ -2159,7 +2191,7 @@ function VectorGradientEditor({ controller, className }) {
2159
2191
  max: 100,
2160
2192
  step: 1,
2161
2193
  "aria-label": "Stop position",
2162
- className: numberClass8,
2194
+ className: numberClass9,
2163
2195
  value: percent(sel.offset),
2164
2196
  onChange: (e) => {
2165
2197
  const n = parseFloat(e.target.value);
@@ -2168,9 +2200,9 @@ function VectorGradientEditor({ controller, className }) {
2168
2200
  }
2169
2201
  )
2170
2202
  ] }),
2171
- !sel.last && /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
2203
+ !sel.last && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
2172
2204
  "Midpoint",
2173
- /* @__PURE__ */ jsx14(
2205
+ /* @__PURE__ */ jsx15(
2174
2206
  "input",
2175
2207
  {
2176
2208
  type: "number",
@@ -2178,7 +2210,7 @@ function VectorGradientEditor({ controller, className }) {
2178
2210
  max: 95,
2179
2211
  step: 1,
2180
2212
  "aria-label": "Stop midpoint",
2181
- className: numberClass8,
2213
+ className: numberClass9,
2182
2214
  value: percent(sel.midpoint ?? 0.5),
2183
2215
  onChange: (e) => {
2184
2216
  const n = parseFloat(e.target.value);
@@ -2187,15 +2219,15 @@ function VectorGradientEditor({ controller, className }) {
2187
2219
  }
2188
2220
  )
2189
2221
  ] }),
2190
- /* @__PURE__ */ jsx14("button", { type: "button", className: buttonClass12, "aria-label": "Delete stop", title: "Delete stop", disabled: ed.stops.length <= 2, onClick: () => controller.removeGradientStop(sel.index), children: "\u2715" })
2222
+ /* @__PURE__ */ jsx15("button", { type: "button", className: buttonClass13, "aria-label": "Delete stop", title: "Delete stop", disabled: ed.stops.length <= 2, onClick: () => controller.removeGradientStop(sel.index), children: "\u2715" })
2191
2223
  ] }),
2192
- /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2193
- /* @__PURE__ */ jsx14("span", { className: "w-16", children: "Colours" }),
2194
- /* @__PURE__ */ jsx14("select", { "aria-label": "Gradient interpolation", className: selectClass9, value: ed.interpolation, onChange: (e) => controller.setGradient({ interpolation: e.target.value }), children: VECTOR_GRADIENT_INTERPOLATIONS.map((i) => /* @__PURE__ */ jsx14("option", { value: i, children: VECTOR_GRADIENT_INTERPOLATION_LABELS[i] }, i)) })
2224
+ /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2225
+ /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Colours" }),
2226
+ /* @__PURE__ */ jsx15("select", { "aria-label": "Gradient interpolation", className: selectClass10, value: ed.interpolation, onChange: (e) => controller.setGradient({ interpolation: e.target.value }), children: VECTOR_GRADIENT_INTERPOLATIONS.map((i) => /* @__PURE__ */ jsx15("option", { value: i, children: VECTOR_GRADIENT_INTERPOLATION_LABELS[i] }, i)) })
2195
2227
  ] }),
2196
- /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2197
- /* @__PURE__ */ jsx14("span", { className: "w-16", children: "Smooth" }),
2198
- /* @__PURE__ */ jsx14(
2228
+ /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2229
+ /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Smooth" }),
2230
+ /* @__PURE__ */ jsx15(
2199
2231
  "input",
2200
2232
  {
2201
2233
  type: "range",
@@ -2211,23 +2243,23 @@ function VectorGradientEditor({ controller, className }) {
2211
2243
  }
2212
2244
  )
2213
2245
  ] }),
2214
- /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2215
- /* @__PURE__ */ jsx14("span", { className: "w-16", children: "Spread" }),
2216
- /* @__PURE__ */ jsx14("select", { "aria-label": "Gradient spread", className: selectClass9, value: ed.spread, onChange: (e) => controller.setGradient({ spread: e.target.value }), children: VECTOR_GRADIENT_SPREADS.map((sp) => /* @__PURE__ */ jsx14("option", { value: sp, children: VECTOR_GRADIENT_SPREAD_LABELS[sp] }, sp)) })
2246
+ /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2247
+ /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Spread" }),
2248
+ /* @__PURE__ */ jsx15("select", { "aria-label": "Gradient spread", className: selectClass10, value: ed.spread, onChange: (e) => controller.setGradient({ spread: e.target.value }), children: VECTOR_GRADIENT_SPREADS.map((sp) => /* @__PURE__ */ jsx15("option", { value: sp, children: VECTOR_GRADIENT_SPREAD_LABELS[sp] }, sp)) })
2217
2249
  ] }),
2218
- ed.target === "stroke" && /* @__PURE__ */ jsxs14("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2219
- /* @__PURE__ */ jsx14("span", { className: "w-16", children: "On stroke" }),
2220
- /* @__PURE__ */ jsx14("select", { "aria-label": "Gradient on stroke", className: selectClass9, value: ed.strokeMode, onChange: (e) => controller.setGradient({ strokeMode: e.target.value }), children: VECTOR_STROKE_GRADIENT_MODES.map((m) => /* @__PURE__ */ jsx14("option", { value: m, children: VECTOR_STROKE_GRADIENT_MODE_LABELS[m] }, m)) })
2250
+ ed.target === "stroke" && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
2251
+ /* @__PURE__ */ jsx15("span", { className: "w-16", children: "On stroke" }),
2252
+ /* @__PURE__ */ jsx15("select", { "aria-label": "Gradient on stroke", className: selectClass10, value: ed.strokeMode, onChange: (e) => controller.setGradient({ strokeMode: e.target.value }), children: VECTOR_STROKE_GRADIENT_MODES.map((m) => /* @__PURE__ */ jsx15("option", { value: m, children: VECTOR_STROKE_GRADIENT_MODE_LABELS[m] }, m)) })
2221
2253
  ] })
2222
2254
  ] })
2223
2255
  ] });
2224
2256
  }
2225
2257
 
2226
2258
  // src/vector/VectorToolbar.tsx
2227
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
2259
+ import { Fragment as Fragment6, jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2228
2260
  var VECTOR_SNAP_TITLE = "Snap to objects";
2229
2261
  var modKeyLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "\u2318" : "Ctrl";
2230
- var buttonClass13 = "inline-flex h-8 min-w-8 items-center justify-center rounded-md border border-border bg-background px-2 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
2262
+ var buttonClass14 = "inline-flex h-8 min-w-8 items-center justify-center rounded-md border border-border bg-background px-2 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
2231
2263
  function VectorToolbar({
2232
2264
  controller,
2233
2265
  tools = VECTOR_TOOLS,
@@ -2247,14 +2279,14 @@ function VectorToolbar({
2247
2279
  const maskSelected = state.selection.some((id) => isVectorMaskGroup(controller.getNode(id)));
2248
2280
  const [live, setLive] = React9.useState(false);
2249
2281
  const booleanDisabled = live ? count < 1 : count < 2;
2250
- const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx15("button", { type: "button", className: buttonClass13, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
2251
- return /* @__PURE__ */ jsxs15("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
2252
- /* @__PURE__ */ jsx15("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx15(
2282
+ const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx16("button", { type: "button", className: buttonClass14, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
2283
+ return /* @__PURE__ */ jsxs16("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
2284
+ /* @__PURE__ */ jsx16("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx16(
2253
2285
  "button",
2254
2286
  {
2255
2287
  type: "button",
2256
2288
  "data-vector-tool": tool,
2257
- className: buttonClass13,
2289
+ className: buttonClass14,
2258
2290
  "aria-pressed": state.tool === tool,
2259
2291
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
2260
2292
  "aria-label": vectorToolLabel(tool),
@@ -2263,17 +2295,17 @@ function VectorToolbar({
2263
2295
  },
2264
2296
  tool
2265
2297
  )) }),
2266
- showHistory && /* @__PURE__ */ jsxs15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
2298
+ showHistory && /* @__PURE__ */ jsxs16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
2267
2299
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
2268
2300
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
2269
2301
  ] }),
2270
- showSnap && /* @__PURE__ */ jsxs15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: [
2271
- /* @__PURE__ */ jsx15(
2302
+ showSnap && /* @__PURE__ */ jsxs16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: [
2303
+ /* @__PURE__ */ jsx16(
2272
2304
  "button",
2273
2305
  {
2274
2306
  type: "button",
2275
2307
  "data-vector-snap": "",
2276
- className: buttonClass13,
2308
+ className: buttonClass14,
2277
2309
  "aria-pressed": state.snap.objects,
2278
2310
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
2279
2311
  "aria-label": VECTOR_SNAP_TITLE,
@@ -2281,7 +2313,7 @@ function VectorToolbar({
2281
2313
  children: "Snap"
2282
2314
  }
2283
2315
  ),
2284
- /* @__PURE__ */ jsxs15(
2316
+ /* @__PURE__ */ jsxs16(
2285
2317
  "select",
2286
2318
  {
2287
2319
  "data-vector-polar": "",
@@ -2291,8 +2323,8 @@ function VectorToolbar({
2291
2323
  value: String(vectorPolarIncrementOf(state.snap.angles) ?? ""),
2292
2324
  onChange: (e) => controller.setSnap({ angles: vectorPolarAngles(Number(e.target.value)) }),
2293
2325
  children: [
2294
- vectorPolarIncrementOf(state.snap.angles) === null && /* @__PURE__ */ jsx15("option", { value: "", children: "Custom" }),
2295
- [90, 45, 30, 15].map((n) => /* @__PURE__ */ jsxs15("option", { value: n, children: [
2326
+ vectorPolarIncrementOf(state.snap.angles) === null && /* @__PURE__ */ jsx16("option", { value: "", children: "Custom" }),
2327
+ [90, 45, 30, 15].map((n) => /* @__PURE__ */ jsxs16("option", { value: n, children: [
2296
2328
  n,
2297
2329
  "\xB0"
2298
2330
  ] }, n))
@@ -2300,13 +2332,13 @@ function VectorToolbar({
2300
2332
  }
2301
2333
  )
2302
2334
  ] }),
2303
- showPathEdit && /* @__PURE__ */ jsxs15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
2304
- VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx15(
2335
+ showPathEdit && /* @__PURE__ */ jsxs16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
2336
+ VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx16(
2305
2337
  "button",
2306
2338
  {
2307
2339
  type: "button",
2308
2340
  "data-vector-path-action": a.id,
2309
- className: buttonClass13,
2341
+ className: buttonClass14,
2310
2342
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
2311
2343
  "aria-label": a.title,
2312
2344
  disabled: !a.enabled(count, points),
@@ -2315,12 +2347,12 @@ function VectorToolbar({
2315
2347
  },
2316
2348
  a.id
2317
2349
  )),
2318
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx15(
2350
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx16(
2319
2351
  "button",
2320
2352
  {
2321
2353
  type: "button",
2322
2354
  "data-vector-edit": t.key,
2323
- className: buttonClass13,
2355
+ className: buttonClass14,
2324
2356
  "aria-pressed": editing[t.key],
2325
2357
  title: t.title,
2326
2358
  "aria-label": t.title,
@@ -2329,9 +2361,9 @@ function VectorToolbar({
2329
2361
  },
2330
2362
  t.key
2331
2363
  )),
2332
- state.tool === "pencil" && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
2364
+ state.tool === "pencil" && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
2333
2365
  "Smoothing",
2334
- /* @__PURE__ */ jsx15(
2366
+ /* @__PURE__ */ jsx16(
2335
2367
  "input",
2336
2368
  {
2337
2369
  type: "range",
@@ -2345,9 +2377,9 @@ function VectorToolbar({
2345
2377
  }
2346
2378
  )
2347
2379
  ] }),
2348
- state.tool === "eraser" && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
2380
+ state.tool === "eraser" && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
2349
2381
  "Size",
2350
- /* @__PURE__ */ jsx15(
2382
+ /* @__PURE__ */ jsx16(
2351
2383
  "input",
2352
2384
  {
2353
2385
  type: "number",
@@ -2363,12 +2395,12 @@ function VectorToolbar({
2363
2395
  }
2364
2396
  )
2365
2397
  ] }),
2366
- state.tool === "paintBucket" && /* @__PURE__ */ jsx15(
2398
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx16(
2367
2399
  "button",
2368
2400
  {
2369
2401
  type: "button",
2370
2402
  "data-vector-fill-gap": "",
2371
- className: buttonClass13,
2403
+ className: buttonClass14,
2372
2404
  "aria-pressed": editing.fillGap > 0,
2373
2405
  title: "Close small gaps between path ends when filling",
2374
2406
  "aria-label": "Close gaps",
@@ -2377,12 +2409,12 @@ function VectorToolbar({
2377
2409
  }
2378
2410
  )
2379
2411
  ] }),
2380
- showDistort && /* @__PURE__ */ jsx15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Distort", children: /* @__PURE__ */ jsx15(
2412
+ showDistort && /* @__PURE__ */ jsx16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Distort", children: /* @__PURE__ */ jsx16(
2381
2413
  "button",
2382
2414
  {
2383
2415
  type: "button",
2384
2416
  "data-vector-free-distort": "",
2385
- className: buttonClass13,
2417
+ className: buttonClass14,
2386
2418
  "aria-pressed": state.freeDistort,
2387
2419
  title: `Free Distort: drag a corner of the selection box to distort in perspective (Shift+Alt+${modKeyLabel()}: symmetric perspective; ${modKeyLabel()}-drag a side to skew)`,
2388
2420
  "aria-label": "Free Distort",
@@ -2390,15 +2422,15 @@ function VectorToolbar({
2390
2422
  children: "Distort"
2391
2423
  }
2392
2424
  ) }),
2393
- showBlend && /* @__PURE__ */ jsx15(VectorBlendControls, { controller }),
2394
- showArrange && /* @__PURE__ */ jsxs15(Fragment5, { children: [
2395
- /* @__PURE__ */ jsxs15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
2396
- /* @__PURE__ */ jsx15(
2425
+ showBlend && /* @__PURE__ */ jsx16(VectorBlendControls, { controller }),
2426
+ showArrange && /* @__PURE__ */ jsxs16(Fragment6, { children: [
2427
+ /* @__PURE__ */ jsxs16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
2428
+ /* @__PURE__ */ jsx16(
2397
2429
  "button",
2398
2430
  {
2399
2431
  type: "button",
2400
2432
  "data-vector-live-boolean": "",
2401
- className: buttonClass13,
2433
+ className: buttonClass14,
2402
2434
  "aria-pressed": live,
2403
2435
  title: "Live booleans: keep the shapes editable",
2404
2436
  "aria-label": "Live booleans",
@@ -2415,12 +2447,12 @@ function VectorToolbar({
2415
2447
  act("\u25D0", `Make Clipping Mask (${modKeyLabel()}+7)`, () => controller.makeMask("clip"), count < 2),
2416
2448
  act("\u25D1", `Release Mask (${modKeyLabel()}+Alt+7)`, () => controller.releaseMask(), !maskSelected)
2417
2449
  ] }),
2418
- /* @__PURE__ */ jsx15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx15(
2450
+ /* @__PURE__ */ jsx16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx16(
2419
2451
  "button",
2420
2452
  {
2421
2453
  type: "button",
2422
2454
  "data-vector-pathfinder": a.id,
2423
- className: buttonClass13,
2455
+ className: buttonClass14,
2424
2456
  title: a.title,
2425
2457
  "aria-label": a.title,
2426
2458
  disabled: count < a.min,
@@ -2429,7 +2461,7 @@ function VectorToolbar({
2429
2461
  },
2430
2462
  a.id
2431
2463
  )) }),
2432
- /* @__PURE__ */ jsxs15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
2464
+ /* @__PURE__ */ jsxs16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
2433
2465
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
2434
2466
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
2435
2467
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -2447,19 +2479,19 @@ function VectorBlendControls({ controller }) {
2447
2479
  const blend = getVectorBlendControls(state);
2448
2480
  const o = blend.options;
2449
2481
  const selected = blend.ids.length > 0;
2450
- return /* @__PURE__ */ jsxs15("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
2451
- /* @__PURE__ */ jsx15("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass13, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
2452
- /* @__PURE__ */ jsx15(
2482
+ return /* @__PURE__ */ jsxs16("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
2483
+ /* @__PURE__ */ jsx16("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass14, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
2484
+ /* @__PURE__ */ jsx16(
2453
2485
  "select",
2454
2486
  {
2455
2487
  "aria-label": "Blend spacing",
2456
2488
  className: "h-7 rounded border border-border bg-background px-1 text-xs",
2457
2489
  value: o.spacing,
2458
2490
  onChange: (e) => applyVectorBlendOptions(controller, { spacing: e.target.value }),
2459
- children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx15("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
2491
+ children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx16("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
2460
2492
  }
2461
2493
  ),
2462
- o.spacing !== "smoothColor" && /* @__PURE__ */ jsx15(
2494
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx16(
2463
2495
  "input",
2464
2496
  {
2465
2497
  type: "number",
@@ -2475,12 +2507,12 @@ function VectorBlendControls({ controller }) {
2475
2507
  }
2476
2508
  }
2477
2509
  ),
2478
- /* @__PURE__ */ jsx15(
2510
+ /* @__PURE__ */ jsx16(
2479
2511
  "button",
2480
2512
  {
2481
2513
  type: "button",
2482
2514
  "data-vector-blend-action": "orient",
2483
- className: buttonClass13,
2515
+ className: buttonClass14,
2484
2516
  "aria-pressed": o.orientToSpine,
2485
2517
  title: "Orient steps to the spine",
2486
2518
  "aria-label": "Orient to spine",
@@ -2488,12 +2520,12 @@ function VectorBlendControls({ controller }) {
2488
2520
  children: "\u293F"
2489
2521
  }
2490
2522
  ),
2491
- /* @__PURE__ */ jsx15(
2523
+ /* @__PURE__ */ jsx16(
2492
2524
  "button",
2493
2525
  {
2494
2526
  type: "button",
2495
2527
  "data-vector-blend-action": "spine",
2496
- className: buttonClass13,
2528
+ className: buttonClass14,
2497
2529
  title: "Replace Spine (select a blend and a path)",
2498
2530
  "aria-label": "Replace Spine",
2499
2531
  disabled: !blend.canReplaceSpine,
@@ -2501,12 +2533,12 @@ function VectorBlendControls({ controller }) {
2501
2533
  children: "\u2307"
2502
2534
  }
2503
2535
  ),
2504
- /* @__PURE__ */ jsx15("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass13, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
2505
- /* @__PURE__ */ jsx15("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass13, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
2536
+ /* @__PURE__ */ jsx16("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass14, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
2537
+ /* @__PURE__ */ jsx16("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass14, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
2506
2538
  ] });
2507
2539
  }
2508
- var selectClass10 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
2509
- var numberClass9 = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
2540
+ var selectClass11 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
2541
+ var numberClass10 = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
2510
2542
  function VectorStylePanel({ controller, className }) {
2511
2543
  const state = useVectorEditorState(controller);
2512
2544
  const style = getVectorSelectionStyle(state);
@@ -2538,17 +2570,17 @@ function VectorStylePanel({ controller, className }) {
2538
2570
  if (style) controller.setWidthProfile(id);
2539
2571
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
2540
2572
  };
2541
- const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx15("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
2573
+ const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx16("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
2542
2574
  const arrowRow = (end, label) => {
2543
2575
  const key = end === "start" ? "startArrow" : "endArrow";
2544
2576
  const scaleKey = end === "start" ? "startArrowScale" : "endArrowScale";
2545
- return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
2546
- /* @__PURE__ */ jsx15("span", { className: "w-16 text-xs text-foreground", children: label }),
2547
- /* @__PURE__ */ jsxs15("select", { "aria-label": `${label} arrowhead`, className: selectClass10, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
2548
- /* @__PURE__ */ jsx15("option", { value: "", children: "None" }),
2549
- VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx15("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
2577
+ return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
2578
+ /* @__PURE__ */ jsx16("span", { className: "w-16 text-xs text-foreground", children: label }),
2579
+ /* @__PURE__ */ jsxs16("select", { "aria-label": `${label} arrowhead`, className: selectClass11, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
2580
+ /* @__PURE__ */ jsx16("option", { value: "", children: "None" }),
2581
+ VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx16("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
2550
2582
  ] }),
2551
- /* @__PURE__ */ jsx15(
2583
+ /* @__PURE__ */ jsx16(
2552
2584
  "input",
2553
2585
  {
2554
2586
  type: "number",
@@ -2556,7 +2588,7 @@ function VectorStylePanel({ controller, className }) {
2556
2588
  step: 0.25,
2557
2589
  "aria-label": `${label} arrowhead scale`,
2558
2590
  title: "Scale (\xD7 stroke width)",
2559
- className: numberClass9,
2591
+ className: numberClass10,
2560
2592
  value: source[scaleKey],
2561
2593
  onChange: (e) => {
2562
2594
  const n = parseFloat(e.target.value);
@@ -2572,14 +2604,14 @@ function VectorStylePanel({ controller, className }) {
2572
2604
  };
2573
2605
  const paintRow = (key, label) => {
2574
2606
  const paint = source[key];
2575
- const hex = vectorPaintToHex5(paint);
2607
+ const hex = vectorPaintToHex6(paint);
2576
2608
  const on = paint !== null && paint !== "none";
2577
- return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
2578
- /* @__PURE__ */ jsxs15("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
2579
- /* @__PURE__ */ jsx15("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
2609
+ return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
2610
+ /* @__PURE__ */ jsxs16("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
2611
+ /* @__PURE__ */ jsx16("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
2580
2612
  label
2581
2613
  ] }),
2582
- /* @__PURE__ */ jsx15(
2614
+ /* @__PURE__ */ jsx16(
2583
2615
  "input",
2584
2616
  {
2585
2617
  type: "color",
@@ -2590,18 +2622,18 @@ function VectorStylePanel({ controller, className }) {
2590
2622
  onChange: (e) => apply({ [key]: e.target.value })
2591
2623
  }
2592
2624
  ),
2593
- on && !hex && /* @__PURE__ */ jsx15("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
2594
- style?.mixed[key] && /* @__PURE__ */ jsx15("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
2625
+ on && !hex && /* @__PURE__ */ jsx16("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
2626
+ style?.mixed[key] && /* @__PURE__ */ jsx16("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
2595
2627
  ] });
2596
2628
  };
2597
- return /* @__PURE__ */ jsxs15("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
2598
- /* @__PURE__ */ jsx15("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
2629
+ return /* @__PURE__ */ jsxs16("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
2630
+ /* @__PURE__ */ jsx16("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
2599
2631
  paintRow("fill", "Fill"),
2600
2632
  paintRow("stroke", "Stroke"),
2601
- style && /* @__PURE__ */ jsx15(VectorGradientEditor, { controller }),
2602
- /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
2603
- /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Width" }),
2604
- /* @__PURE__ */ jsx15(
2633
+ style && /* @__PURE__ */ jsx16(VectorGradientEditor, { controller }),
2634
+ /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
2635
+ /* @__PURE__ */ jsx16("span", { className: "w-16", children: "Width" }),
2636
+ /* @__PURE__ */ jsx16(
2605
2637
  "input",
2606
2638
  {
2607
2639
  type: "number",
@@ -2617,33 +2649,33 @@ function VectorStylePanel({ controller, className }) {
2617
2649
  }
2618
2650
  )
2619
2651
  ] }),
2620
- /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
2621
- /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Align" }),
2622
- /* @__PURE__ */ jsx15("select", { "aria-label": "Stroke alignment", className: selectClass10, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS2).map((a) => /* @__PURE__ */ jsx15("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS2[a] }, a)) }),
2652
+ /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
2653
+ /* @__PURE__ */ jsx16("span", { className: "w-16", children: "Align" }),
2654
+ /* @__PURE__ */ jsx16("select", { "aria-label": "Stroke alignment", className: selectClass11, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS2).map((a) => /* @__PURE__ */ jsx16("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS2[a] }, a)) }),
2623
2655
  mixed("strokeAlign")
2624
2656
  ] }),
2625
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
2626
- /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Profile" }),
2627
- /* @__PURE__ */ jsxs15(
2657
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
2658
+ /* @__PURE__ */ jsx16("span", { className: "w-16", children: "Profile" }),
2659
+ /* @__PURE__ */ jsxs16(
2628
2660
  "select",
2629
2661
  {
2630
2662
  "aria-label": "Width profile",
2631
- className: selectClass10,
2663
+ className: selectClass11,
2632
2664
  value: source.widthProfile,
2633
2665
  onChange: (e) => {
2634
2666
  if (e.target.value !== "custom") setProfile(e.target.value);
2635
2667
  },
2636
2668
  children: [
2637
- VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx15("option", { value: p.id, children: p.label }, p.id)),
2638
- source.widthProfile === "custom" && /* @__PURE__ */ jsx15("option", { value: "custom", disabled: true, children: "Custom" })
2669
+ VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx16("option", { value: p.id, children: p.label }, p.id)),
2670
+ source.widthProfile === "custom" && /* @__PURE__ */ jsx16("option", { value: "custom", disabled: true, children: "Custom" })
2639
2671
  ]
2640
2672
  }
2641
2673
  ),
2642
- /* @__PURE__ */ jsx15(
2674
+ /* @__PURE__ */ jsx16(
2643
2675
  "button",
2644
2676
  {
2645
2677
  type: "button",
2646
- className: buttonClass13,
2678
+ className: buttonClass14,
2647
2679
  title: "Reset to uniform width",
2648
2680
  "aria-label": "Reset to uniform width",
2649
2681
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -2653,27 +2685,27 @@ function VectorStylePanel({ controller, className }) {
2653
2685
  ),
2654
2686
  mixed("widthProfile")
2655
2687
  ] }),
2656
- source.hasOpenPath && /* @__PURE__ */ jsxs15(Fragment5, { children: [
2688
+ source.hasOpenPath && /* @__PURE__ */ jsxs16(Fragment6, { children: [
2657
2689
  arrowRow("start", "Start"),
2658
2690
  arrowRow("end", "End"),
2659
- (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
2660
- /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Heads" }),
2661
- /* @__PURE__ */ jsxs15("select", { "aria-label": "Arrowhead alignment", className: selectClass10, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
2662
- /* @__PURE__ */ jsx15("option", { value: "extend", children: "Tip past end" }),
2663
- /* @__PURE__ */ jsx15("option", { value: "tip", children: "Tip at end" })
2691
+ (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
2692
+ /* @__PURE__ */ jsx16("span", { className: "w-16", children: "Heads" }),
2693
+ /* @__PURE__ */ jsxs16("select", { "aria-label": "Arrowhead alignment", className: selectClass11, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
2694
+ /* @__PURE__ */ jsx16("option", { value: "extend", children: "Tip past end" }),
2695
+ /* @__PURE__ */ jsx16("option", { value: "tip", children: "Tip at end" })
2664
2696
  ] })
2665
2697
  ] })
2666
2698
  ] }),
2667
- source.corners > 0 && /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
2668
- /* @__PURE__ */ jsx15("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
2669
- /* @__PURE__ */ jsx15(
2699
+ source.corners > 0 && /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
2700
+ /* @__PURE__ */ jsx16("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
2701
+ /* @__PURE__ */ jsx16(
2670
2702
  "input",
2671
2703
  {
2672
2704
  type: "number",
2673
2705
  min: 0,
2674
2706
  step: 1,
2675
2707
  "aria-label": "Corner radius",
2676
- className: numberClass9,
2708
+ className: numberClass10,
2677
2709
  value: Math.round(source.cornerRadius * 100) / 100,
2678
2710
  onChange: (e) => {
2679
2711
  const n = parseFloat(e.target.value);
@@ -2681,12 +2713,12 @@ function VectorStylePanel({ controller, className }) {
2681
2713
  }
2682
2714
  }
2683
2715
  ),
2684
- /* @__PURE__ */ jsx15("select", { "aria-label": "Corner kind", className: selectClass10, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx15("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
2716
+ /* @__PURE__ */ jsx16("select", { "aria-label": "Corner kind", className: selectClass11, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx16("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
2685
2717
  mixed("cornerRadius")
2686
2718
  ] }),
2687
- style && source.corners > 0 && source.cornerKind === "round" && source.cornerRadius > 0 && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "corner-smoothing", title: "Corner smoothing: squircle corners that ease into the curve (0.6 is iOS-like)", children: [
2688
- /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Smoothing" }),
2689
- /* @__PURE__ */ jsx15(
2719
+ style && source.corners > 0 && source.cornerKind === "round" && source.cornerRadius > 0 && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "corner-smoothing", title: "Corner smoothing: squircle corners that ease into the curve (0.6 is iOS-like)", children: [
2720
+ /* @__PURE__ */ jsx16("span", { className: "w-16", children: "Smoothing" }),
2721
+ /* @__PURE__ */ jsx16(
2690
2722
  "input",
2691
2723
  {
2692
2724
  type: "range",
@@ -2701,9 +2733,9 @@ function VectorStylePanel({ controller, className }) {
2701
2733
  }
2702
2734
  )
2703
2735
  ] }),
2704
- style && /* @__PURE__ */ jsxs15("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
2705
- /* @__PURE__ */ jsx15("span", { className: "w-16", children: "Opacity" }),
2706
- /* @__PURE__ */ jsx15(
2736
+ style && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
2737
+ /* @__PURE__ */ jsx16("span", { className: "w-16", children: "Opacity" }),
2738
+ /* @__PURE__ */ jsx16(
2707
2739
  "input",
2708
2740
  {
2709
2741
  type: "range",
@@ -2717,28 +2749,29 @@ function VectorStylePanel({ controller, className }) {
2717
2749
  }
2718
2750
  )
2719
2751
  ] }),
2720
- /* @__PURE__ */ jsx15(VectorTextPanel, { controller }),
2721
- style && /* @__PURE__ */ jsx15(VectorAppearancePanel, { controller }),
2722
- style && /* @__PURE__ */ jsx15(VectorEffectsPanel, { controller }),
2723
- style && /* @__PURE__ */ jsx15(VectorEnvelopePanel, { controller }),
2724
- style && /* @__PURE__ */ jsx15(VectorMaskPanel, { controller }),
2725
- /* @__PURE__ */ jsx15(VectorFramePanel, { controller }),
2726
- /* @__PURE__ */ jsx15(VectorConnectorPanel, { controller }),
2727
- /* @__PURE__ */ jsx15(VectorShadingPanel, { controller }),
2728
- /* @__PURE__ */ jsx15(VectorTracePanel, { controller }),
2729
- /* @__PURE__ */ jsx15(VectorConstraintsPanel, { controller }),
2730
- /* @__PURE__ */ jsx15(VectorSymbolsPanel, { controller })
2752
+ /* @__PURE__ */ jsx16(VectorTextPanel, { controller }),
2753
+ style && /* @__PURE__ */ jsx16(VectorAppearancePanel, { controller }),
2754
+ style && /* @__PURE__ */ jsx16(VectorEffectsPanel, { controller }),
2755
+ style && /* @__PURE__ */ jsx16(VectorEnvelopePanel, { controller }),
2756
+ style && /* @__PURE__ */ jsx16(VectorMaskPanel, { controller }),
2757
+ /* @__PURE__ */ jsx16(VectorFramePanel, { controller }),
2758
+ /* @__PURE__ */ jsx16(VectorConnectorPanel, { controller }),
2759
+ /* @__PURE__ */ jsx16(VectorShadingPanel, { controller }),
2760
+ /* @__PURE__ */ jsx16(VectorSymmetryPanel, { controller }),
2761
+ /* @__PURE__ */ jsx16(VectorTracePanel, { controller }),
2762
+ /* @__PURE__ */ jsx16(VectorConstraintsPanel, { controller }),
2763
+ /* @__PURE__ */ jsx16(VectorSymbolsPanel, { controller })
2731
2764
  ] });
2732
2765
  }
2733
2766
 
2734
2767
  // src/vector/VectorEditor.tsx
2735
- import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2768
+ import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
2736
2769
  function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
2737
- return /* @__PURE__ */ jsxs16("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
2738
- /* @__PURE__ */ jsx16(VectorToolbar, { controller, tools }),
2739
- /* @__PURE__ */ jsxs16("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
2740
- /* @__PURE__ */ jsx16("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx16(VectorCanvas, { controller, ...canvasProps }) }),
2741
- showStylePanel && /* @__PURE__ */ jsx16(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
2770
+ return /* @__PURE__ */ jsxs17("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
2771
+ /* @__PURE__ */ jsx17(VectorToolbar, { controller, tools }),
2772
+ /* @__PURE__ */ jsxs17("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
2773
+ /* @__PURE__ */ jsx17("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx17(VectorCanvas, { controller, ...canvasProps }) }),
2774
+ showStylePanel && /* @__PURE__ */ jsx17(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
2742
2775
  ] })
2743
2776
  ] });
2744
2777
  }
@@ -2761,6 +2794,7 @@ export {
2761
2794
  VectorShadingPanel,
2762
2795
  VectorStylePanel,
2763
2796
  VectorSymbolsPanel,
2797
+ VectorSymmetryPanel,
2764
2798
  VectorTextElement,
2765
2799
  VectorTextPanel,
2766
2800
  VectorToolbar,