@pixerate/editor-react 0.32.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.
@@ -140,12 +140,50 @@ function VectorWidthPointEditor({ controller, edit, accentColor = THEME.accent,
140
140
 
141
141
  // src/vector/VectorCanvas.tsx
142
142
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
143
+ function GradientElement({ def }) {
144
+ const stops = def.stops.map((s, i) => /* @__PURE__ */ jsx2("stop", { offset: s.offset, stopColor: s.stopColor, stopOpacity: s.stopOpacity }, i));
145
+ return def.type === "radial" ? /* @__PURE__ */ jsx2("radialGradient", { id: def.id, ...def.attrs, children: stops }) : /* @__PURE__ */ jsx2("linearGradient", { id: def.id, ...def.attrs, children: stops });
146
+ }
143
147
  function VectorGradientDefs({ defs }) {
144
148
  if (!defs.length) return null;
145
- return /* @__PURE__ */ jsx2("defs", { children: defs.map((def) => {
146
- const stops = def.stops.map((s, i) => /* @__PURE__ */ jsx2("stop", { offset: s.offset, stopColor: s.stopColor, stopOpacity: s.stopOpacity }, i));
147
- return def.type === "radial" ? /* @__PURE__ */ jsx2("radialGradient", { id: def.id, ...def.attrs, children: stops }, def.id) : /* @__PURE__ */ jsx2("linearGradient", { id: def.id, ...def.attrs, children: stops }, def.id);
148
- }) });
149
+ return /* @__PURE__ */ jsx2("defs", { children: defs.map(
150
+ (def) => def.type === "pattern" ? /* @__PURE__ */ jsxs2("pattern", { id: def.id, ...def.attrs, children: [
151
+ def.gradients.map((g) => /* @__PURE__ */ jsx2(GradientElement, { def: g }, g.id)),
152
+ def.tiles.map((t, i) => /* @__PURE__ */ jsx2("path", { d: t.d, fill: t.fill }, i))
153
+ ] }, def.id) : /* @__PURE__ */ jsx2(GradientElement, { def }, def.id)
154
+ ) });
155
+ }
156
+ function VectorGradientAnnotator({ annotator: g, accentColor = THEME2.accent }) {
157
+ const r = THEME2.handleRadius + 1.5;
158
+ const ring = (p, part, size = r) => /* @__PURE__ */ jsx2("circle", { "data-vector-gradient-handle": part, cx: p.x, cy: p.y, r: size + (p.hovered ? 1 : 0), fill: THEME2.handleFill, stroke: accentColor, strokeWidth: p.hovered ? 2 : 1 });
159
+ return /* @__PURE__ */ jsxs2("g", { "data-vector-gradient-annotator": g.type, children: [
160
+ g.extent && /* @__PURE__ */ jsx2("path", { "data-vector-gradient-extent": "", d: g.extent, fill: "none", stroke: accentColor, strokeDasharray: "4 3" }),
161
+ /* @__PURE__ */ jsx2("line", { x1: g.start.x, y1: g.start.y, x2: g.end.x, y2: g.end.y, stroke: "#ffffff", strokeWidth: 3 }),
162
+ /* @__PURE__ */ jsx2("line", { "data-vector-gradient-bar": "", x1: g.start.x, y1: g.start.y, x2: g.end.x, y2: g.end.y, stroke: accentColor }),
163
+ g.aspect && /* @__PURE__ */ jsx2("line", { x1: g.start.x, y1: g.start.y, x2: g.aspect.x, y2: g.aspect.y, stroke: accentColor, strokeDasharray: "2 2" }),
164
+ ring(g.start, "start"),
165
+ /* @__PURE__ */ jsx2(
166
+ "rect",
167
+ {
168
+ "data-vector-gradient-handle": g.end.handle,
169
+ x: g.end.x - r,
170
+ y: g.end.y - r,
171
+ width: 2 * r,
172
+ height: 2 * r,
173
+ fill: THEME2.handleFill,
174
+ stroke: accentColor,
175
+ strokeWidth: g.end.hovered ? 2 : 1
176
+ }
177
+ ),
178
+ g.aspect && ring(g.aspect, "aspect", THEME2.handleRadius),
179
+ g.focal && /* @__PURE__ */ jsx2("g", { "data-vector-gradient-handle": "focal", children: /* @__PURE__ */ jsx2("circle", { cx: g.focal.x, cy: g.focal.y, r: THEME2.handleRadius - 1 + (g.focal.hovered ? 1 : 0), fill: accentColor, stroke: THEME2.handleFill }) }),
180
+ g.midpoints.map((m) => /* @__PURE__ */ jsx2("path", { "data-vector-gradient-midpoint": m.index, d: vectorDiamondPath(m.x, m.y, 4 + (m.hovered ? 1 : 0)), fill: THEME2.handleFill, stroke: accentColor }, m.index)),
181
+ g.stops.map((s) => /* @__PURE__ */ jsxs2("g", { "data-vector-gradient-stop": s.index, "data-selected": s.selected || void 0, opacity: g.removing && s.selected ? 0.35 : 1, children: [
182
+ /* @__PURE__ */ jsx2("line", { x1: s.barX, y1: s.barY, x2: s.x, y2: s.y, stroke: accentColor }),
183
+ /* @__PURE__ */ jsx2("rect", { x: s.x - 5, y: s.y - 5, width: 10, height: 10, rx: 2, fill: s.color, fillOpacity: s.opacity, stroke: s.selected ? accentColor : "#ffffff", strokeWidth: s.selected || s.hovered ? 2 : 1 }),
184
+ /* @__PURE__ */ jsx2("rect", { x: s.x - 6.5, y: s.y - 6.5, width: 13, height: 13, rx: 3, fill: "none", stroke: s.selected ? "#ffffff" : accentColor })
185
+ ] }, s.index))
186
+ ] });
149
187
  }
150
188
  function VectorSceneNodes({ nodes }) {
151
189
  return /* @__PURE__ */ jsx2(Fragment, { children: nodes.map(
@@ -327,13 +365,28 @@ function VectorCanvas({
327
365
  /* @__PURE__ */ jsxs2("g", { "data-vector-overlay": "", pointerEvents: "none", fill: "none", children: [
328
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)),
329
367
  overlay.penPreview && /* @__PURE__ */ jsx2("path", { d: overlay.penPreview, stroke: accentColor, strokeDasharray: "4 3" }),
330
- 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: [
331
369
  /* @__PURE__ */ jsx2("rect", { x: box.x, y: box.y, width: box.width, height: box.height, stroke: accentColor }),
332
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 }),
333
371
  box.handles.map(
334
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)
335
373
  )
336
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
+ ] }),
337
390
  overlay.handles.map((h) => /* @__PURE__ */ jsxs2("g", { "data-vector-handle": h.ref.handle, children: [
338
391
  /* @__PURE__ */ jsx2("line", { x1: h.anchorX, y1: h.anchorY, x2: h.x, y2: h.y, stroke: accentColor }),
339
392
  /* @__PURE__ */ jsx2("circle", { cx: h.x, cy: h.y, r: THEME2.handleRadius + (h.hovered ? 1 : 0), fill: THEME2.handleFill, stroke: accentColor })
@@ -369,6 +422,7 @@ function VectorCanvas({
369
422
  },
370
423
  `blend-anchor:${i}`
371
424
  )),
425
+ overlay.gradient && /* @__PURE__ */ jsx2(VectorGradientAnnotator, { annotator: overlay.gradient, accentColor }),
372
426
  overlay.widthSpot && /* @__PURE__ */ jsxs2("g", { "data-vector-width-spot": "", children: [
373
427
  /* @__PURE__ */ jsx2("line", { x1: overlay.widthSpot.left.x, y1: overlay.widthSpot.left.y, x2: overlay.widthSpot.right.x, y2: overlay.widthSpot.right.y, stroke: accentColor, strokeDasharray: "3 2" }),
374
428
  /* @__PURE__ */ jsx2("path", { d: vectorDiamondPath(overlay.widthSpot.x, overlay.widthSpot.y), fill: "none", stroke: accentColor })
@@ -474,7 +528,7 @@ function VectorCanvas({
474
528
  }
475
529
 
476
530
  // src/vector/VectorToolbar.tsx
477
- import React3 from "react";
531
+ import React4 from "react";
478
532
  import {
479
533
  VECTOR_TOOLS,
480
534
  VECTOR_TOOL_GLYPHS,
@@ -496,7 +550,7 @@ import {
496
550
  getVectorSelectionStyle,
497
551
  widthProfilePreset,
498
552
  widthProfilePresetId,
499
- vectorPaintToHex,
553
+ vectorPaintToHex as vectorPaintToHex2,
500
554
  vectorToolLabel
501
555
  } from "@pixerate/editor/vector";
502
556
 
@@ -643,11 +697,302 @@ function VectorEffectsPanel({ controller, className }) {
643
697
  ] });
644
698
  }
645
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
+
811
+ // src/vector/VectorGradientEditor.tsx
812
+ import {
813
+ VECTOR_GRADIENT_TYPES,
814
+ VECTOR_GRADIENT_SPREADS,
815
+ VECTOR_GRADIENT_INTERPOLATIONS,
816
+ VECTOR_STROKE_GRADIENT_MODES,
817
+ VECTOR_GRADIENT_TYPE_LABELS,
818
+ VECTOR_GRADIENT_SPREAD_LABELS,
819
+ VECTOR_GRADIENT_INTERPOLATION_LABELS,
820
+ VECTOR_STROKE_GRADIENT_MODE_LABELS,
821
+ getVectorGradientEditor,
822
+ vectorPaintToHex
823
+ } from "@pixerate/editor/vector";
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";
828
+ var percent = (v) => Math.round(v * 1e3) / 10;
829
+ function VectorGradientEditor({ controller, className }) {
830
+ const state = useVectorEditorState(controller);
831
+ const ed = getVectorGradientEditor(state);
832
+ if (!ed) return null;
833
+ const g = ed.gradient;
834
+ const sel = ed.selected;
835
+ const end = () => controller.endMergedEdit();
836
+ const setType = (value) => {
837
+ if (value === "solid") {
838
+ const first = ed.stops[0];
839
+ controller.setStyle({ [ed.target]: vectorPaintToHex(first?.color) ?? first?.color ?? "#000000" }, void 0, { label: "Solid Colour" });
840
+ } else controller.setGradient({ type: value });
841
+ };
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" })
847
+ ] }),
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))
853
+ ] }),
854
+ /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
855
+ ] }),
856
+ g && /* @__PURE__ */ jsxs5(Fragment2, { children: [
857
+ /* @__PURE__ */ jsxs5("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
858
+ /* @__PURE__ */ jsx5(
859
+ "div",
860
+ {
861
+ role: "button",
862
+ tabIndex: -1,
863
+ "aria-label": "Add gradient stop",
864
+ className: "absolute inset-x-1.5 top-0 h-4 cursor-copy rounded border border-border",
865
+ style: { backgroundImage: `${ed.preview}, repeating-conic-gradient(#ddd 0% 25%, #fff 0% 50%)`, backgroundSize: "auto, 8px 8px" },
866
+ onClick: (e) => {
867
+ const r = e.currentTarget.getBoundingClientRect();
868
+ controller.addGradientStop(r.width > 0 ? Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)) : 0.5);
869
+ }
870
+ }
871
+ ),
872
+ ed.stops.map((s) => /* @__PURE__ */ jsx5(
873
+ "button",
874
+ {
875
+ type: "button",
876
+ "data-vector-gradient-stop": s.index,
877
+ "aria-label": `Gradient stop ${s.index + 1} at ${percent(s.offset)}%`,
878
+ "aria-pressed": s.selected,
879
+ className: "absolute top-4 h-3.5 w-3 -translate-x-1/2 rounded-sm border border-border aria-pressed:ring-2 aria-pressed:ring-primary",
880
+ style: { left: `calc(6px + (100% - 12px) * ${s.offset})`, background: s.color, opacity: 0.4 + 0.6 * (s.opacity ?? 1) },
881
+ onClick: () => controller.selectGradientStop(s.selected ? null : s.index)
882
+ },
883
+ s.index
884
+ ))
885
+ ] }),
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(
888
+ "input",
889
+ {
890
+ type: "color",
891
+ "aria-label": "Stop colour",
892
+ className: "h-7 w-9 cursor-pointer rounded border border-border bg-background",
893
+ value: vectorPaintToHex(sel.color) ?? "#000000",
894
+ onChange: (e) => controller.setGradientStop(sel.index, { color: e.target.value }, { merge: `gradient-stop-color-${sel.index}` }),
895
+ onBlur: end
896
+ }
897
+ ),
898
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
899
+ "Opacity",
900
+ /* @__PURE__ */ jsx5(
901
+ "input",
902
+ {
903
+ type: "number",
904
+ min: 0,
905
+ max: 100,
906
+ step: 5,
907
+ "aria-label": "Stop opacity",
908
+ className: numberClass3,
909
+ value: percent(sel.opacity ?? 1),
910
+ onChange: (e) => {
911
+ const n = parseFloat(e.target.value);
912
+ if (Number.isFinite(n)) controller.setGradientStop(sel.index, { opacity: n / 100 });
913
+ }
914
+ }
915
+ )
916
+ ] }),
917
+ /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
918
+ "At",
919
+ /* @__PURE__ */ jsx5(
920
+ "input",
921
+ {
922
+ type: "number",
923
+ min: 0,
924
+ max: 100,
925
+ step: 1,
926
+ "aria-label": "Stop position",
927
+ className: numberClass3,
928
+ value: percent(sel.offset),
929
+ onChange: (e) => {
930
+ const n = parseFloat(e.target.value);
931
+ if (Number.isFinite(n)) controller.setGradientStop(sel.index, { offset: n / 100 });
932
+ }
933
+ }
934
+ )
935
+ ] }),
936
+ !sel.last && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
937
+ "Midpoint",
938
+ /* @__PURE__ */ jsx5(
939
+ "input",
940
+ {
941
+ type: "number",
942
+ min: 5,
943
+ max: 95,
944
+ step: 1,
945
+ "aria-label": "Stop midpoint",
946
+ className: numberClass3,
947
+ value: percent(sel.midpoint ?? 0.5),
948
+ onChange: (e) => {
949
+ const n = parseFloat(e.target.value);
950
+ if (Number.isFinite(n)) controller.setGradientStop(sel.index, { midpoint: n / 100 });
951
+ }
952
+ }
953
+ )
954
+ ] }),
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" })
956
+ ] }),
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)) })
960
+ ] }),
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(
964
+ "input",
965
+ {
966
+ type: "range",
967
+ min: 0,
968
+ max: 1,
969
+ step: 0.05,
970
+ "aria-label": "Gradient smoothness",
971
+ className: "h-4 flex-1 accent-primary",
972
+ value: ed.smoothness,
973
+ onChange: (e) => controller.setGradient({ smoothness: parseFloat(e.target.value) }, { merge: "gradient-smoothness" }),
974
+ onPointerUp: end,
975
+ onBlur: end
976
+ }
977
+ )
978
+ ] }),
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)) })
982
+ ] }),
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)) })
986
+ ] })
987
+ ] })
988
+ ] });
989
+ }
990
+
646
991
  // src/vector/VectorToolbar.tsx
