@pixerate/editor-react 0.31.0 → 0.33.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(
@@ -369,6 +407,7 @@ function VectorCanvas({
369
407
  },
370
408
  `blend-anchor:${i}`
371
409
  )),
410
+ overlay.gradient && /* @__PURE__ */ jsx2(VectorGradientAnnotator, { annotator: overlay.gradient, accentColor }),
372
411
  overlay.widthSpot && /* @__PURE__ */ jsxs2("g", { "data-vector-width-spot": "", children: [
373
412
  /* @__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
413
  /* @__PURE__ */ jsx2("path", { d: vectorDiamondPath(overlay.widthSpot.x, overlay.widthSpot.y), fill: "none", stroke: accentColor })
@@ -474,6 +513,7 @@ function VectorCanvas({
474
513
  }
475
514
 
476
515
  // src/vector/VectorToolbar.tsx
516
+ import React3 from "react";
477
517
  import {
478
518
  VECTOR_TOOLS,
479
519
  VECTOR_TOOL_GLYPHS,
@@ -495,13 +535,338 @@ import {
495
535
  getVectorSelectionStyle,
496
536
  widthProfilePreset,
497
537
  widthProfilePresetId,
498
- vectorPaintToHex,
538
+ vectorPaintToHex as vectorPaintToHex2,
499
539
  vectorToolLabel
500
540
  } from "@pixerate/editor/vector";
501
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
541
+
542
+ // src/vector/VectorEffectsPanel.tsx
543
+ import {
544
+ VECTOR_EFFECT_MENU,
545
+ getVectorEffectsPanel
546
+ } from "@pixerate/editor/vector";
547
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
548
+ var buttonClass = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
549
+ var numberClass = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
550
+ var selectClass = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
551
+ var labelClass = "w-full truncate text-[11px] text-muted-foreground";
552
+ function VectorEffectsPanel({ controller, className }) {
553
+ const state = useVectorEditorState(controller);
554
+ const panel = getVectorEffectsPanel(state);
555
+ if (!panel) return null;
556
+ const nodeId = panel.nodeId;
557
+ const end = () => controller.endMergedEdit();
558
+ const set = (e, key, value, merge) => {
559
+ if (nodeId) controller.updateEffect(nodeId, e.id, { params: { [key]: value } }, { merge });
560
+ };
561
+ const number = (e, spec) => {
562
+ const value = Number(e.params[spec.key] ?? 0);
563
+ const label = `${e.label} ${spec.label}`;
564
+ const onChange = (ev) => {
565
+ const n = parseFloat(ev.target.value);
566
+ if (Number.isFinite(n)) set(e, spec.key, n, true);
567
+ };
568
+ return /* @__PURE__ */ jsxs3("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
569
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
570
+ spec.slider && /* @__PURE__ */ jsx3(
571
+ "input",
572
+ {
573
+ type: "range",
574
+ min: spec.min,
575
+ max: spec.max,
576
+ step: spec.step,
577
+ "aria-label": `${label} slider`,
578
+ className: "h-4 min-w-0 flex-1 accent-primary",
579
+ value,
580
+ onChange,
581
+ onPointerUp: end,
582
+ onKeyUp: end,
583
+ onBlur: end
584
+ }
585
+ ),
586
+ /* @__PURE__ */ jsx3(
587
+ "input",
588
+ {
589
+ type: "number",
590
+ min: spec.min,
591
+ max: spec.max,
592
+ step: spec.step,
593
+ "aria-label": label,
594
+ className: numberClass,
595
+ value: Math.round(value * 1e3) / 1e3,
596
+ onChange,
597
+ onBlur: end
598
+ }
599
+ )
600
+ ] }, spec.key);
601
+ };
602
+ const vec2 = (e, spec) => {
603
+ const v = e.params[spec.key] ?? { x: 0, y: 0 };
604
+ const field = (axis) => /* @__PURE__ */ jsx3(
605
+ "input",
606
+ {
607
+ type: "number",
608
+ step: spec.kind === "vec2" ? spec.step : 1,
609
+ "aria-label": `${e.label} ${spec.label} ${axis}`,
610
+ className: numberClass,
611
+ value: Math.round(v[axis] * 1e3) / 1e3,
612
+ onChange: (ev) => {
613
+ const n = parseFloat(ev.target.value);
614
+ if (Number.isFinite(n)) set(e, spec.key, { ...v, [axis]: n }, true);
615
+ },
616
+ onBlur: end
617
+ }
618
+ );
619
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
620
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
621
+ field("x"),
622
+ field("y")
623
+ ] }, spec.key);
624
+ };
625
+ const param = (e, spec) => {
626
+ if (spec.kind === "number") return number(e, spec);
627
+ if (spec.kind === "vec2") return vec2(e, spec);
628
+ if (spec.kind === "boolean")
629
+ return /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
630
+ /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1 truncate", children: spec.label }),
631
+ /* @__PURE__ */ jsx3("input", { type: "checkbox", "aria-label": `${e.label} ${spec.label}`, checked: !!e.params[spec.key], onChange: (ev) => set(e, spec.key, ev.target.checked, false) })
632
+ ] }, spec.key);
633
+ return /* @__PURE__ */ jsxs3("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
634
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
635
+ /* @__PURE__ */ jsx3("select", { "aria-label": `${e.label} ${spec.label}`, className: selectClass, value: String(e.params[spec.key] ?? ""), onChange: (ev) => set(e, spec.key, ev.target.value, false), children: spec.options.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: o.label }, o.value)) })
636
+ ] }, spec.key);
637
+ };
638
+ return /* @__PURE__ */ jsxs3("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-effects": "", "aria-label": "Effects", children: [
639
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
640
+ /* @__PURE__ */ jsx3("span", { className: "w-16 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Effects" }),
641
+ /* @__PURE__ */ jsxs3(
642
+ "select",
643
+ {
644
+ "aria-label": "Add effect",
645
+ className: selectClass,
646
+ value: "",
647
+ onChange: (ev) => {
648
+ if (ev.target.value) controller.addEffect(ev.target.value);
649
+ },
650
+ children: [
651
+ /* @__PURE__ */ jsx3("option", { value: "", children: "Add effect\u2026" }),
652
+ VECTOR_EFFECT_MENU.map((m) => /* @__PURE__ */ jsx3("option", { value: m.type, children: m.label }, m.type))
653
+ ]
654
+ }
655
+ ),
656
+ /* @__PURE__ */ jsx3("button", { type: "button", className: buttonClass, title: "Expand Appearance", "aria-label": "Expand Appearance", disabled: !panel.expandable, onClick: () => controller.expandAppearance(), children: "Expand" })
657
+ ] }),
658
+ panel.count > 1 && /* @__PURE__ */ jsx3("p", { className: "text-[11px] text-muted-foreground", children: "Select one object to edit its effects." }),
659
+ nodeId && panel.effects.map((e, i) => /* @__PURE__ */ jsxs3("div", { className: "space-y-1 rounded border border-border p-1.5", "data-vector-effect": e.id, "data-vector-effect-type": e.type, children: [
660
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
661
+ /* @__PURE__ */ jsxs3("label", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-xs font-medium text-foreground", children: [
662
+ /* @__PURE__ */ jsx3("input", { type: "checkbox", "aria-label": `${e.label} on`, checked: e.enabled, onChange: (ev) => controller.updateEffect(nodeId, e.id, { enabled: ev.target.checked }) }),
663
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: e.label })
664
+ ] }),
665
+ /* @__PURE__ */ jsx3("button", { type: "button", className: buttonClass, title: "Move up", "aria-label": `Move ${e.label} up`, disabled: i === 0, onClick: () => controller.moveEffect(nodeId, e.id, i - 1), children: "\u2191" }),
666
+ /* @__PURE__ */ jsx3(
667
+ "button",
668
+ {
669
+ type: "button",
670
+ className: buttonClass,
671
+ title: "Move down",
672
+ "aria-label": `Move ${e.label} down`,
673
+ disabled: i === panel.effects.length - 1,
674
+ onClick: () => controller.moveEffect(nodeId, e.id, i + 1),
675
+ children: "\u2193"
676
+ }
677
+ ),
678
+ /* @__PURE__ */ jsx3("button", { type: "button", className: buttonClass, title: "Remove", "aria-label": `Remove ${e.label}`, onClick: () => controller.removeEffect(nodeId, e.id), children: "\xD7" })
679
+ ] }),
680
+ e.specs.map((spec) => param(e, spec))
681
+ ] }, e.id))
682
+ ] });
683
+ }
684
+
685
+ // src/vector/VectorGradientEditor.tsx
686
+ import {
687
+ VECTOR_GRADIENT_TYPES,
688
+ VECTOR_GRADIENT_SPREADS,
689
+ VECTOR_GRADIENT_INTERPOLATIONS,
690
+ VECTOR_STROKE_GRADIENT_MODES,
691
+ VECTOR_GRADIENT_TYPE_LABELS,
692
+ VECTOR_GRADIENT_SPREAD_LABELS,
693
+ VECTOR_GRADIENT_INTERPOLATION_LABELS,
694
+ VECTOR_STROKE_GRADIENT_MODE_LABELS,
695
+ getVectorGradientEditor,
696
+ vectorPaintToHex
697
+ } 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";
702
+ var percent = (v) => Math.round(v * 1e3) / 10;
703
+ function VectorGradientEditor({ controller, className }) {
704
+ const state = useVectorEditorState(controller);
705
+ const ed = getVectorGradientEditor(state);
706
+ if (!ed) return null;
707
+ const g = ed.gradient;
708
+ const sel = ed.selected;
709
+ const end = () => controller.endMergedEdit();
710
+ const setType = (value) => {
711
+ if (value === "solid") {
712
+ const first = ed.stops[0];
713
+ controller.setStyle({ [ed.target]: vectorPaintToHex(first?.color) ?? first?.color ?? "#000000" }, void 0, { label: "Solid Colour" });
714
+ } else controller.setGradient({ type: value });
715
+ };
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" })
721
+ ] }),
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))
727
+ ] }),
728
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
729
+ ] }),
730
+ g && /* @__PURE__ */ jsxs4(Fragment2, { children: [
731
+ /* @__PURE__ */ jsxs4("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
732
+ /* @__PURE__ */ jsx4(
733
+ "div",
734
+ {
735
+ role: "button",
736
+ tabIndex: -1,
737
+ "aria-label": "Add gradient stop",
738
+ className: "absolute inset-x-1.5 top-0 h-4 cursor-copy rounded border border-border",
739
+ style: { backgroundImage: `${ed.preview}, repeating-conic-gradient(#ddd 0% 25%, #fff 0% 50%)`, backgroundSize: "auto, 8px 8px" },
740
+ onClick: (e) => {
741
+ const r = e.currentTarget.getBoundingClientRect();
742
+ controller.addGradientStop(r.width > 0 ? Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)) : 0.5);
743
+ }
744
+ }
745
+ ),
746
+ ed.stops.map((s) => /* @__PURE__ */ jsx4(
747
+ "button",
748
+ {
749
+ type: "button",
750
+ "data-vector-gradient-stop": s.index,
751
+ "aria-label": `Gradient stop ${s.index + 1} at ${percent(s.offset)}%`,
752
+ "aria-pressed": s.selected,
753
+ 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",
754
+ style: { left: `calc(6px + (100% - 12px) * ${s.offset})`, background: s.color, opacity: 0.4 + 0.6 * (s.opacity ?? 1) },
755
+ onClick: () => controller.selectGradientStop(s.selected ? null : s.index)
756
+ },
757
+ s.index
758
+ ))
759
+ ] }),
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(
762
+ "input",
763
+ {
764
+ type: "color",
765
+ "aria-label": "Stop colour",
766
+ className: "h-7 w-9 cursor-pointer rounded border border-border bg-background",
767
+ value: vectorPaintToHex(sel.color) ?? "#000000",
768
+ onChange: (e) => controller.setGradientStop(sel.index, { color: e.target.value }, { merge: `gradient-stop-color-${sel.index}` }),
769
+ onBlur: end
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
773
+ "Opacity",
774
+ /* @__PURE__ */ jsx4(
775
+ "input",
776
+ {
777
+ type: "number",
778
+ min: 0,
779
+ max: 100,
780
+ step: 5,
781
+ "aria-label": "Stop opacity",
782
+ className: numberClass2,
783
+ value: percent(sel.opacity ?? 1),
784
+ onChange: (e) => {
785
+ const n = parseFloat(e.target.value);
786
+ if (Number.isFinite(n)) controller.setGradientStop(sel.index, { opacity: n / 100 });
787
+ }
788
+ }
789
+ )
790
+ ] }),
791
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
792
+ "At",
793
+ /* @__PURE__ */ jsx4(
794
+ "input",
795
+ {
796
+ type: "number",
797
+ min: 0,
798
+ max: 100,
799
+ step: 1,
800
+ "aria-label": "Stop position",
801
+ className: numberClass2,
802
+ value: percent(sel.offset),
803
+ onChange: (e) => {
804
+ const n = parseFloat(e.target.value);
805
+ if (Number.isFinite(n)) controller.setGradientStop(sel.index, { offset: n / 100 });
806
+ }
807
+ }
808
+ )
809
+ ] }),
810
+ !sel.last && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
811
+ "Midpoint",
812
+ /* @__PURE__ */ jsx4(
813
+ "input",
814
+ {
815
+ type: "number",
816
+ min: 5,
817
+ max: 95,
818
+ step: 1,
819
+ "aria-label": "Stop midpoint",
820
+ className: numberClass2,
821
+ value: percent(sel.midpoint ?? 0.5),
822
+ onChange: (e) => {
823
+ const n = parseFloat(e.target.value);
824
+ if (Number.isFinite(n)) controller.setGradientStop(sel.index, { midpoint: n / 100 });
825
+ }
826
+ }
827
+ )
828
+ ] }),
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" })
830
+ ] }),
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)) })
834
+ ] }),
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(
838
+ "input",
839
+ {
840
+ type: "range",
841
+ min: 0,
842
+ max: 1,
843
+ step: 0.05,
844
+ "aria-label": "Gradient smoothness",
845
+ className: "h-4 flex-1 accent-primary",
846
+ value: ed.smoothness,
847
+ onChange: (e) => controller.setGradient({ smoothness: parseFloat(e.target.value) }, { merge: "gradient-smoothness" }),
848
+ onPointerUp: end,
849
+ onBlur: end
850
+ }
851
+ )
852
+ ] }),
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)) })
856
+ ] }),
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)) })
860
+ ] })
861
+ ] })
862
+ ] });
863
+ }
864
+
865
+ // src/vector/VectorToolbar.tsx
866
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
502
867
  var VECTOR_SNAP_TITLE = "Snap to objects";
