@pixerate/editor-react 0.33.0 → 0.34.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.
@@ -365,13 +365,28 @@ function VectorCanvas({
365
365
  /* @__PURE__ */ jsxs2("g", { "data-vector-overlay": "", pointerEvents: "none", fill: "none", children: [
366
366
  overlay.outlines.map((o) => /* @__PURE__ */ jsx2("path", { d: o.d, stroke: accentColor, strokeWidth: o.selected ? 1 : 1.5, opacity: o.selected ? 1 : 0.7 }, o.id)),
367
367
  overlay.penPreview && /* @__PURE__ */ jsx2("path", { d: overlay.penPreview, stroke: accentColor, strokeDasharray: "4 3" }),
368
- box && /* @__PURE__ */ jsxs2("g", { "data-vector-selection": "", transform: box.transform || void 0, children: [
368
+ box && /* @__PURE__ */ jsxs2("g", { "data-vector-selection": "", "data-free-distort": box.freeDistort || void 0, transform: box.transform || void 0, children: [
369
369
  /* @__PURE__ */ jsx2("rect", { x: box.x, y: box.y, width: box.width, height: box.height, stroke: accentColor }),
370
370
  /* @__PURE__ */ jsx2("line", { x1: box.x + box.width / 2, y1: box.y, x2: box.x + box.width / 2, y2: box.y - 24, stroke: accentColor }),
371
371
  box.handles.map(
372
372
  (h) => h.handle === "rotate" ? /* @__PURE__ */ jsx2("circle", { "data-handle": h.handle, cx: h.x, cy: h.y, r: hs / 2 + 1, fill: THEME2.handleFill, stroke: accentColor }, h.handle) : /* @__PURE__ */ jsx2("rect", { "data-handle": h.handle, x: h.x - hs / 2, y: h.y - hs / 2, width: hs, height: hs, fill: THEME2.handleFill, stroke: accentColor }, h.handle)
373
373
  )
374
374
  ] }),
375
+ overlay.distort && /* @__PURE__ */ jsxs2("g", { "data-vector-distort": overlay.distort.mode, children: [
376
+ /* @__PURE__ */ jsx2("path", { d: overlay.distort.d, stroke: accentColor }),
377
+ overlay.distort.corners.map((c, i) => /* @__PURE__ */ jsx2("rect", { x: c.x - hs / 2, y: c.y - hs / 2, width: hs, height: hs, fill: THEME2.handleFill, stroke: accentColor }, i))
378
+ ] }),
379
+ overlay.envelope && /* @__PURE__ */ jsxs2("g", { "data-vector-envelope-mesh": overlay.envelope.nodeId, children: [
380
+ /* @__PURE__ */ jsx2("path", { "data-vector-envelope-grid": "", d: overlay.envelope.grid, stroke: accentColor, strokeOpacity: 0.8 }),
381
+ overlay.envelope.handles.map((h) => /* @__PURE__ */ jsxs2("g", { "data-vector-envelope-handle": h.index, children: [
382
+ /* @__PURE__ */ jsx2("line", { x1: h.anchorX, y1: h.anchorY, x2: h.x, y2: h.y, stroke: accentColor, strokeOpacity: 0.6 }),
383
+ /* @__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 })
384
+ ] }, h.index)),
385
+ overlay.envelope.anchors.map((a) => {
386
+ const size = as + (a.hovered || a.active ? 2 : 0);
387
+ 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);
388
+ })
389
+ ] }),
375
390
  overlay.handles.map((h) => /* @__PURE__ */ jsxs2("g", { "data-vector-handle": h.ref.handle, children: [
376
391
  /* @__PURE__ */ jsx2("line", { x1: h.anchorX, y1: h.anchorY, x2: h.x, y2: h.y, stroke: accentColor }),
377
392
  /* @__PURE__ */ jsx2("circle", { cx: h.x, cy: h.y, r: THEME2.handleRadius + (h.hovered ? 1 : 0), fill: THEME2.handleFill, stroke: accentColor })
@@ -513,7 +528,7 @@ function VectorCanvas({
513
528
  }
514
529
 
515
530
  // src/vector/VectorToolbar.tsx
516
- import React3 from "react";
531
+ import React4 from "react";
517
532
  import {
518
533
  VECTOR_TOOLS,
519
534
  VECTOR_TOOL_GLYPHS,
@@ -682,6 +697,117 @@ function VectorEffectsPanel({ controller, className }) {
682
697
  ] });
683
698
  }
684
699
 
700
+ // src/vector/VectorEnvelopePanel.tsx
701
+ import React3 from "react";
702
+ import { VECTOR_ENVELOPE_STYLE_OPTIONS, getVectorEnvelopePanel } from "@pixerate/editor/vector";
703
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
704
+ var buttonClass2 = "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";
705
+ var numberClass2 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
706
+ var selectClass2 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
707
+ var labelClass2 = "w-full truncate text-[11px] text-muted-foreground";
708
+ var SLIDERS = [
709
+ { key: "bend", label: "Bend %" },
710
+ { key: "horizontal", label: "Horizontal distortion %" },
711
+ { key: "vertical", label: "Vertical distortion %" }
712
+ ];
713
+ function VectorEnvelopePanel({ controller, className }) {
714
+ const state = useVectorEditorState(controller);
715
+ const panel = getVectorEnvelopePanel(state);
716
+ const [size, setSize] = React3.useState({ rows: 2, columns: 2 });
717
+ if (!panel) return null;
718
+ const env = panel.envelope;
719
+ const ids = panel.nodeId ? [panel.nodeId] : void 0;
720
+ const end = () => controller.endMergedEdit();
721
+ const set = (patch, merge = false) => controller.envelope(patch, { ids, merge });
722
+ const rows = env?.params.rows ?? size.rows;
723
+ const columns = env?.params.columns ?? size.columns;
724
+ const setCount = (key, raw) => {
725
+ const n = Math.round(parseFloat(raw));
726
+ if (!Number.isFinite(n) || n < 1 || n > 16) return;
727
+ if (env) set({ [key]: n });
728
+ else setSize({ ...size, [key]: n });
729
+ };
730
+ return /* @__PURE__ */ jsxs4("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-envelope": "", "aria-label": "Envelope", children: [
731
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", children: [
732
+ /* @__PURE__ */ jsx4("span", { className: "w-16 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Envelope" }),
733
+ /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-envelope-action": "warp", className: buttonClass2, title: "Make with Warp", "aria-label": "Make with Warp", disabled: !panel.canMake || !!env, onClick: () => set({ style: "arc", bend: 50 }), children: "Warp" }),
734
+ /* @__PURE__ */ jsx4(
735
+ "button",
736
+ {
737
+ type: "button",
738
+ "data-vector-envelope-action": "mesh",
739
+ className: buttonClass2,
740
+ title: "Make with Mesh",
741
+ "aria-label": "Make with Mesh",
742
+ disabled: !panel.canMake || !!env,
743
+ onClick: () => set({ style: "mesh", rows: size.rows, columns: size.columns }),
744
+ children: "Mesh"
745
+ }
746
+ ),
747
+ /* @__PURE__ */ jsx4(
748
+ "button",
749
+ {
750
+ type: "button",
751
+ "data-vector-envelope-action": "top",
752
+ className: buttonClass2,
753
+ title: "Make with Top Object",
754
+ "aria-label": "Make with Top Object",
755
+ disabled: !panel.canMakeWithTopObject,
756
+ onClick: () => controller.makeEnvelopeWithTopObject({ rows: size.rows, columns: size.columns }),
757
+ children: "Top"
758
+ }
759
+ ),
760
+ /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-envelope-action": "release", className: buttonClass2, title: "Release Envelope", "aria-label": "Release Envelope", disabled: !panel.canRelease, onClick: () => controller.releaseEnvelope(), children: "Release" }),
761
+ /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-envelope-action": "expand", className: buttonClass2, title: "Expand Envelope", "aria-label": "Expand Envelope", disabled: !panel.canRelease, onClick: () => controller.expandAppearance(), children: "Expand" })
762
+ ] }),
763
+ env && /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": "style", children: [
764
+ /* @__PURE__ */ jsx4("span", { className: labelClass2, children: "Warp" }),
765
+ /* @__PURE__ */ jsx4("select", { "aria-label": "Envelope warp", className: selectClass2, value: env.params.style, onChange: (e) => set({ style: e.target.value }), children: VECTOR_ENVELOPE_STYLE_OPTIONS.map((o) => /* @__PURE__ */ jsx4("option", { value: o.value, children: o.label }, o.value)) })
766
+ ] }),
767
+ env && env.params.style !== "mesh" && SLIDERS.map((s) => {
768
+ const value = env.params[s.key];
769
+ const onChange = (ev) => {
770
+ const n = parseFloat(ev.target.value);
771
+ if (Number.isFinite(n)) set({ [s.key]: n }, true);
772
+ };
773
+ return /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": s.key, children: [
774
+ /* @__PURE__ */ jsx4("span", { className: labelClass2, children: s.label }),
775
+ /* @__PURE__ */ jsx4(
776
+ "input",
777
+ {
778
+ type: "range",
779
+ min: -100,
780
+ max: 100,
781
+ step: 1,
782
+ "aria-label": `Envelope ${s.label} slider`,
783
+ className: "h-4 min-w-0 flex-1 accent-primary",
784
+ value,
785
+ onChange,
786
+ onPointerUp: end,
787
+ onKeyUp: end,
788
+ onBlur: end
789
+ }
790
+ ),
791
+ /* @__PURE__ */ jsx4("input", { type: "number", min: -100, max: 100, step: 1, "aria-label": `Envelope ${s.label}`, className: numberClass2, value, onChange, onBlur: end })
792
+ ] }, s.key);
793
+ }),
794
+ env && env.params.style !== "mesh" && /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": "orientation", children: [
795
+ /* @__PURE__ */ jsx4("span", { className: labelClass2, children: "Orientation" }),
796
+ /* @__PURE__ */ jsxs4("select", { "aria-label": "Envelope orientation", className: selectClass2, value: env.params.orientation, onChange: (e) => set({ orientation: e.target.value }), children: [
797
+ /* @__PURE__ */ jsx4("option", { value: "horizontal", children: "Horizontal" }),
798
+ /* @__PURE__ */ jsx4("option", { value: "vertical", children: "Vertical" })
799
+ ] })
800
+ ] }),
801
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-envelope-param": "mesh", children: [
802
+ /* @__PURE__ */ jsx4("span", { className: "min-w-0 flex-1 truncate text-[11px] text-muted-foreground", children: "Mesh rows \xD7 columns" }),
803
+ /* @__PURE__ */ jsx4("input", { type: "number", min: 1, max: 16, step: 1, "aria-label": "Envelope mesh rows", className: numberClass2, value: rows, onChange: (e) => setCount("rows", e.target.value) }),
804
+ /* @__PURE__ */ jsx4("input", { type: "number", min: 1, max: 16, step: 1, "aria-label": "Envelope mesh columns", className: numberClass2, value: columns, onChange: (e) => setCount("columns", e.target.value) })
805
+ ] }),
806
+ panel.count > 1 && !panel.canMakeWithTopObject && /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-muted-foreground", children: "Select one object to edit its envelope." }),
807
+ env && /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-muted-foreground", children: "Edit the mesh with the Envelope tool (U)." })
808
+ ] });
809
+ }
810
+
685
811
  // src/vector/VectorGradientEditor.tsx
686
812
  import {
687
813
  VECTOR_GRADIENT_TYPES,
@@ -695,10 +821,10 @@ import {
695
821
  getVectorGradientEditor,
696
822
  vectorPaintToHex
697
823
  } from "@pixerate/editor/vector";
698
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
699
- var selectClass2 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
700
- var numberClass2 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
701
- var buttonClass2 = "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";
824
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
825
+ var selectClass3 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
826
+ var numberClass3 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
827
+ var buttonClass3 = "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";
702
828
  var percent = (v) => Math.round(v * 1e3) / 10;
703
829
  function VectorGradientEditor({ controller, className }) {
704
830
  const state = useVectorEditorState(controller);
@@ -713,23 +839,23 @@ function VectorGradientEditor({ controller, className }) {
713
839
  controller.setStyle({ [ed.target]: vectorPaintToHex(first?.color) ?? first?.color ?? "#000000" }, void 0, { label: "Solid Colour" });
714
840
  } else controller.setGradient({ type: value });
715
841
  };
716
- return /* @__PURE__ */ jsxs4("div", { className: `space-y-1.5 ${className ?? ""}`, "data-vector-gradient-editor": ed.target, "aria-label": "Gradient", children: [
717
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1.5", children: [
718
- /* @__PURE__ */ jsx4("span", { className: "w-16 text-xs text-foreground", children: "Gradient" }),
719
- ["fill", "stroke"].map((t) => /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, "aria-pressed": ed.target === t, onClick: () => controller.setGradientTarget(t), children: t === "fill" ? "Fill" : "Stroke" }, t)),
720
- ed.mixed && /* @__PURE__ */ jsx4("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
842
+ return /* @__PURE__ */ jsxs5("div", { className: `space-y-1.5 ${className ?? ""}`, "data-vector-gradient-editor": ed.target, "aria-label": "Gradient", children: [
843
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1.5", children: [
844
+ /* @__PURE__ */ jsx5("span", { className: "w-16 text-xs text-foreground", children: "Gradient" }),
845
+ ["fill", "stroke"].map((t) => /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-pressed": ed.target === t, onClick: () => controller.setGradientTarget(t), children: t === "fill" ? "Fill" : "Stroke" }, t)),
846
+ ed.mixed && /* @__PURE__ */ jsx5("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
721
847
  ] }),
722
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1.5", children: [
723
- /* @__PURE__ */ jsx4("span", { className: "w-16 text-xs text-foreground", children: "Type" }),
724
- /* @__PURE__ */ jsxs4("select", { "aria-label": "Gradient type", className: selectClass2, value: ed.type ?? "solid", onChange: (e) => setType(e.target.value), children: [
725
- /* @__PURE__ */ jsx4("option", { value: "solid", children: "Solid" }),
726
- VECTOR_GRADIENT_TYPES.map((t) => /* @__PURE__ */ jsx4("option", { value: t, children: VECTOR_GRADIENT_TYPE_LABELS[t] }, t))
848
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1.5", children: [
849
+ /* @__PURE__ */ jsx5("span", { className: "w-16 text-xs text-foreground", children: "Type" }),
850
+ /* @__PURE__ */ jsxs5("select", { "aria-label": "Gradient type", className: selectClass3, value: ed.type ?? "solid", onChange: (e) => setType(e.target.value), children: [
851
+ /* @__PURE__ */ jsx5("option", { value: "solid", children: "Solid" }),
852
+ VECTOR_GRADIENT_TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: VECTOR_GRADIENT_TYPE_LABELS[t] }, t))
727
853
  ] }),
728
- /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
854
+ /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
729
855
  ] }),
730
- g && /* @__PURE__ */ jsxs4(Fragment2, { children: [
731
- /* @__PURE__ */ jsxs4("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
732
- /* @__PURE__ */ jsx4(
856
+ g && /* @__PURE__ */ jsxs5(Fragment2, { children: [
857
+ /* @__PURE__ */ jsxs5("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
858
+ /* @__PURE__ */ jsx5(
733
859
  "div",
734
860
  {
735
861
  role: "button",
@@ -743,7 +869,7 @@ function VectorGradientEditor({ controller, className }) {
743
869
  }
744
870
  }
745
871
  ),
746
- ed.stops.map((s) => /* @__PURE__ */ jsx4(
872
+ ed.stops.map((s) => /* @__PURE__ */ jsx5(
747
873
  "button",
748
874
  {
749
875
  type: "button",
@@ -757,8 +883,8 @@ function VectorGradientEditor({ controller, className }) {
757
883
  s.index
758
884
  ))
759
885
  ] }),
760
- sel && /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1.5", "data-vector-gradient-selected-stop": sel.index, children: [
761
- /* @__PURE__ */ jsx4(
886
+ sel && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-1.5", "data-vector-gradient-selected-stop": sel.index, children: [
887
+ /* @__PURE__ */ jsx5(
762
888
  "input",
763
889
  {
764
890
  type: "color",
@@ -769,9 +895,9 @@ function VectorGradientEditor({ controller, className }) {
769
895
  onBlur: end
770
896
  }
771
897
  ),
772
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
898
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
773
899
  "Opacity",
774
- /* @__PURE__ */ jsx4(
900
+ /* @__PURE__ */ jsx5(
775
901
  "input",
776
902
  {
777
903
  type: "number",
@@ -779,7 +905,7 @@ function VectorGradientEditor({ controller, className }) {
779
905
  max: 100,
780
906
  step: 5,
781
907
  "aria-label": "Stop opacity",
782
- className: numberClass2,
908
+ className: numberClass3,
783
909
  value: percent(sel.opacity ?? 1),
784
910
  onChange: (e) => {
785
911
  const n = parseFloat(e.target.value);
@@ -788,9 +914,9 @@ function VectorGradientEditor({ controller, className }) {
788
914
  }
789
915
  )
790
916
  ] }),
791
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
917
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
792
918
  "At",
793
- /* @__PURE__ */ jsx4(
919
+ /* @__PURE__ */ jsx5(
794
920
  "input",
795
921
  {
796
922
  type: "number",
@@ -798,7 +924,7 @@ function VectorGradientEditor({ controller, className }) {
798
924
  max: 100,
799
925
  step: 1,
800
926
  "aria-label": "Stop position",
801
- className: numberClass2,
927
+ className: numberClass3,
802
928
  value: percent(sel.offset),
803
929
  onChange: (e) => {
804
930
  const n = parseFloat(e.target.value);
@@ -807,9 +933,9 @@ function VectorGradientEditor({ controller, className }) {
807
933
  }
808
934
  )
809
935
  ] }),
810
- !sel.last && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
936
+ !sel.last && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
811
937
  "Midpoint",
812
- /* @__PURE__ */ jsx4(
938
+ /* @__PURE__ */ jsx5(
813
939
  "input",
814
940
  {
815
941
  type: "number",
@@ -817,7 +943,7 @@ function VectorGradientEditor({ controller, className }) {
817
943
  max: 95,
818
944
  step: 1,
819
945
  "aria-label": "Stop midpoint",
820
- className: numberClass2,
946
+ className: numberClass3,
821
947
  value: percent(sel.midpoint ?? 0.5),
822
948
  onChange: (e) => {
823
949
  const n = parseFloat(e.target.value);
@@ -826,15 +952,15 @@ function VectorGradientEditor({ controller, className }) {
826
952
  }
827
953
  )
828
954
  ] }),
829
- /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, "aria-label": "Delete stop", title: "Delete stop", disabled: ed.stops.length <= 2, onClick: () => controller.removeGradientStop(sel.index), children: "\u2715" })
955
+ /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-label": "Delete stop", title: "Delete stop", disabled: ed.stops.length <= 2, onClick: () => controller.removeGradientStop(sel.index), children: "\u2715" })
830
956
  ] }),
831
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
832
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Colours" }),
833
- /* @__PURE__ */ jsx4("select", { "aria-label": "Gradient interpolation", className: selectClass2, value: ed.interpolation, onChange: (e) => controller.setGradient({ interpolation: e.target.value }), children: VECTOR_GRADIENT_INTERPOLATIONS.map((i) => /* @__PURE__ */ jsx4("option", { value: i, children: VECTOR_GRADIENT_INTERPOLATION_LABELS[i] }, i)) })
957
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
958
+ /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Colours" }),
959
+ /* @__PURE__ */ jsx5("select", { "aria-label": "Gradient interpolation", className: selectClass3, value: ed.interpolation, onChange: (e) => controller.setGradient({ interpolation: e.target.value }), children: VECTOR_GRADIENT_INTERPOLATIONS.map((i) => /* @__PURE__ */ jsx5("option", { value: i, children: VECTOR_GRADIENT_INTERPOLATION_LABELS[i] }, i)) })
834
960
  ] }),
835
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
836
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Smooth" }),
837
- /* @__PURE__ */ jsx4(
961
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
962
+ /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Smooth" }),
963
+ /* @__PURE__ */ jsx5(
838
964
  "input",
839
965
  {
840
966
  type: "range",
@@ -850,23 +976,23 @@ function VectorGradientEditor({ controller, className }) {
850
976
  }
851
977
  )
852
978
  ] }),
853
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
854
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Spread" }),
855
- /* @__PURE__ */ jsx4("select", { "aria-label": "Gradient spread", className: selectClass2, value: ed.spread, onChange: (e) => controller.setGradient({ spread: e.target.value }), children: VECTOR_GRADIENT_SPREADS.map((sp) => /* @__PURE__ */ jsx4("option", { value: sp, children: VECTOR_GRADIENT_SPREAD_LABELS[sp] }, sp)) })
979
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
980
+ /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Spread" }),
981
+ /* @__PURE__ */ jsx5("select", { "aria-label": "Gradient spread", className: selectClass3, value: ed.spread, onChange: (e) => controller.setGradient({ spread: e.target.value }), children: VECTOR_GRADIENT_SPREADS.map((sp) => /* @__PURE__ */ jsx5("option", { value: sp, children: VECTOR_GRADIENT_SPREAD_LABELS[sp] }, sp)) })
856
982
  ] }),
857
- ed.target === "stroke" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
858
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "On stroke" }),
859
- /* @__PURE__ */ jsx4("select", { "aria-label": "Gradient on stroke", className: selectClass2, value: ed.strokeMode, onChange: (e) => controller.setGradient({ strokeMode: e.target.value }), children: VECTOR_STROKE_GRADIENT_MODES.map((m) => /* @__PURE__ */ jsx4("option", { value: m, children: VECTOR_STROKE_GRADIENT_MODE_LABELS[m] }, m)) })
983
+ ed.target === "stroke" && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
984
+ /* @__PURE__ */ jsx5("span", { className: "w-16", children: "On stroke" }),
985
+ /* @__PURE__ */ jsx5("select", { "aria-label": "Gradient on stroke", className: selectClass3, value: ed.strokeMode, onChange: (e) => controller.setGradient({ strokeMode: e.target.value }), children: VECTOR_STROKE_GRADIENT_MODES.map((m) => /* @__PURE__ */ jsx5("option", { value: m, children: VECTOR_STROKE_GRADIENT_MODE_LABELS[m] }, m)) })
860
986
  ] })
861
987
  ] })