647
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
992
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
648
993
  var VECTOR_SNAP_TITLE = "Snap to objects";
649
994
  var modKeyLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "\u2318" : "Ctrl";
650
- var buttonClass2 = "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";
651
996
  function VectorToolbar({
652
997
  controller,
653
998
  tools = VECTOR_TOOLS,
@@ -656,6 +1001,7 @@ function VectorToolbar({
656
1001
  showSnap = true,
657
1002
  showPathEdit = true,
658
1003
  showBlend = true,
1004
+ showDistort = true,
659
1005
  className
660
1006
  }) {
661
1007
  const state = useVectorEditorState(controller);
@@ -663,16 +1009,16 @@ function VectorToolbar({
663
1009
  const points = state.tool === "direct" ? state.selectedAnchors.length : 0;
664
1010
  const editing = state.editing;
665
1011
  const groupSelected = state.selection.some((id) => controller.getNode(id)?.type === "group");
666
- const [live, setLive] = React3.useState(false);
1012
+ const [live, setLive] = React4.useState(false);
667
1013
  const booleanDisabled = live ? count < 1 : count < 2;
668
- const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
669
- return /* @__PURE__ */ jsxs4("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
670
- /* @__PURE__ */ jsx4("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx4(
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(
671
1017
  "button",
672
1018
  {
673
1019
  type: "button",
674
1020
  "data-vector-tool": tool,
675
- className: buttonClass2,
1021
+ className: buttonClass4,
676
1022
  "aria-pressed": state.tool === tool,
677
1023
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
678
1024
  "aria-label": vectorToolLabel(tool),
@@ -681,16 +1027,16 @@ function VectorToolbar({
681
1027
  },
682
1028
  tool
683
1029
  )) }),
684
- showHistory && /* @__PURE__ */ jsxs4("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: [
685
1031
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
686
1032
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
687
1033
  ] }),
688
- showSnap && /* @__PURE__ */ jsx4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx4(
1034
+ showSnap && /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx6(
689
1035
  "button",
690
1036
  {
691
1037
  type: "button",
692
1038
  "data-vector-snap": "",
693
- className: buttonClass2,
1039
+ className: buttonClass4,
694
1040
  "aria-pressed": state.snap.objects,
695
1041
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
696
1042
  "aria-label": VECTOR_SNAP_TITLE,
@@ -698,13 +1044,13 @@ function VectorToolbar({
698
1044
  children: "Snap"
699
1045
  }
700
1046
  ) }),
701
- showPathEdit && /* @__PURE__ */ jsxs4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
702
- VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx4(
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(
703
1049
  "button",
704
1050
  {
705
1051
  type: "button",
706
1052
  "data-vector-path-action": a.id,
707
- className: buttonClass2,
1053
+ className: buttonClass4,
708
1054
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
709
1055
  "aria-label": a.title,
710
1056
  disabled: !a.enabled(count, points),
@@ -713,12 +1059,12 @@ function VectorToolbar({
713
1059
  },
714
1060
  a.id
715
1061
  )),
716
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx4(
1062
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx6(
717
1063
  "button",
718
1064
  {
719
1065
  type: "button",
720
1066
  "data-vector-edit": t.key,
721
- className: buttonClass2,
1067
+ className: buttonClass4,
722
1068
  "aria-pressed": editing[t.key],
723
1069
  title: t.title,
724
1070
  "aria-label": t.title,
@@ -727,9 +1073,9 @@ function VectorToolbar({
727
1073
  },
728
1074
  t.key
729
1075
  )),
730
- state.tool === "pencil" && /* @__PURE__ */ jsxs4("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: [
731
1077
  "Smoothing",
732
- /* @__PURE__ */ jsx4(
1078
+ /* @__PURE__ */ jsx6(
733
1079
  "input",
734
1080
  {
735
1081
  type: "range",
@@ -743,9 +1089,9 @@ function VectorToolbar({
743
1089
  }
744
1090
  )
745
1091
  ] }),
746
- state.tool === "eraser" && /* @__PURE__ */ jsxs4("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: [
747
1093
  "Size",
748
- /* @__PURE__ */ jsx4(
1094
+ /* @__PURE__ */ jsx6(
749
1095
  "input",
750
1096
  {
751
1097
  type: "number",
@@ -761,12 +1107,12 @@ function VectorToolbar({
761
1107
  }
762
1108
  )
763
1109
  ] }),
764
- state.tool === "paintBucket" && /* @__PURE__ */ jsx4(
1110
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx6(
765
1111
  "button",
766
1112
  {
767
1113
  type: "button",
768
1114
  "data-vector-fill-gap": "",
769
- className: buttonClass2,
1115
+ className: buttonClass4,
770
1116
  "aria-pressed": editing.fillGap > 0,
771
1117
  title: "Close small gaps between path ends when filling",
772
1118
  "aria-label": "Close gaps",
@@ -775,15 +1121,28 @@ function VectorToolbar({
775
1121
  }
776
1122
  )
777
1123
  ] }),
778
- showBlend && /* @__PURE__ */ jsx4(VectorBlendControls, { controller }),
779
- showArrange && /* @__PURE__ */ jsxs4(Fragment2, { children: [
780
- /* @__PURE__ */ jsxs4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
781
- /* @__PURE__ */ jsx4(
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(
782
1141
  "button",
783
1142
  {
784
1143
  type: "button",
785
1144
  "data-vector-live-boolean": "",
786
- className: buttonClass2,
1145
+ className: buttonClass4,
787
1146
  "aria-pressed": live,
788
1147
  title: "Live booleans: keep the shapes editable",
789
1148
  "aria-label": "Live booleans",
@@ -798,12 +1157,12 @@ function VectorToolbar({
798
1157
  act("G", "Group", () => controller.group(), count < 1),
799
1158
  act("U", "Ungroup", () => controller.ungroup(), !groupSelected)
800
1159
  ] }),
801
- /* @__PURE__ */ jsx4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx4(
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(
802
1161
  "button",
803
1162
  {
804
1163
  type: "button",
805
1164
  "data-vector-pathfinder": a.id,
806
- className: buttonClass2,
1165
+ className: buttonClass4,
807
1166
  title: a.title,
808
1167
  "aria-label": a.title,
809
1168
  disabled: count < a.min,
@@ -812,7 +1171,7 @@ function VectorToolbar({
812
1171
  },
813
1172
  a.id
814
1173
  )) }),
815
- /* @__PURE__ */ jsxs4("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: [
816
1175
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
817
1176
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
818
1177
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -830,19 +1189,19 @@ function VectorBlendControls({ controller }) {
830
1189
  const blend = getVectorBlendControls(state);
831
1190
  const o = blend.options;
832
1191
  const selected = blend.ids.length > 0;
833
- return /* @__PURE__ */ jsxs4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
834
- /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass2, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
835
- /* @__PURE__ */ jsx4(
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(
836
1195
  "select",
837
1196
  {
838
1197
  "aria-label": "Blend spacing",
839
1198
  className: "h-7 rounded border border-border bg-background px-1 text-xs",
840
1199
  value: o.spacing,
841
1200
  onChange: (e) => applyVectorBlendOptions(controller, { spacing: e.target.value }),
842
- children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx4("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))
843
1202
  }
844
1203
  ),
845
- o.spacing !== "smoothColor" && /* @__PURE__ */ jsx4(
1204
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx6(
846
1205
  "input",
847
1206
  {
848
1207
  type: "number",
@@ -858,12 +1217,12 @@ function VectorBlendControls({ controller }) {
858
1217
  }
859
1218
  }
860
1219
  ),
861
- /* @__PURE__ */ jsx4(
1220
+ /* @__PURE__ */ jsx6(
862
1221
  "button",
863
1222
  {
864
1223
  type: "button",
865
1224
  "data-vector-blend-action": "orient",
866
- className: buttonClass2,
1225
+ className: buttonClass4,
867
1226
  "aria-pressed": o.orientToSpine,
868
1227
  title: "Orient steps to the spine",
869
1228
  "aria-label": "Orient to spine",
@@ -871,12 +1230,12 @@ function VectorBlendControls({ controller }) {
871
1230
  children: "\u293F"
872
1231
  }
873
1232
  ),
874
- /* @__PURE__ */ jsx4(
1233
+ /* @__PURE__ */ jsx6(
875
1234
  "button",
876
1235
  {
877
1236
  type: "button",
878
1237
  "data-vector-blend-action": "spine",
879
- className: buttonClass2,
1238
+ className: buttonClass4,
880
1239
  title: "Replace Spine (select a blend and a path)",
881
1240
  "aria-label": "Replace Spine",
882
1241
  disabled: !blend.canReplaceSpine,
@@ -884,12 +1243,12 @@ function VectorBlendControls({ controller }) {
884
1243
  children: "\u2307"
885
1244
  }
886
1245
  ),
887
- /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass2, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
888
- /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass2, 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" })
889
1248
  ] });
890
1249
  }
891
- var selectClass2 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
892
- var numberClass2 = "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";
893
1252
  function VectorStylePanel({ controller, className }) {
894
1253
  const state = useVectorEditorState(controller);
895
1254
  const style = getVectorSelectionStyle(state);
@@ -920,17 +1279,17 @@ function VectorStylePanel({ controller, className }) {
920
1279
  if (style) controller.setWidthProfile(id);
921
1280
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
922
1281
  };
923
- const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx4("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;
924
1283
  const arrowRow = (end, label) => {
925
1284
  const key = end === "start" ? "startArrow" : "endArrow";
926
1285
  const scaleKey = end === "start" ? "startArrowScale" : "endArrowScale";
927
- return /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
928
- /* @__PURE__ */ jsx4("span", { className: "w-16 text-xs text-foreground", children: label }),
929
- /* @__PURE__ */ jsxs4("select", { "aria-label": `${label} arrowhead`, className: selectClass2, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
930
- /* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
931
- VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx4("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))
932
1291
  ] }),
933
- /* @__PURE__ */ jsx4(
1292
+ /* @__PURE__ */ jsx6(
934
1293
  "input",
935
1294
  {
936
1295
  type: "number",
@@ -938,7 +1297,7 @@ function VectorStylePanel({ controller, className }) {
938
1297
  step: 0.25,
939
1298
  "aria-label": `${label} arrowhead scale`,
940
1299
  title: "Scale (\xD7 stroke width)",
941
- className: numberClass2,
1300
+ className: numberClass4,
942
1301
  value: source[scaleKey],
943
1302
  onChange: (e) => {
944
1303
  const n = parseFloat(e.target.value);
@@ -954,14 +1313,14 @@ function VectorStylePanel({ controller, className }) {
954
1313
  };
955
1314
  const paintRow = (key, label) => {
956
1315
  const paint = source[key];
957
- const hex = vectorPaintToHex(paint);
1316
+ const hex = vectorPaintToHex2(paint);
958
1317
  const on = paint !== null && paint !== "none";
959
- return /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
960
- /* @__PURE__ */ jsxs4("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
961
- /* @__PURE__ */ jsx4("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 }) }),
962
1321
  label
963
1322
  ] }),
964
- /* @__PURE__ */ jsx4(
1323
+ /* @__PURE__ */ jsx6(
965
1324
  "input",
966
1325
  {
967
1326
  type: "color",
@@ -972,17 +1331,18 @@ function VectorStylePanel({ controller, className }) {
972
1331
  onChange: (e) => apply({ [key]: e.target.value })
973
1332
  }
974
1333
  ),
975
- on && !hex && /* @__PURE__ */ jsx4("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
976
- style?.mixed[key] && /* @__PURE__ */ jsx4("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" })
977
1336
  ] });
978
1337
  };
979
- return /* @__PURE__ */ jsxs4("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
980
- /* @__PURE__ */ jsx4("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" }),
981
1340
  paintRow("fill", "Fill"),
982
1341
  paintRow("stroke", "Stroke"),
983
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
984
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Width" }),
985
- /* @__PURE__ */ jsx4(
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(
986
1346
  "input",
987
1347
  {
988
1348
  type: "number",
@@ -998,33 +1358,33 @@ function VectorStylePanel({ controller, className }) {
998
1358
  }
999
1359
  )
1000
1360
  ] }),
1001
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
1002
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Align" }),
1003
- /* @__PURE__ */ jsx4("select", { "aria-label": "Stroke alignment", className: selectClass2, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS).map((a) => /* @__PURE__ */ jsx4("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)) }),
1004
1364
  mixed("strokeAlign")
1005
1365
  ] }),
1006
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
1007
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Profile" }),
1008
- /* @__PURE__ */ jsxs4(
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(
1009
1369
  "select",
1010
1370
  {
1011
1371
  "aria-label": "Width profile",
1012
- className: selectClass2,
1372
+ className: selectClass4,
1013
1373
  value: source.widthProfile,
1014
1374
  onChange: (e) => {
1015
1375
  if (e.target.value !== "custom") setProfile(e.target.value);
1016
1376
  },
1017
1377
  children: [
1018
- VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx4("option", { value: p.id, children: p.label }, p.id)),
1019
- source.widthProfile === "custom" && /* @__PURE__ */ jsx4("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" })
1020
1380
  ]
1021
1381
  }
1022
1382
  ),
1023
- /* @__PURE__ */ jsx4(
1383
+ /* @__PURE__ */ jsx6(
1024
1384
  "button",
1025
1385
  {
1026
1386
  type: "button",
1027
- className: buttonClass2,
1387
+ className: buttonClass4,
1028
1388
  title: "Reset to uniform width",
1029
1389
  "aria-label": "Reset to uniform width",
1030
1390
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -1034,27 +1394,27 @@ function VectorStylePanel({ controller, className }) {
1034
1394
  ),
1035
1395
  mixed("widthProfile")
1036
1396
  ] }),
1037
- source.hasOpenPath && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1397
+ source.hasOpenPath && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1038
1398
  arrowRow("start", "Start"),
1039
1399
  arrowRow("end", "End"),
1040
- (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
1041
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Heads" }),
1042
- /* @__PURE__ */ jsxs4("select", { "aria-label": "Arrowhead alignment", className: selectClass2, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
1043
- /* @__PURE__ */ jsx4("option", { value: "extend", children: "Tip past end" }),
1044
- /* @__PURE__ */ jsx4("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" })
1045
1405
  ] })
1046
1406
  ] })
1047
1407
  ] }),
1048
- source.corners > 0 && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
1049
- /* @__PURE__ */ jsx4("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
1050
- /* @__PURE__ */ jsx4(
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(
1051
1411
  "input",
1052
1412
  {
1053
1413
  type: "number",
1054
1414
  min: 0,
1055
1415
  step: 1,
1056
1416
  "aria-label": "Corner radius",
1057
- className: numberClass2,
1417
+ className: numberClass4,
1058
1418
  value: Math.round(source.cornerRadius * 100) / 100,
1059
1419
  onChange: (e) => {
1060
1420
  const n = parseFloat(e.target.value);
@@ -1062,12 +1422,12 @@ function VectorStylePanel({ controller, className }) {
1062
1422
  }
1063
1423
  }
1064
1424
  ),
1065
- /* @__PURE__ */ jsx4("select", { "aria-label": "Corner kind", className: selectClass2, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx4("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)) }),
1066
1426
  mixed("cornerRadius")
1067
1427
  ] }),
1068
- style && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1069
- /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Opacity" }),
1070
- /* @__PURE__ */ jsx4(
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(
1071
1431
  "input",
1072
1432
  {
1073
1433
  type: "range",
@@ -1081,18 +1441,19 @@ function VectorStylePanel({ controller, className }) {
1081
1441
  }
1082
1442
  )
1083
1443
  ] }),
1084
- style && /* @__PURE__ */ jsx4(VectorEffectsPanel, { controller })
1444
+ style && /* @__PURE__ */ jsx6(VectorEffectsPanel, { controller }),
1445
+ style && /* @__PURE__ */ jsx6(VectorEnvelopePanel, { controller })
1085
1446
  ] });
1086
1447
  }
1087
1448
 
1088
1449
  // src/vector/VectorEditor.tsx
1089
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1450
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1090
1451
  function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
1091
- return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
1092
- /* @__PURE__ */ jsx5(VectorToolbar, { controller, tools }),
1093
- /* @__PURE__ */ jsxs5("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
1094
- /* @__PURE__ */ jsx5("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx5(VectorCanvas, { controller, ...canvasProps }) }),
1095
- showStylePanel && /* @__PURE__ */ jsx5(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" })
1096
1457
  ] })
1097
1458
  ] });
1098
1459
  }
@@ -1100,7 +1461,10 @@ export {
1100
1461
  VectorCanvas,
1101
1462
  VectorEditor,
1102
1463
  VectorEffectsPanel,
1464
+ VectorEnvelopePanel,
1465
+ VectorGradientAnnotator,
1103
1466
  VectorGradientDefs,
1467
+ VectorGradientEditor,
1104
1468
  VectorSceneNodes,
1105
1469
  VectorStylePanel,
1106
1470
  VectorToolbar,