503
868
  var modKeyLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "\u2318" : "Ctrl";
504
- var buttonClass = "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";
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";
505
870
  function VectorToolbar({
506
871
  controller,
507
872
  tools = VECTOR_TOOLS,
@@ -517,14 +882,16 @@ function VectorToolbar({
517
882
  const points = state.tool === "direct" ? state.selectedAnchors.length : 0;
518
883
  const editing = state.editing;
519
884
  const groupSelected = state.selection.some((id) => controller.getNode(id)?.type === "group");
520
- const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx3("button", { type: "button", className: buttonClass, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
521
- return /* @__PURE__ */ jsxs3("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
522
- /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx3(
885
+ const [live, setLive] = React3.useState(false);
886
+ 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(
523
890
  "button",
524
891
  {
525
892
  type: "button",
526
893
  "data-vector-tool": tool,
527
- className: buttonClass,
894
+ className: buttonClass3,
528
895
  "aria-pressed": state.tool === tool,
529
896
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
530
897
  "aria-label": vectorToolLabel(tool),
@@ -533,16 +900,16 @@ function VectorToolbar({
533
900
  },
534
901
  tool
535
902
  )) }),
536
- showHistory && /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
903
+ showHistory && /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
537
904
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
538
905
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
539
906
  ] }),
540
- showSnap && /* @__PURE__ */ jsx3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx3(
907
+ showSnap && /* @__PURE__ */ jsx5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx5(
541
908
  "button",
542
909
  {
543
910
  type: "button",
544
911
  "data-vector-snap": "",
545
- className: buttonClass,
912
+ className: buttonClass3,
546
913
  "aria-pressed": state.snap.objects,
547
914
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
548
915
  "aria-label": VECTOR_SNAP_TITLE,
@@ -550,13 +917,13 @@ function VectorToolbar({
550
917
  children: "Snap"
551
918
  }
552
919
  ) }),
553
- showPathEdit && /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
554
- VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx3(
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(
555
922
  "button",
556
923
  {
557
924
  type: "button",
558
925
  "data-vector-path-action": a.id,
559
- className: buttonClass,
926
+ className: buttonClass3,
560
927
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
561
928
  "aria-label": a.title,
562
929
  disabled: !a.enabled(count, points),
@@ -565,12 +932,12 @@ function VectorToolbar({
565
932
  },
566
933
  a.id
567
934
  )),
568
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx3(
935
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx5(
569
936
  "button",
570
937
  {
571
938
  type: "button",
572
939
  "data-vector-edit": t.key,
573
- className: buttonClass,
940
+ className: buttonClass3,
574
941
  "aria-pressed": editing[t.key],
575
942
  title: t.title,
576
943
  "aria-label": t.title,
@@ -579,9 +946,9 @@ function VectorToolbar({
579
946
  },
580
947
  t.key
581
948
  )),
582
- state.tool === "pencil" && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
949
+ state.tool === "pencil" && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
583
950
  "Smoothing",
584
- /* @__PURE__ */ jsx3(
951
+ /* @__PURE__ */ jsx5(
585
952
  "input",
586
953
  {
587
954
  type: "range",
@@ -595,9 +962,9 @@ function VectorToolbar({
595
962
  }
596
963
  )
597
964
  ] }),
598
- state.tool === "eraser" && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
965
+ state.tool === "eraser" && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
599
966
  "Size",
600
- /* @__PURE__ */ jsx3(
967
+ /* @__PURE__ */ jsx5(
601
968
  "input",
602
969
  {
603
970
  type: "number",
@@ -613,12 +980,12 @@ function VectorToolbar({
613
980
  }
614
981
  )
615
982
  ] }),
616
- state.tool === "paintBucket" && /* @__PURE__ */ jsx3(
983
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx5(
617
984
  "button",
618
985
  {
619
986
  type: "button",
620
987
  "data-vector-fill-gap": "",
621
- className: buttonClass,
988
+ className: buttonClass3,
622
989
  "aria-pressed": editing.fillGap > 0,
623
990
  title: "Close small gaps between path ends when filling",
624
991
  "aria-label": "Close gaps",
@@ -627,22 +994,35 @@ function VectorToolbar({
627
994
  }
628
995
  )
629
996
  ] }),
630
- showBlend && /* @__PURE__ */ jsx3(VectorBlendControls, { controller }),
631
- showArrange && /* @__PURE__ */ jsxs3(Fragment2, { children: [
632
- /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
633
- act("\u222A", "Union", () => controller.booleanOp("union"), count < 2),
634
- act("\u2212", "Subtract", () => controller.booleanOp("subtract"), count < 2),
635
- act("\u2229", "Intersect", () => controller.booleanOp("intersect"), count < 2),
636
- act("\u22BB", "Exclude", () => controller.booleanOp("exclude"), count < 2),
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(
1001
+ "button",
1002
+ {
1003
+ type: "button",
1004
+ "data-vector-live-boolean": "",
1005
+ className: buttonClass3,
1006
+ "aria-pressed": live,
1007
+ title: "Live booleans: keep the shapes editable",
1008
+ "aria-label": "Live booleans",
1009
+ onClick: () => setLive(!live),
1010
+ children: "Live"
1011
+ }
1012
+ ),
1013
+ act("\u222A", "Union", () => controller.booleanOp("union", { live }), booleanDisabled),
1014
+ act("\u2212", "Subtract", () => controller.booleanOp("subtract", { live }), booleanDisabled),
1015
+ act("\u2229", "Intersect", () => controller.booleanOp("intersect", { live }), booleanDisabled),
1016
+ act("\u22BB", "Exclude", () => controller.booleanOp("exclude", { live }), booleanDisabled),
637
1017
  act("G", "Group", () => controller.group(), count < 1),
638
1018
  act("U", "Ungroup", () => controller.ungroup(), !groupSelected)
639
1019
  ] }),
640
- /* @__PURE__ */ jsx3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx3(
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(
641
1021
  "button",
642
1022
  {
643
1023
  type: "button",
644
1024
  "data-vector-pathfinder": a.id,
645
- className: buttonClass,
1025
+ className: buttonClass3,
646
1026
  title: a.title,
647
1027
  "aria-label": a.title,
648
1028
  disabled: count < a.min,
@@ -651,7 +1031,7 @@ function VectorToolbar({
651
1031
  },
652
1032
  a.id
653
1033
  )) }),
654
- /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
1034
+ /* @__PURE__ */ jsxs5("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
655
1035
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
656
1036
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
657
1037
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -669,19 +1049,19 @@ function VectorBlendControls({ controller }) {
669
1049
  const blend = getVectorBlendControls(state);
670
1050
  const o = blend.options;
671
1051
  const selected = blend.ids.length > 0;
672
- return /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
673
- /* @__PURE__ */ jsx3("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
674
- /* @__PURE__ */ jsx3(
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(
675
1055
  "select",
676
1056
  {
677
1057
  "aria-label": "Blend spacing",
678
1058
  className: "h-7 rounded border border-border bg-background px-1 text-xs",
679
1059
  value: o.spacing,
680
1060
  onChange: (e) => applyVectorBlendOptions(controller, { spacing: e.target.value }),
681
- children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx3("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
1061
+ children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx5("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
682
1062
  }
683
1063
  ),
684
- o.spacing !== "smoothColor" && /* @__PURE__ */ jsx3(
1064
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx5(
685
1065
  "input",
686
1066
  {
687
1067
  type: "number",
@@ -697,12 +1077,12 @@ function VectorBlendControls({ controller }) {
697
1077
  }
698
1078
  }
699
1079
  ),
700
- /* @__PURE__ */ jsx3(
1080
+ /* @__PURE__ */ jsx5(
701
1081
  "button",
702
1082
  {
703
1083
  type: "button",
704
1084
  "data-vector-blend-action": "orient",
705
- className: buttonClass,
1085
+ className: buttonClass3,
706
1086
  "aria-pressed": o.orientToSpine,
707
1087
  title: "Orient steps to the spine",
708
1088
  "aria-label": "Orient to spine",
@@ -710,12 +1090,12 @@ function VectorBlendControls({ controller }) {
710
1090
  children: "\u293F"
711
1091
  }
712
1092
  ),
713
- /* @__PURE__ */ jsx3(
1093
+ /* @__PURE__ */ jsx5(
714
1094
  "button",
715
1095
  {
716
1096
  type: "button",
717
1097
  "data-vector-blend-action": "spine",
718
- className: buttonClass,
1098
+ className: buttonClass3,
719
1099
  title: "Replace Spine (select a blend and a path)",
720
1100
  "aria-label": "Replace Spine",
721
1101
  disabled: !blend.canReplaceSpine,
@@ -723,12 +1103,12 @@ function VectorBlendControls({ controller }) {
723
1103
  children: "\u2307"
724
1104
  }
725
1105
  ),
726
- /* @__PURE__ */ jsx3("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
727
- /* @__PURE__ */ jsx3("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
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" })
728
1108
  ] });
729
1109
  }
730
- var selectClass = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
731
- var numberClass = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
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";
732
1112
  function VectorStylePanel({ controller, className }) {
733
1113
  const state = useVectorEditorState(controller);
734
1114
  const style = getVectorSelectionStyle(state);
@@ -759,17 +1139,17 @@ function VectorStylePanel({ controller, className }) {
759
1139
  if (style) controller.setWidthProfile(id);
760
1140
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
761
1141
  };
762
- const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx3("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
1142
+ const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx5("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
763
1143
  const arrowRow = (end, label) => {
764
1144
  const key = end === "start" ? "startArrow" : "endArrow";
765
1145
  const scaleKey = end === "start" ? "startArrowScale" : "endArrowScale";
766
- return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
767
- /* @__PURE__ */ jsx3("span", { className: "w-16 text-xs text-foreground", children: label }),
768
- /* @__PURE__ */ jsxs3("select", { "aria-label": `${label} arrowhead`, className: selectClass, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
769
- /* @__PURE__ */ jsx3("option", { value: "", children: "None" }),
770
- VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx3("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
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))
771
1151
  ] }),
772
- /* @__PURE__ */ jsx3(
1152
+ /* @__PURE__ */ jsx5(
773
1153
  "input",
774
1154
  {
775
1155
  type: "number",
@@ -777,7 +1157,7 @@ function VectorStylePanel({ controller, className }) {
777
1157
  step: 0.25,
778
1158
  "aria-label": `${label} arrowhead scale`,
779
1159
  title: "Scale (\xD7 stroke width)",
780
- className: numberClass,
1160
+ className: numberClass3,
781
1161
  value: source[scaleKey],
782
1162
  onChange: (e) => {
783
1163
  const n = parseFloat(e.target.value);
@@ -793,14 +1173,14 @@ function VectorStylePanel({ controller, className }) {
793
1173
  };
794
1174
  const paintRow = (key, label) => {
795
1175
  const paint = source[key];
796
- const hex = vectorPaintToHex(paint);
1176
+ const hex = vectorPaintToHex2(paint);
797
1177
  const on = paint !== null && paint !== "none";
798
- return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
799
- /* @__PURE__ */ jsxs3("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
800
- /* @__PURE__ */ jsx3("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
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 }) }),
801
1181
  label
802
1182
  ] }),
803
- /* @__PURE__ */ jsx3(
1183
+ /* @__PURE__ */ jsx5(
804
1184
  "input",
805
1185
  {
806
1186
  type: "color",
@@ -811,17 +1191,18 @@ function VectorStylePanel({ controller, className }) {
811
1191
  onChange: (e) => apply({ [key]: e.target.value })
812
1192
  }
813
1193
  ),
814
- on && !hex && /* @__PURE__ */ jsx3("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
815
- style?.mixed[key] && /* @__PURE__ */ jsx3("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
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" })
816
1196
  ] });
817
1197
  };
818
- return /* @__PURE__ */ jsxs3("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
819
- /* @__PURE__ */ jsx3("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
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" }),
820
1200
  paintRow("fill", "Fill"),
821
1201
  paintRow("stroke", "Stroke"),
822
- /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
823
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Width" }),
824
- /* @__PURE__ */ jsx3(
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(
825
1206
  "input",
826
1207
  {
827
1208
  type: "number",
@@ -837,33 +1218,33 @@ function VectorStylePanel({ controller, className }) {
837
1218
  }
838
1219
  )
839
1220
  ] }),
840
- /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
841
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Align" }),
842
- /* @__PURE__ */ jsx3("select", { "aria-label": "Stroke alignment", className: selectClass, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS).map((a) => /* @__PURE__ */ jsx3("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS[a] }, a)) }),
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)) }),
843
1224
  mixed("strokeAlign")
844
1225
  ] }),
845
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
846
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Profile" }),
847
- /* @__PURE__ */ jsxs3(
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(
848
1229
  "select",
849
1230
  {
850
1231
  "aria-label": "Width profile",
851
- className: selectClass,
1232
+ className: selectClass3,
852
1233
  value: source.widthProfile,
853
1234
  onChange: (e) => {
854
1235
  if (e.target.value !== "custom") setProfile(e.target.value);
855
1236
  },
856
1237
  children: [
857
- VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx3("option", { value: p.id, children: p.label }, p.id)),
858
- source.widthProfile === "custom" && /* @__PURE__ */ jsx3("option", { value: "custom", disabled: true, children: "Custom" })
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" })
859
1240
  ]
860
1241
  }
861
1242
  ),
862
- /* @__PURE__ */ jsx3(
1243
+ /* @__PURE__ */ jsx5(
863
1244
  "button",
864
1245
  {
865
1246
  type: "button",
866
- className: buttonClass,
1247
+ className: buttonClass3,
867
1248
  title: "Reset to uniform width",
868
1249
  "aria-label": "Reset to uniform width",
869
1250
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -873,27 +1254,27 @@ function VectorStylePanel({ controller, className }) {
873
1254
  ),
874
1255
  mixed("widthProfile")
875
1256
  ] }),
876
- source.hasOpenPath && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1257
+ source.hasOpenPath && /* @__PURE__ */ jsxs5(Fragment3, { children: [
877
1258
  arrowRow("start", "Start"),
878
1259
  arrowRow("end", "End"),
879
- (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
880
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Heads" }),
881
- /* @__PURE__ */ jsxs3("select", { "aria-label": "Arrowhead alignment", className: selectClass, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
882
- /* @__PURE__ */ jsx3("option", { value: "extend", children: "Tip past end" }),
883
- /* @__PURE__ */ jsx3("option", { value: "tip", children: "Tip at 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" })
884
1265
  ] })
885
1266
  ] })
886
1267
  ] }),
887
- source.corners > 0 && /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
888
- /* @__PURE__ */ jsx3("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
889
- /* @__PURE__ */ jsx3(
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(
890
1271
  "input",
891
1272
  {
892
1273
  type: "number",
893
1274
  min: 0,
894
1275
  step: 1,
895
1276
  "aria-label": "Corner radius",
896
- className: numberClass,
1277
+ className: numberClass3,
897
1278
  value: Math.round(source.cornerRadius * 100) / 100,
898
1279
  onChange: (e) => {
899
1280
  const n = parseFloat(e.target.value);
@@ -901,12 +1282,12 @@ function VectorStylePanel({ controller, className }) {
901
1282
  }
902
1283
  }
903
1284
  ),
904
- /* @__PURE__ */ jsx3("select", { "aria-label": "Corner kind", className: selectClass, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx3("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
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)) }),
905
1286
  mixed("cornerRadius")
906
1287
  ] }),
907
- style && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
908
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Opacity" }),
909
- /* @__PURE__ */ jsx3(
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(
910
1291
  "input",
911
1292
  {
912
1293
  type: "range",
@@ -919,25 +1300,29 @@ function VectorStylePanel({ controller, className }) {
919
1300
  onChange: (e) => apply({ opacity: parseFloat(e.target.value) })
920
1301
  }
921
1302
  )
922
- ] })
1303
+ ] }),
1304
+ style && /* @__PURE__ */ jsx5(VectorEffectsPanel, { controller })
923
1305
  ] });
924
1306
  }
925
1307
 
926
1308
  // src/vector/VectorEditor.tsx
927
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1309
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
928
1310
  function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
929
- return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
930
- /* @__PURE__ */ jsx4(VectorToolbar, { controller, tools }),
931
- /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
932
- /* @__PURE__ */ jsx4("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx4(VectorCanvas, { controller, ...canvasProps }) }),
933
- showStylePanel && /* @__PURE__ */ jsx4(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
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" })
934
1316
  ] })
935
1317
  ] });
936
1318
  }
937
1319
  export {
938
1320
  VectorCanvas,
939
1321
  VectorEditor,
1322
+ VectorEffectsPanel,
1323
+ VectorGradientAnnotator,
940
1324
  VectorGradientDefs,
1325
+ VectorGradientEditor,
941
1326
  VectorSceneNodes,
942
1327
  VectorStylePanel,
943
1328
  VectorToolbar,