@pixerate/editor-react 0.31.0 → 0.32.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.
@@ -474,6 +474,7 @@ function VectorCanvas({
474
474
  }
475
475
 
476
476
  // src/vector/VectorToolbar.tsx
477
+ import React3 from "react";
477
478
  import {
478
479
  VECTOR_TOOLS,
479
480
  VECTOR_TOOL_GLYPHS,
@@ -498,10 +499,155 @@ import {
498
499
  vectorPaintToHex,
499
500
  vectorToolLabel
500
501
  } from "@pixerate/editor/vector";
501
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
502
+
503
+ // src/vector/VectorEffectsPanel.tsx
504
+ import {
505
+ VECTOR_EFFECT_MENU,
506
+ getVectorEffectsPanel
507
+ } from "@pixerate/editor/vector";
508
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
509
+ 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";
510
+ var numberClass = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
511
+ var selectClass = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
512
+ var labelClass = "w-full truncate text-[11px] text-muted-foreground";
513
+ function VectorEffectsPanel({ controller, className }) {
514
+ const state = useVectorEditorState(controller);
515
+ const panel = getVectorEffectsPanel(state);
516
+ if (!panel) return null;
517
+ const nodeId = panel.nodeId;
518
+ const end = () => controller.endMergedEdit();
519
+ const set = (e, key, value, merge) => {
520
+ if (nodeId) controller.updateEffect(nodeId, e.id, { params: { [key]: value } }, { merge });
521
+ };
522
+ const number = (e, spec) => {
523
+ const value = Number(e.params[spec.key] ?? 0);
524
+ const label = `${e.label} ${spec.label}`;
525
+ const onChange = (ev) => {
526
+ const n = parseFloat(ev.target.value);
527
+ if (Number.isFinite(n)) set(e, spec.key, n, true);
528
+ };
529
+ 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: [
530
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
531
+ spec.slider && /* @__PURE__ */ jsx3(
532
+ "input",
533
+ {
534
+ type: "range",
535
+ min: spec.min,
536
+ max: spec.max,
537
+ step: spec.step,
538
+ "aria-label": `${label} slider`,
539
+ className: "h-4 min-w-0 flex-1 accent-primary",
540
+ value,
541
+ onChange,
542
+ onPointerUp: end,
543
+ onKeyUp: end,
544
+ onBlur: end
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsx3(
548
+ "input",
549
+ {
550
+ type: "number",
551
+ min: spec.min,
552
+ max: spec.max,
553
+ step: spec.step,
554
+ "aria-label": label,
555
+ className: numberClass,
556
+ value: Math.round(value * 1e3) / 1e3,
557
+ onChange,
558
+ onBlur: end
559
+ }
560
+ )
561
+ ] }, spec.key);
562
+ };
563
+ const vec2 = (e, spec) => {
564
+ const v = e.params[spec.key] ?? { x: 0, y: 0 };
565
+ const field = (axis) => /* @__PURE__ */ jsx3(
566
+ "input",
567
+ {
568
+ type: "number",
569
+ step: spec.kind === "vec2" ? spec.step : 1,
570
+ "aria-label": `${e.label} ${spec.label} ${axis}`,
571
+ className: numberClass,
572
+ value: Math.round(v[axis] * 1e3) / 1e3,
573
+ onChange: (ev) => {
574
+ const n = parseFloat(ev.target.value);
575
+ if (Number.isFinite(n)) set(e, spec.key, { ...v, [axis]: n }, true);
576
+ },
577
+ onBlur: end
578
+ }
579
+ );
580
+ 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: [
581
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
582
+ field("x"),
583
+ field("y")
584
+ ] }, spec.key);
585
+ };
586
+ const param = (e, spec) => {
587
+ if (spec.kind === "number") return number(e, spec);
588
+ if (spec.kind === "vec2") return vec2(e, spec);
589
+ if (spec.kind === "boolean")
590
+ return /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-effect-param": spec.key, children: [
591
+ /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1 truncate", children: spec.label }),
592
+ /* @__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) })
593
+ ] }, spec.key);
594
+ 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: [
595
+ /* @__PURE__ */ jsx3("span", { className: labelClass, children: spec.label }),
596
+ /* @__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)) })
597
+ ] }, spec.key);
598
+ };
599
+ return /* @__PURE__ */ jsxs3("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-effects": "", "aria-label": "Effects", children: [
600
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
601
+ /* @__PURE__ */ jsx3("span", { className: "w-16 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Effects" }),
602
+ /* @__PURE__ */ jsxs3(
603
+ "select",
604
+ {
605
+ "aria-label": "Add effect",
606
+ className: selectClass,
607
+ value: "",
608
+ onChange: (ev) => {
609
+ if (ev.target.value) controller.addEffect(ev.target.value);
610
+ },
611
+ children: [
612
+ /* @__PURE__ */ jsx3("option", { value: "", children: "Add effect\u2026" }),
613
+ VECTOR_EFFECT_MENU.map((m) => /* @__PURE__ */ jsx3("option", { value: m.type, children: m.label }, m.type))
614
+ ]
615
+ }
616
+ ),
617
+ /* @__PURE__ */ jsx3("button", { type: "button", className: buttonClass, title: "Expand Appearance", "aria-label": "Expand Appearance", disabled: !panel.expandable, onClick: () => controller.expandAppearance(), children: "Expand" })
618
+ ] }),
619
+ panel.count > 1 && /* @__PURE__ */ jsx3("p", { className: "text-[11px] text-muted-foreground", children: "Select one object to edit its effects." }),
620
+ 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: [
621
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
622
+ /* @__PURE__ */ jsxs3("label", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-xs font-medium text-foreground", children: [
623
+ /* @__PURE__ */ jsx3("input", { type: "checkbox", "aria-label": `${e.label} on`, checked: e.enabled, onChange: (ev) => controller.updateEffect(nodeId, e.id, { enabled: ev.target.checked }) }),
624
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: e.label })
625
+ ] }),
626
+ /* @__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" }),
627
+ /* @__PURE__ */ jsx3(
628
+ "button",
629
+ {
630
+ type: "button",
631
+ className: buttonClass,
632
+ title: "Move down",
633
+ "aria-label": `Move ${e.label} down`,
634
+ disabled: i === panel.effects.length - 1,
635
+ onClick: () => controller.moveEffect(nodeId, e.id, i + 1),
636
+ children: "\u2193"
637
+ }
638
+ ),
639
+ /* @__PURE__ */ jsx3("button", { type: "button", className: buttonClass, title: "Remove", "aria-label": `Remove ${e.label}`, onClick: () => controller.removeEffect(nodeId, e.id), children: "\xD7" })
640
+ ] }),
641
+ e.specs.map((spec) => param(e, spec))
642
+ ] }, e.id))
643
+ ] });
644
+ }
645
+
646
+ // src/vector/VectorToolbar.tsx
647
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
502
648
  var VECTOR_SNAP_TITLE = "Snap to objects";