862
988
  ] });
863
989
  }
864
990
 
865
991
  // src/vector/VectorToolbar.tsx
866
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
992
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
867
993
  var VECTOR_SNAP_TITLE = "Snap to objects";
868
994
  var modKeyLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "\u2318" : "Ctrl";
869
- var buttonClass3 = "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";
995
+ var buttonClass4 = "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";
870
996
  function VectorToolbar({
871
997
  controller,
872
998
  tools = VECTOR_TOOLS,
@@ -875,6 +1001,7 @@ function VectorToolbar({
875
1001
  showSnap = true,
876
1002
  showPathEdit = true,
877
1003
  showBlend = true,
1004
+ showDistort = true,
878
1005
  className
879
1006
  }) {
880
1007
  const state = useVectorEditorState(controller);
@@ -882,16 +1009,16 @@ function VectorToolbar({
882
1009
  const points = state.tool === "direct" ? state.selectedAnchors.length : 0;
883
1010
  const editing = state.editing;
884
1011
  const groupSelected = state.selection.some((id) => controller.getNode(id)?.type === "group");
885
- const [live, setLive] = React3.useState(false);
1012
+ const [live, setLive] = React4.useState(false);
886
1013
  const booleanDisabled = live ? count < 1 : count < 2;
887
- const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
888
- return /* @__PURE__ */ jsxs5("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
889
- /* @__PURE__ */ jsx5("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx5(
1014
+ const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass4, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
1015
+ return /* @__PURE__ */ jsxs6("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
1016
+ /* @__PURE__ */ jsx6("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx6(
890
1017
  "button",
891
1018
  {
892
1019
  type: "button",
893
1020
  "data-vector-tool": tool,
894
- className: buttonClass3,
1021
+ className: buttonClass4,
895
1022
  "aria-pressed": state.tool === tool,
896
1023
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
897
1024
  "aria-label": vectorToolLabel(tool),
@@ -900,16 +1027,16 @@ function VectorToolbar({
900
1027
  },
901
1028
  tool
902
1029
  )) }),
903
- showHistory && /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
1030
+ showHistory && /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
904
1031
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
905
1032
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
906
1033
  ] }),
907
- showSnap && /* @__PURE__ */ jsx5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx5(
1034
+ showSnap && /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx6(
908
1035
  "button",
909
1036
  {
910
1037
  type: "button",
911
1038
  "data-vector-snap": "",
912
- className: buttonClass3,
1039
+ className: buttonClass4,
913
1040
  "aria-pressed": state.snap.objects,
914
1041
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
915
1042
  "aria-label": VECTOR_SNAP_TITLE,
@@ -917,13 +1044,13 @@ function VectorToolbar({
917
1044
  children: "Snap"
918
1045
  }
919
1046
  ) }),
920
- showPathEdit && /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
921
- VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx5(
1047
+ showPathEdit && /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
1048
+ VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx6(
922
1049
  "button",
923
1050
  {
924
1051
  type: "button",
925
1052
  "data-vector-path-action": a.id,
926
- className: buttonClass3,
1053
+ className: buttonClass4,
927
1054
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
928
1055
  "aria-label": a.title,
929
1056
  disabled: !a.enabled(count, points),
@@ -932,12 +1059,12 @@ function VectorToolbar({
932
1059
  },
933
1060
  a.id
934
1061
  )),
935
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx5(
1062
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx6(
936
1063
  "button",
937
1064
  {
938
1065
  type: "button",
939
1066
  "data-vector-edit": t.key,
940
- className: buttonClass3,
1067
+ className: buttonClass4,
941
1068
  "aria-pressed": editing[t.key],
942
1069
  title: t.title,
943
1070
  "aria-label": t.title,
@@ -946,9 +1073,9 @@ function VectorToolbar({
946
1073
  },
947
1074
  t.key
948
1075
  )),
949
- state.tool === "pencil" && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1076
+ state.tool === "pencil" && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
950
1077
  "Smoothing",
951
- /* @__PURE__ */ jsx5(
1078
+ /* @__PURE__ */ jsx6(
952
1079
  "input",
953
1080
  {
954
1081
  type: "range",
@@ -962,9 +1089,9 @@ function VectorToolbar({
962
1089
  }
963
1090
  )
964
1091
  ] }),
965
- state.tool === "eraser" && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1092
+ state.tool === "eraser" && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
966
1093
  "Size",
967
- /* @__PURE__ */ jsx5(
1094
+ /* @__PURE__ */ jsx6(
968
1095
  "input",
969
1096
  {
970
1097
  type: "number",
@@ -980,12 +1107,12 @@ function VectorToolbar({
980
1107
  }
981
1108
  )
982
1109
  ] }),
983
- state.tool === "paintBucket" && /* @__PURE__ */ jsx5(
1110
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx6(
984
1111
  "button",
985
1112
  {
986
1113
  type: "button",
987
1114
  "data-vector-fill-gap": "",
988
- className: buttonClass3,
1115
+ className: buttonClass4,
989
1116
  "aria-pressed": editing.fillGap > 0,
990
1117
  title: "Close small gaps between path ends when filling",
991
1118
  "aria-label": "Close gaps",
@@ -994,15 +1121,28 @@ function VectorToolbar({
994
1121
  }
995
1122
  )
996
1123
  ] }),
997
- showBlend && /* @__PURE__ */ jsx5(VectorBlendControls, { controller }),
998
- showArrange && /* @__PURE__ */ jsxs5(Fragment3, { children: [
999
- /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
1000
- /* @__PURE__ */ jsx5(
1124
+ showDistort && /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Distort", children: /* @__PURE__ */ jsx6(
1125
+ "button",
1126
+ {
1127
+ type: "button",
1128
+ "data-vector-free-distort": "",
1129
+ className: buttonClass4,
1130
+ "aria-pressed": state.freeDistort,
1131
+ 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)`,
1132
+ "aria-label": "Free Distort",
1133
+ onClick: () => controller.setFreeDistort(!state.freeDistort),
1134
+ children: "Distort"
1135
+ }
1136
+ ) }),
1137
+ showBlend && /* @__PURE__ */ jsx6(VectorBlendControls, { controller }),
1138
+ showArrange && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1139
+ /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
1140
+ /* @__PURE__ */ jsx6(
1001
1141
  "button",
1002
1142
  {
1003
1143
  type: "button",
1004
1144
  "data-vector-live-boolean": "",
1005
- className: buttonClass3,
1145
+ className: buttonClass4,
1006
1146
  "aria-pressed": live,
1007
1147
  title: "Live booleans: keep the shapes editable",
1008
1148
  "aria-label": "Live booleans",
@@ -1017,12 +1157,12 @@ function VectorToolbar({
1017
1157
  act("G", "Group", () => controller.group(), count < 1),
1018
1158
  act("U", "Ungroup", () => controller.ungroup(), !groupSelected)
1019
1159
  ] }),
1020
- /* @__PURE__ */ jsx5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx5(
1160
+ /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx6(
1021
1161
  "button",
1022
1162
  {
1023
1163
  type: "button",
1024
1164
  "data-vector-pathfinder": a.id,
1025
- className: buttonClass3,
1165
+ className: buttonClass4,
1026
1166
  title: a.title,
1027
1167
  "aria-label": a.title,
1028
1168
  disabled: count < a.min,
@@ -1031,7 +1171,7 @@ function VectorToolbar({
1031
1171
  },
1032
1172
  a.id
1033
1173
  )) }),
1034
- /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
1174
+ /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
1035
1175
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
1036
1176
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
1037
1177
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -1049,19 +1189,19 @@ function VectorBlendControls({ controller }) {
1049
1189
  const blend = getVectorBlendControls(state);
1050
1190
  const o = blend.options;
1051
1191
  const selected = blend.ids.length > 0;
1052
- return /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
1053
- /* @__PURE__ */ jsx5("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass3, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
1054
- /* @__PURE__ */ jsx5(
1192
+ return /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
1193
+ /* @__PURE__ */ jsx6("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass4, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
1194
+ /* @__PURE__ */ jsx6(
1055
1195
  "select",
1056
1196
  {
1057
1197
  "aria-label": "Blend spacing",
1058
1198
  className: "h-7 rounded border border-border bg-background px-1 text-xs",
1059
1199
  value: o.spacing,
1060
1200
  onChange: (e) => applyVectorBlendOptions(controller, { spacing: e.target.value }),
1061
- children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx5("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
1201
+ children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx6("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
1062
1202
  }
1063
1203
  ),
1064
- o.spacing !== "smoothColor" && /* @__PURE__ */ jsx5(
1204
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx6(
1065
1205
  "input",
1066
1206
  {
1067
1207
  type: "number",
@@ -1077,12 +1217,12 @@ function VectorBlendControls({ controller }) {
1077
1217
  }
1078
1218
  }
1079
1219
  ),
1080
- /* @__PURE__ */ jsx5(
1220
+ /* @__PURE__ */ jsx6(
1081
1221
  "button",
1082
1222
  {
1083
1223
  type: "button",
1084
1224
  "data-vector-blend-action": "orient",
1085
- className: buttonClass3,
1225
+ className: buttonClass4,
1086
1226
  "aria-pressed": o.orientToSpine,
1087
1227
  title: "Orient steps to the spine",
1088
1228
  "aria-label": "Orient to spine",
@@ -1090,12 +1230,12 @@ function VectorBlendControls({ controller }) {
1090
1230
  children: "\u293F"
1091
1231
  }
1092
1232
  ),
1093
- /* @__PURE__ */ jsx5(
1233
+ /* @__PURE__ */ jsx6(
1094
1234
  "button",
1095
1235
  {
1096
1236
  type: "button",
1097
1237
  "data-vector-blend-action": "spine",
1098
- className: buttonClass3,
1238
+ className: buttonClass4,
1099
1239
  title: "Replace Spine (select a blend and a path)",
1100
1240
  "aria-label": "Replace Spine",
1101
1241
  disabled: !blend.canReplaceSpine,
@@ -1103,12 +1243,12 @@ function VectorBlendControls({ controller }) {
1103
1243
  children: "\u2307"
1104
1244
  }
1105
1245
  ),
1106
- /* @__PURE__ */ jsx5("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass3, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
1107
- /* @__PURE__ */ jsx5("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass3, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
1246
+ /* @__PURE__ */ jsx6("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass4, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
1247
+ /* @__PURE__ */ jsx6("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass4, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
1108
1248
  ] });
1109
1249
  }
1110
- var selectClass3 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
1111
- var numberClass3 = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
1250
+ var selectClass4 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
1251
+ var numberClass4 = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
1112
1252
  function VectorStylePanel({ controller, className }) {
1113
1253
  const state = useVectorEditorState(controller);
1114
1254
  const style = getVectorSelectionStyle(state);
@@ -1139,17 +1279,17 @@ function VectorStylePanel({ controller, className }) {
1139
1279
  if (style) controller.setWidthProfile(id);
1140
1280
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
1141
1281
  };
1142
- const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx5("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
1282
+ const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
1143
1283
  const arrowRow = (end, label) => {
1144
1284
  const key = end === "start" ? "startArrow" : "endArrow";
1145
1285
  const scaleKey = end === "start" ? "startArrowScale" : "endArrowScale";
1146
- return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1147
- /* @__PURE__ */ jsx5("span", { className: "w-16 text-xs text-foreground", children: label }),
1148
- /* @__PURE__ */ jsxs5("select", { "aria-label": `${label} arrowhead`, className: selectClass3, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
1149
- /* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
1150
- VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx5("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
1286
+ return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1287
+ /* @__PURE__ */ jsx6("span", { className: "w-16 text-xs text-foreground", children: label }),
1288
+ /* @__PURE__ */ jsxs6("select", { "aria-label": `${label} arrowhead`, className: selectClass4, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
1289
+ /* @__PURE__ */ jsx6("option", { value: "", children: "None" }),
1290
+ VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx6("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
1151
1291
  ] }),
1152
- /* @__PURE__ */ jsx5(
1292
+ /* @__PURE__ */ jsx6(
1153
1293
  "input",
1154
1294
  {
1155
1295
  type: "number",
@@ -1157,7 +1297,7 @@ function VectorStylePanel({ controller, className }) {
1157
1297
  step: 0.25,
1158
1298
  "aria-label": `${label} arrowhead scale`,
1159
1299
  title: "Scale (\xD7 stroke width)",
1160
- className: numberClass3,
1300
+ className: numberClass4,
1161
1301
  value: source[scaleKey],
1162
1302
  onChange: (e) => {
1163
1303
  const n = parseFloat(e.target.value);
@@ -1175,12 +1315,12 @@ function VectorStylePanel({ controller, className }) {
1175
1315
  const paint = source[key];
1176
1316
  const hex = vectorPaintToHex2(paint);
1177
1317
  const on = paint !== null && paint !== "none";
1178
- return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1179
- /* @__PURE__ */ jsxs5("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
1180
- /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
1318
+ return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1319
+ /* @__PURE__ */ jsxs6("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
1320
+ /* @__PURE__ */ jsx6("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
1181
1321
  label
1182
1322
  ] }),
1183
- /* @__PURE__ */ jsx5(
1323
+ /* @__PURE__ */ jsx6(
1184
1324
  "input",
1185
1325
  {
1186
1326
  type: "color",
@@ -1191,18 +1331,18 @@ function VectorStylePanel({ controller, className }) {
1191
1331
  onChange: (e) => apply({ [key]: e.target.value })
1192
1332
  }
1193
1333
  ),
1194
- on && !hex && /* @__PURE__ */ jsx5("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
1195
- style?.mixed[key] && /* @__PURE__ */ jsx5("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
1334
+ on && !hex && /* @__PURE__ */ jsx6("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
1335
+ style?.mixed[key] && /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
1196
1336
  ] });
1197
1337
  };
1198
- return /* @__PURE__ */ jsxs5("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
1199
- /* @__PURE__ */ jsx5("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
1338
+ return /* @__PURE__ */ jsxs6("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
1339
+ /* @__PURE__ */ jsx6("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
1200
1340
  paintRow("fill", "Fill"),
1201
1341
  paintRow("stroke", "Stroke"),
1202
- style && /* @__PURE__ */ jsx5(VectorGradientEditor, { controller }),
1203
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1204
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Width" }),
1205
- /* @__PURE__ */ jsx5(
1342
+ style && /* @__PURE__ */ jsx6(VectorGradientEditor, { controller }),
1343
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1344
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Width" }),
1345
+ /* @__PURE__ */ jsx6(
1206
1346
  "input",
1207
1347
  {
1208
1348
  type: "number",
@@ -1218,33 +1358,33 @@ function VectorStylePanel({ controller, className }) {
1218
1358
  }
1219
1359
  )
1220
1360
  ] }),
1221
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
1222
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Align" }),
1223
- /* @__PURE__ */ jsx5("select", { "aria-label": "Stroke alignment", className: selectClass3, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS).map((a) => /* @__PURE__ */ jsx5("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS[a] }, a)) }),
1361
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
1362
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Align" }),
1363
+ /* @__PURE__ */ jsx6("select", { "aria-label": "Stroke alignment", className: selectClass4, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS).map((a) => /* @__PURE__ */ jsx6("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS[a] }, a)) }),
1224
1364
  mixed("strokeAlign")
1225
1365
  ] }),
1226
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
1227
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Profile" }),
1228
- /* @__PURE__ */ jsxs5(
1366
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
1367
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Profile" }),
1368
+ /* @__PURE__ */ jsxs6(
1229
1369
  "select",
1230
1370
  {
1231
1371
  "aria-label": "Width profile",
1232
- className: selectClass3,
1372
+ className: selectClass4,
1233
1373
  value: source.widthProfile,
1234
1374
  onChange: (e) => {
1235
1375
  if (e.target.value !== "custom") setProfile(e.target.value);
1236
1376
  },
1237
1377
  children: [
1238
- VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx5("option", { value: p.id, children: p.label }, p.id)),
1239
- source.widthProfile === "custom" && /* @__PURE__ */ jsx5("option", { value: "custom", disabled: true, children: "Custom" })
1378
+ VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx6("option", { value: p.id, children: p.label }, p.id)),
1379
+ source.widthProfile === "custom" && /* @__PURE__ */ jsx6("option", { value: "custom", disabled: true, children: "Custom" })
1240
1380
  ]
1241
1381
  }
1242
1382
  ),
1243
- /* @__PURE__ */ jsx5(
1383
+ /* @__PURE__ */ jsx6(
1244
1384
  "button",
1245
1385
  {
1246
1386
  type: "button",
1247
- className: buttonClass3,
1387
+ className: buttonClass4,
1248
1388
  title: "Reset to uniform width",
1249
1389
  "aria-label": "Reset to uniform width",
1250
1390
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -1254,27 +1394,27 @@ function VectorStylePanel({ controller, className }) {
1254
1394
  ),
1255
1395
  mixed("widthProfile")
1256
1396
  ] }),
1257
- source.hasOpenPath && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1397
+ source.hasOpenPath && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1258
1398
  arrowRow("start", "Start"),
1259
1399
  arrowRow("end", "End"),
1260
- (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
1261
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Heads" }),
1262
- /* @__PURE__ */ jsxs5("select", { "aria-label": "Arrowhead alignment", className: selectClass3, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
1263
- /* @__PURE__ */ jsx5("option", { value: "extend", children: "Tip past end" }),
1264
- /* @__PURE__ */ jsx5("option", { value: "tip", children: "Tip at end" })
1400
+ (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
1401
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Heads" }),
1402
+ /* @__PURE__ */ jsxs6("select", { "aria-label": "Arrowhead alignment", className: selectClass4, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
1403
+ /* @__PURE__ */ jsx6("option", { value: "extend", children: "Tip past end" }),
1404
+ /* @__PURE__ */ jsx6("option", { value: "tip", children: "Tip at end" })
1265
1405
  ] })
1266
1406
  ] })
1267
1407
  ] }),
1268
- source.corners > 0 && /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
1269
- /* @__PURE__ */ jsx5("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
1270
- /* @__PURE__ */ jsx5(
1408
+ source.corners > 0 && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
1409
+ /* @__PURE__ */ jsx6("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
1410
+ /* @__PURE__ */ jsx6(
1271
1411
  "input",
1272
1412
  {
1273
1413
  type: "number",
1274
1414
  min: 0,
1275
1415
  step: 1,
1276
1416
  "aria-label": "Corner radius",
1277
- className: numberClass3,
1417
+ className: numberClass4,
1278
1418
  value: Math.round(source.cornerRadius * 100) / 100,
1279
1419
  onChange: (e) => {
1280
1420
  const n = parseFloat(e.target.value);
@@ -1282,12 +1422,12 @@ function VectorStylePanel({ controller, className }) {
1282
1422
  }
1283
1423
  }
1284
1424
  ),
1285
- /* @__PURE__ */ jsx5("select", { "aria-label": "Corner kind", className: selectClass3, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx5("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
1425
+ /* @__PURE__ */ jsx6("select", { "aria-label": "Corner kind", className: selectClass4, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx6("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
1286
1426
  mixed("cornerRadius")
1287
1427
  ] }),
1288
- style && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1289
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Opacity" }),
1290
- /* @__PURE__ */ jsx5(
1428
+ style && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1429
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Opacity" }),
1430
+ /* @__PURE__ */ jsx6(
1291
1431
  "input",
1292
1432
  {
1293
1433
  type: "range",
@@ -1301,18 +1441,19 @@ function VectorStylePanel({ controller, className }) {
1301
1441
  }
1302
1442
  )
1303
1443
  ] }),
1304
- style && /* @__PURE__ */ jsx5(VectorEffectsPanel, { controller })
1444
+ style && /* @__PURE__ */ jsx6(VectorEffectsPanel, { controller }),
1445
+ style && /* @__PURE__ */ jsx6(VectorEnvelopePanel, { controller })
1305
1446
  ] });
1306
1447
  }
1307
1448
 
1308
1449
  // src/vector/VectorEditor.tsx
1309
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1450
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1310
1451
  function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
1311
- return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
1312
- /* @__PURE__ */ jsx6(VectorToolbar, { controller, tools }),
1313
- /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
1314
- /* @__PURE__ */ jsx6("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx6(VectorCanvas, { controller, ...canvasProps }) }),
1315
- showStylePanel && /* @__PURE__ */ jsx6(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
1452
+ return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
1453
+ /* @__PURE__ */ jsx7(VectorToolbar, { controller, tools }),
1454
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
1455
+ /* @__PURE__ */ jsx7("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx7(VectorCanvas, { controller, ...canvasProps }) }),
1456
+ showStylePanel && /* @__PURE__ */ jsx7(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
1316
1457
  ] })
1317
1458
  ] });
1318
1459
  }
@@ -1320,6 +1461,7 @@ export {
1320
1461
  VectorCanvas,
1321
1462
  VectorEditor,
1322
1463
  VectorEffectsPanel,
1464
+ VectorEnvelopePanel,
1323
1465
  VectorGradientAnnotator,
1324
1466
  VectorGradientDefs,
1325
1467
  VectorGradientEditor,