503
649
  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";
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";
505
651
  function VectorToolbar({
506
652
  controller,
507
653
  tools = VECTOR_TOOLS,
@@ -517,14 +663,16 @@ function VectorToolbar({
517
663
  const points = state.tool === "direct" ? state.selectedAnchors.length : 0;
518
664
  const editing = state.editing;
519
665
  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(
666
+ const [live, setLive] = React3.useState(false);
667
+ 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(
523
671
  "button",
524
672
  {
525
673
  type: "button",
526
674
  "data-vector-tool": tool,
527
- className: buttonClass,
675
+ className: buttonClass2,
528
676
  "aria-pressed": state.tool === tool,
529
677
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
530
678
  "aria-label": vectorToolLabel(tool),
@@ -533,16 +681,16 @@ function VectorToolbar({
533
681
  },
534
682
  tool
535
683
  )) }),
536
- showHistory && /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
684
+ showHistory && /* @__PURE__ */ jsxs4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
537
685
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
538
686
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
539
687
  ] }),
540
- showSnap && /* @__PURE__ */ jsx3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx3(
688
+ showSnap && /* @__PURE__ */ jsx4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx4(
541
689
  "button",
542
690
  {
543
691
  type: "button",
544
692
  "data-vector-snap": "",
545
- className: buttonClass,
693
+ className: buttonClass2,
546
694
  "aria-pressed": state.snap.objects,
547
695
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
548
696
  "aria-label": VECTOR_SNAP_TITLE,
@@ -550,13 +698,13 @@ function VectorToolbar({
550
698
  children: "Snap"
551
699
  }
552
700
  ) }),
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(
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(
555
703
  "button",
556
704
  {
557
705
  type: "button",
558
706
  "data-vector-path-action": a.id,
559
- className: buttonClass,
707
+ className: buttonClass2,
560
708
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
561
709
  "aria-label": a.title,
562
710
  disabled: !a.enabled(count, points),
@@ -565,12 +713,12 @@ function VectorToolbar({
565
713
  },
566
714
  a.id
567
715
  )),
568
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx3(
716
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx4(
569
717
  "button",
570
718
  {
571
719
  type: "button",
572
720
  "data-vector-edit": t.key,
573
- className: buttonClass,
721
+ className: buttonClass2,
574
722
  "aria-pressed": editing[t.key],
575
723
  title: t.title,
576
724
  "aria-label": t.title,
@@ -579,9 +727,9 @@ function VectorToolbar({
579
727
  },
580
728
  t.key
581
729
  )),
582
- state.tool === "pencil" && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
730
+ state.tool === "pencil" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
583
731
  "Smoothing",
584
- /* @__PURE__ */ jsx3(
732
+ /* @__PURE__ */ jsx4(
585
733
  "input",
586
734
  {
587
735
  type: "range",
@@ -595,9 +743,9 @@ function VectorToolbar({
595
743
  }
596
744
  )
597
745
  ] }),
598
- state.tool === "eraser" && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
746
+ state.tool === "eraser" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
599
747
  "Size",
600
- /* @__PURE__ */ jsx3(
748
+ /* @__PURE__ */ jsx4(
601
749
  "input",
602
750
  {
603
751
  type: "number",
@@ -613,12 +761,12 @@ function VectorToolbar({
613
761
  }
614
762
  )
615
763
  ] }),
616
- state.tool === "paintBucket" && /* @__PURE__ */ jsx3(
764
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx4(
617
765
  "button",
618
766
  {
619
767
  type: "button",
620
768
  "data-vector-fill-gap": "",
621
- className: buttonClass,
769
+ className: buttonClass2,
622
770
  "aria-pressed": editing.fillGap > 0,
623
771
  title: "Close small gaps between path ends when filling",
624
772
  "aria-label": "Close gaps",
@@ -627,22 +775,35 @@ function VectorToolbar({
627
775
  }
628
776
  )
629
777
  ] }),
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),
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(
782
+ "button",
783
+ {
784
+ type: "button",
785
+ "data-vector-live-boolean": "",
786
+ className: buttonClass2,
787
+ "aria-pressed": live,
788
+ title: "Live booleans: keep the shapes editable",
789
+ "aria-label": "Live booleans",
790
+ onClick: () => setLive(!live),
791
+ children: "Live"
792
+ }
793
+ ),
794
+ act("\u222A", "Union", () => controller.booleanOp("union", { live }), booleanDisabled),
795
+ act("\u2212", "Subtract", () => controller.booleanOp("subtract", { live }), booleanDisabled),
796
+ act("\u2229", "Intersect", () => controller.booleanOp("intersect", { live }), booleanDisabled),
797
+ act("\u22BB", "Exclude", () => controller.booleanOp("exclude", { live }), booleanDisabled),
637
798
  act("G", "Group", () => controller.group(), count < 1),
638
799
  act("U", "Ungroup", () => controller.ungroup(), !groupSelected)
639
800
  ] }),
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(
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(
641
802
  "button",
642
803
  {
643
804
  type: "button",
644
805
  "data-vector-pathfinder": a.id,
645
- className: buttonClass,
806
+ className: buttonClass2,
646
807
  title: a.title,
647
808
  "aria-label": a.title,
648
809
  disabled: count < a.min,
@@ -651,7 +812,7 @@ function VectorToolbar({
651
812
  },
652
813
  a.id
653
814
  )) }),
654
- /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
815
+ /* @__PURE__ */ jsxs4("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
655
816
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
656
817
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
657
818
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -669,19 +830,19 @@ function VectorBlendControls({ controller }) {
669
830
  const blend = getVectorBlendControls(state);
670
831
  const o = blend.options;
671
832
  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(
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(
675
836
  "select",
676
837
  {
677
838
  "aria-label": "Blend spacing",
678
839
  className: "h-7 rounded border border-border bg-background px-1 text-xs",
679
840
  value: o.spacing,
680
841
  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))
842
+ children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx4("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
682
843
  }
683
844
  ),
684
- o.spacing !== "smoothColor" && /* @__PURE__ */ jsx3(
845
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx4(
685
846
  "input",
686
847
  {
687
848
  type: "number",
@@ -697,12 +858,12 @@ function VectorBlendControls({ controller }) {
697
858
  }
698
859
  }
699
860
  ),
700
- /* @__PURE__ */ jsx3(
861
+ /* @__PURE__ */ jsx4(
701
862
  "button",
702
863
  {
703
864
  type: "button",
704
865
  "data-vector-blend-action": "orient",
705
- className: buttonClass,
866
+ className: buttonClass2,
706
867
  "aria-pressed": o.orientToSpine,
707
868
  title: "Orient steps to the spine",
708
869
  "aria-label": "Orient to spine",
@@ -710,12 +871,12 @@ function VectorBlendControls({ controller }) {
710
871
  children: "\u293F"
711
872
  }
712
873
  ),
713
- /* @__PURE__ */ jsx3(
874
+ /* @__PURE__ */ jsx4(
714
875
  "button",
715
876
  {
716
877
  type: "button",
717
878
  "data-vector-blend-action": "spine",
718
- className: buttonClass,
879
+ className: buttonClass2,
719
880
  title: "Replace Spine (select a blend and a path)",
720
881
  "aria-label": "Replace Spine",
721
882
  disabled: !blend.canReplaceSpine,
@@ -723,12 +884,12 @@ function VectorBlendControls({ controller }) {
723
884
  children: "\u2307"
724
885
  }
725
886
  ),
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" })
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" })
728
889
  ] });
729
890
  }
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";
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";
732
893
  function VectorStylePanel({ controller, className }) {
733
894
  const state = useVectorEditorState(controller);
734
895
  const style = getVectorSelectionStyle(state);
@@ -759,17 +920,17 @@ function VectorStylePanel({ controller, className }) {
759
920
  if (style) controller.setWidthProfile(id);
760
921
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
761
922
  };
762
- const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx3("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
923
+ const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx4("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
763
924
  const arrowRow = (end, label) => {
764
925
  const key = end === "start" ? "startArrow" : "endArrow";
765
926
  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))
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))
771
932
  ] }),
772
- /* @__PURE__ */ jsx3(
933
+ /* @__PURE__ */ jsx4(
773
934
  "input",
774
935
  {
775
936
  type: "number",
@@ -777,7 +938,7 @@ function VectorStylePanel({ controller, className }) {
777
938
  step: 0.25,
778
939
  "aria-label": `${label} arrowhead scale`,
779
940
  title: "Scale (\xD7 stroke width)",
780
- className: numberClass,
941
+ className: numberClass2,
781
942
  value: source[scaleKey],
782
943
  onChange: (e) => {
783
944
  const n = parseFloat(e.target.value);
@@ -795,12 +956,12 @@ function VectorStylePanel({ controller, className }) {
795
956
  const paint = source[key];
796
957
  const hex = vectorPaintToHex(paint);
797
958
  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 }) }),
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 }) }),
801
962
  label
802
963
  ] }),
803
- /* @__PURE__ */ jsx3(
964
+ /* @__PURE__ */ jsx4(
804
965
  "input",
805
966
  {
806
967
  type: "color",
@@ -811,17 +972,17 @@ function VectorStylePanel({ controller, className }) {
811
972
  onChange: (e) => apply({ [key]: e.target.value })
812
973
  }
813
974
  ),
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" })
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" })
816
977
  ] });
817
978
  };
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" }),
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" }),
820
981
  paintRow("fill", "Fill"),
821
982
  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(
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(
825
986
  "input",
826
987
  {
827
988
  type: "number",
@@ -837,33 +998,33 @@ function VectorStylePanel({ controller, className }) {
837
998
  }
838
999
  )
839
1000
  ] }),
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)) }),
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)) }),
843
1004
  mixed("strokeAlign")
844
1005
  ] }),
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(
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(
848
1009
  "select",
849
1010
  {
850
1011
  "aria-label": "Width profile",
851
- className: selectClass,
1012
+ className: selectClass2,
852
1013
  value: source.widthProfile,
853
1014
  onChange: (e) => {
854
1015
  if (e.target.value !== "custom") setProfile(e.target.value);
855
1016
  },
856
1017
  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" })
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" })
859
1020
  ]
860
1021
  }
861
1022
  ),
862
- /* @__PURE__ */ jsx3(
1023
+ /* @__PURE__ */ jsx4(
863
1024
  "button",
864
1025
  {
865
1026
  type: "button",
866
- className: buttonClass,
1027
+ className: buttonClass2,
867
1028
  title: "Reset to uniform width",
868
1029
  "aria-label": "Reset to uniform width",
869
1030
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -873,27 +1034,27 @@ function VectorStylePanel({ controller, className }) {
873
1034
  ),
874
1035
  mixed("widthProfile")
875
1036
  ] }),
876
- source.hasOpenPath && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1037
+ source.hasOpenPath && /* @__PURE__ */ jsxs4(Fragment2, { children: [
877
1038
  arrowRow("start", "Start"),
878
1039
  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" })
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" })
884
1045
  ] })
885
1046
  ] })
886
1047
  ] }),
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(
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(
890
1051
  "input",
891
1052
  {
892
1053
  type: "number",
893
1054
  min: 0,
894
1055
  step: 1,
895
1056
  "aria-label": "Corner radius",
896
- className: numberClass,
1057
+ className: numberClass2,
897
1058
  value: Math.round(source.cornerRadius * 100) / 100,
898
1059
  onChange: (e) => {
899
1060
  const n = parseFloat(e.target.value);
@@ -901,12 +1062,12 @@ function VectorStylePanel({ controller, className }) {
901
1062
  }
902
1063
  }
903
1064
  ),
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)) }),
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)) }),
905
1066
  mixed("cornerRadius")
906
1067
  ] }),
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(
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(
910
1071
  "input",
911
1072
  {
912
1073
  type: "range",
@@ -919,24 +1080,26 @@ function VectorStylePanel({ controller, className }) {
919
1080
  onChange: (e) => apply({ opacity: parseFloat(e.target.value) })
920
1081
  }
921
1082
  )
922
- ] })
1083
+ ] }),
1084
+ style && /* @__PURE__ */ jsx4(VectorEffectsPanel, { controller })
923
1085
  ] });
924
1086
  }
925
1087
 
926
1088
  // src/vector/VectorEditor.tsx
927
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1089
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
928
1090
  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" })
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" })
934
1096
  ] })
935
1097
  ] });
936
1098
  }
937
1099
  export {
938
1100
  VectorCanvas,
939
1101
  VectorEditor,
1102
+ VectorEffectsPanel,
940
1103
  VectorGradientDefs,
941
1104
  VectorSceneNodes,
942
1105
  VectorStylePanel,