@pixerate/editor-react 0.30.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.
@@ -348,6 +348,27 @@ function VectorCanvas({
348
348
  /* @__PURE__ */ jsx2("circle", { cx: w.x, cy: w.y, r: THEME2.cornerWidgetRadius + (w.hovered ? 1 : 0), fill: THEME2.handleFill, stroke: accentColor }),
349
349
  /* @__PURE__ */ jsx2("circle", { cx: w.x, cy: w.y, r: 1.5, fill: accentColor })
350
350
  ] }, `corner:${w.ref.nodeId}:${w.ref.subpath}:${w.ref.index}`)),
351
+ overlay.blend.spines.map((sp) => /* @__PURE__ */ jsxs2("g", { "data-vector-blend-spine": sp.id, "data-replaced": sp.replaced || void 0, children: [
352
+ /* @__PURE__ */ jsx2("path", { d: sp.d, fill: "none", stroke: accentColor, strokeDasharray: "4 3" }),
353
+ sp.keys.map((k, i) => /* @__PURE__ */ jsx2("circle", { cx: k.x, cy: k.y, r: THEME2.handleRadius, fill: THEME2.handleFill, stroke: accentColor }, i))
354
+ ] }, `blend:${sp.id}`)),
355
+ overlay.blend.start && /* @__PURE__ */ jsxs2("g", { "data-vector-blend-start": "", children: [
356
+ /* @__PURE__ */ jsx2("path", { d: overlay.blend.start.d, fill: "none", stroke: accentColor, strokeWidth: 2 }),
357
+ overlay.blend.start.anchor && /* @__PURE__ */ jsx2("circle", { cx: overlay.blend.start.anchor.x, cy: overlay.blend.start.anchor.y, r: THEME2.handleRadius + 1, fill: accentColor })
358
+ ] }),
359
+ overlay.blend.anchors.map((a, i) => /* @__PURE__ */ jsx2(
360
+ "rect",
361
+ {
362
+ "data-vector-blend-anchor": i,
363
+ x: a.x - as / 2,
364
+ y: a.y - as / 2,
365
+ width: as,
366
+ height: as,
367
+ fill: a.hovered ? accentColor : THEME2.handleFill,
368
+ stroke: accentColor
369
+ },
370
+ `blend-anchor:${i}`
371
+ )),
351
372
  overlay.widthSpot && /* @__PURE__ */ jsxs2("g", { "data-vector-width-spot": "", children: [
352
373
  /* @__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" }),
353
374
  /* @__PURE__ */ jsx2("path", { d: vectorDiamondPath(overlay.widthSpot.x, overlay.widthSpot.y), fill: "none", stroke: accentColor })
@@ -453,6 +474,7 @@ function VectorCanvas({
453
474
  }
454
475
 
455
476
  // src/vector/VectorToolbar.tsx
477
+ import React3 from "react";
456
478
  import {
457
479
  VECTOR_TOOLS,
458
480
  VECTOR_TOOL_GLYPHS,
@@ -467,30 +489,190 @@ import {
467
489
  VECTOR_CORNER_KIND_LABELS,
468
490
  VECTOR_STROKE_ALIGN_LABELS,
469
491
  VECTOR_WIDTH_PRESETS,
492
+ VECTOR_BLEND_SPACINGS,
493
+ VECTOR_BLEND_SPACING_LABELS,
494
+ applyVectorBlendOptions,
495
+ getVectorBlendControls,
470
496
  getVectorSelectionStyle,
471
497
  widthProfilePreset,
472
498
  widthProfilePresetId,
473
499
  vectorPaintToHex,
474
500
  vectorToolLabel
475
501
  } from "@pixerate/editor/vector";
476
- 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";
477
648
  var VECTOR_SNAP_TITLE = "Snap to objects";
478
649
  var modKeyLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "\u2318" : "Ctrl";
479
- 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";
480
- function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, showArrange = true, showSnap = true, showPathEdit = true, className }) {
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";
651
+ function VectorToolbar({
652
+ controller,
653
+ tools = VECTOR_TOOLS,
654
+ showHistory = true,
655
+ showArrange = true,
656
+ showSnap = true,
657
+ showPathEdit = true,
658
+ showBlend = true,
659
+ className
660
+ }) {
481
661
  const state = useVectorEditorState(controller);
482
662
  const count = state.selection.length;
483
663
  const points = state.tool === "direct" ? state.selectedAnchors.length : 0;
484
664
  const editing = state.editing;
485
665
  const groupSelected = state.selection.some((id) => controller.getNode(id)?.type === "group");
486
- 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);
487
- return /* @__PURE__ */ jsxs3("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
488
- /* @__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(
489
671
  "button",
490
672
  {
491
673
  type: "button",
492
674
  "data-vector-tool": tool,
493
- className: buttonClass,
675
+ className: buttonClass2,
494
676
  "aria-pressed": state.tool === tool,
495
677
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
496
678
  "aria-label": vectorToolLabel(tool),
@@ -499,16 +681,16 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
499
681
  },
500
682
  tool
501
683
  )) }),
502
- 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: [
503
685
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
504
686
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
505
687
  ] }),
506
- 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(
507
689
  "button",
508
690
  {
509
691
  type: "button",
510
692
  "data-vector-snap": "",
511
- className: buttonClass,
693
+ className: buttonClass2,
512
694
  "aria-pressed": state.snap.objects,
513
695
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
514
696
  "aria-label": VECTOR_SNAP_TITLE,
@@ -516,13 +698,13 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
516
698
  children: "Snap"
517
699
  }
518
700
  ) }),
519
- showPathEdit && /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
520
- 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(
521
703
  "button",
522
704
  {
523
705
  type: "button",
524
706
  "data-vector-path-action": a.id,
525
- className: buttonClass,
707
+ className: buttonClass2,
526
708
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
527
709
  "aria-label": a.title,
528
710
  disabled: !a.enabled(count, points),
@@ -531,12 +713,12 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
531
713
  },
532
714
  a.id
533
715
  )),
534
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx3(
716
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx4(
535
717
  "button",
536
718
  {
537
719
  type: "button",
538
720
  "data-vector-edit": t.key,
539
- className: buttonClass,
721
+ className: buttonClass2,
540
722
  "aria-pressed": editing[t.key],
541
723
  title: t.title,
542
724
  "aria-label": t.title,
@@ -545,9 +727,9 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
545
727
  },
546
728
  t.key
547
729
  )),
548
- 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: [
549
731
  "Smoothing",
550
- /* @__PURE__ */ jsx3(
732
+ /* @__PURE__ */ jsx4(
551
733
  "input",
552
734
  {
553
735
  type: "range",
@@ -561,9 +743,9 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
561
743
  }
562
744
  )
563
745
  ] }),
564
- 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: [
565
747
  "Size",
566
- /* @__PURE__ */ jsx3(
748
+ /* @__PURE__ */ jsx4(
567
749
  "input",
568
750
  {
569
751
  type: "number",
@@ -579,12 +761,12 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
579
761
  }
580
762
  )
581
763
  ] }),
582
- state.tool === "paintBucket" && /* @__PURE__ */ jsx3(
764
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx4(
583
765
  "button",
584
766
  {
585
767
  type: "button",
586
768
  "data-vector-fill-gap": "",
587
- className: buttonClass,
769
+ className: buttonClass2,
588
770
  "aria-pressed": editing.fillGap > 0,
589
771
  title: "Close small gaps between path ends when filling",
590
772
  "aria-label": "Close gaps",
@@ -593,21 +775,35 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
593
775
  }
594
776
  )
595
777
  ] }),
596
- showArrange && /* @__PURE__ */ jsxs3(Fragment2, { children: [
597
- /* @__PURE__ */ jsxs3("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
598
- act("\u222A", "Union", () => controller.booleanOp("union"), count < 2),
599
- act("\u2212", "Subtract", () => controller.booleanOp("subtract"), count < 2),
600
- act("\u2229", "Intersect", () => controller.booleanOp("intersect"), count < 2),
601
- 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),
602
798
  act("G", "Group", () => controller.group(), count < 1),
603
799
  act("U", "Ungroup", () => controller.ungroup(), !groupSelected)
604
800
  ] }),
605
- /* @__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(
606
802
  "button",
607
803
  {
608
804
  type: "button",
609
805
  "data-vector-pathfinder": a.id,
610
- className: buttonClass,
806
+ className: buttonClass2,
611
807
  title: a.title,
612
808
  "aria-label": a.title,
613
809
  disabled: count < a.min,
@@ -616,7 +812,7 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
616
812
  },
617
813
  a.id
618
814
  )) }),
619
- /* @__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: [
620
816
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
621
817
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
622
818
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -629,8 +825,71 @@ function VectorToolbar({ controller, tools = VECTOR_TOOLS, showHistory = true, s
629
825
  ] })
630
826
  ] });
631
827
  }
632
- var selectClass = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
633
- var numberClass = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
828
+ function VectorBlendControls({ controller }) {
829
+ const state = useVectorEditorState(controller);
830
+ const blend = getVectorBlendControls(state);
831
+ const o = blend.options;
832
+ 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(
836
+ "select",
837
+ {
838
+ "aria-label": "Blend spacing",
839
+ className: "h-7 rounded border border-border bg-background px-1 text-xs",
840
+ value: o.spacing,
841
+ 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))
843
+ }
844
+ ),
845
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx4(
846
+ "input",
847
+ {
848
+ type: "number",
849
+ min: o.spacing === "steps" ? 1 : 0.5,
850
+ step: 1,
851
+ "aria-label": o.spacing === "steps" ? "Blend steps" : "Blend distance",
852
+ className: "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs",
853
+ value: o.spacing === "steps" ? o.steps : o.distance,
854
+ onChange: (e) => {
855
+ const n = parseFloat(e.target.value);
856
+ if (!Number.isFinite(n) || n <= 0) return;
857
+ applyVectorBlendOptions(controller, o.spacing === "steps" ? { steps: n } : { distance: n });
858
+ }
859
+ }
860
+ ),
861
+ /* @__PURE__ */ jsx4(
862
+ "button",
863
+ {
864
+ type: "button",
865
+ "data-vector-blend-action": "orient",
866
+ className: buttonClass2,
867
+ "aria-pressed": o.orientToSpine,
868
+ title: "Orient steps to the spine",
869
+ "aria-label": "Orient to spine",
870
+ onClick: () => applyVectorBlendOptions(controller, { orientToSpine: !o.orientToSpine }),
871
+ children: "\u293F"
872
+ }
873
+ ),
874
+ /* @__PURE__ */ jsx4(
875
+ "button",
876
+ {
877
+ type: "button",
878
+ "data-vector-blend-action": "spine",
879
+ className: buttonClass2,
880
+ title: "Replace Spine (select a blend and a path)",
881
+ "aria-label": "Replace Spine",
882
+ disabled: !blend.canReplaceSpine,
883
+ onClick: () => controller.replaceBlendSpine(),
884
+ children: "\u2307"
885
+ }
886
+ ),
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" })
889
+ ] });
890
+ }
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";
634
893
  function VectorStylePanel({ controller, className }) {
635
894
  const state = useVectorEditorState(controller);
636
895
  const style = getVectorSelectionStyle(state);
@@ -661,17 +920,17 @@ function VectorStylePanel({ controller, className }) {
661
920
  if (style) controller.setWidthProfile(id);
662
921
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
663
922
  };
664
- 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;
665
924
  const arrowRow = (end, label) => {
666
925
  const key = end === "start" ? "startArrow" : "endArrow";
667
926
  const scaleKey = end === "start" ? "startArrowScale" : "endArrowScale";
668
- return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
669
- /* @__PURE__ */ jsx3("span", { className: "w-16 text-xs text-foreground", children: label }),
670
- /* @__PURE__ */ jsxs3("select", { "aria-label": `${label} arrowhead`, className: selectClass, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
671
- /* @__PURE__ */ jsx3("option", { value: "", children: "None" }),
672
- 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))
673
932
  ] }),
674
- /* @__PURE__ */ jsx3(
933
+ /* @__PURE__ */ jsx4(
675
934
  "input",
676
935
  {
677
936
  type: "number",
@@ -679,7 +938,7 @@ function VectorStylePanel({ controller, className }) {
679
938
  step: 0.25,
680
939
  "aria-label": `${label} arrowhead scale`,
681
940
  title: "Scale (\xD7 stroke width)",
682
- className: numberClass,
941
+ className: numberClass2,
683
942
  value: source[scaleKey],
684
943
  onChange: (e) => {
685
944
  const n = parseFloat(e.target.value);
@@ -697,12 +956,12 @@ function VectorStylePanel({ controller, className }) {
697
956
  const paint = source[key];
698
957
  const hex = vectorPaintToHex(paint);
699
958
  const on = paint !== null && paint !== "none";
700
- return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
701
- /* @__PURE__ */ jsxs3("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
702
- /* @__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 }) }),
703
962
  label
704
963
  ] }),
705
- /* @__PURE__ */ jsx3(
964
+ /* @__PURE__ */ jsx4(
706
965
  "input",
707
966
  {
708
967
  type: "color",
@@ -713,17 +972,17 @@ function VectorStylePanel({ controller, className }) {
713
972
  onChange: (e) => apply({ [key]: e.target.value })
714
973
  }
715
974
  ),
716
- on && !hex && /* @__PURE__ */ jsx3("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
717
- 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" })
718
977
  ] });
719
978
  };
720
- return /* @__PURE__ */ jsxs3("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
721
- /* @__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" }),
722
981
  paintRow("fill", "Fill"),
723
982
  paintRow("stroke", "Stroke"),
724
- /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
725
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Width" }),
726
- /* @__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(
727
986
  "input",
728
987
  {
729
988
  type: "number",
@@ -739,33 +998,33 @@ function VectorStylePanel({ controller, className }) {
739
998
  }
740
999
  )
741
1000
  ] }),
742
- /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
743
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Align" }),
744
- /* @__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)) }),
745
1004
  mixed("strokeAlign")
746
1005
  ] }),
747
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
748
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Profile" }),
749
- /* @__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(
750
1009
  "select",
751
1010
  {
752
1011
  "aria-label": "Width profile",
753
- className: selectClass,
1012
+ className: selectClass2,
754
1013
  value: source.widthProfile,
755
1014
  onChange: (e) => {
756
1015
  if (e.target.value !== "custom") setProfile(e.target.value);
757
1016
  },
758
1017
  children: [
759
- VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx3("option", { value: p.id, children: p.label }, p.id)),
760
- 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" })
761
1020
  ]
762
1021
  }
763
1022
  ),
764
- /* @__PURE__ */ jsx3(
1023
+ /* @__PURE__ */ jsx4(
765
1024
  "button",
766
1025
  {
767
1026
  type: "button",
768
- className: buttonClass,
1027
+ className: buttonClass2,
769
1028
  title: "Reset to uniform width",
770
1029
  "aria-label": "Reset to uniform width",
771
1030
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -775,27 +1034,27 @@ function VectorStylePanel({ controller, className }) {
775
1034
  ),
776
1035
  mixed("widthProfile")
777
1036
  ] }),
778
- source.hasOpenPath && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1037
+ source.hasOpenPath && /* @__PURE__ */ jsxs4(Fragment2, { children: [
779
1038
  arrowRow("start", "Start"),
780
1039
  arrowRow("end", "End"),
781
- (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
782
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Heads" }),
783
- /* @__PURE__ */ jsxs3("select", { "aria-label": "Arrowhead alignment", className: selectClass, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
784
- /* @__PURE__ */ jsx3("option", { value: "extend", children: "Tip past end" }),
785
- /* @__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" })
786
1045
  ] })
787
1046
  ] })
788
1047
  ] }),
789
- source.corners > 0 && /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
790
- /* @__PURE__ */ jsx3("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
791
- /* @__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(
792
1051
  "input",
793
1052
  {
794
1053
  type: "number",
795
1054
  min: 0,
796
1055
  step: 1,
797
1056
  "aria-label": "Corner radius",
798
- className: numberClass,
1057
+ className: numberClass2,
799
1058
  value: Math.round(source.cornerRadius * 100) / 100,
800
1059
  onChange: (e) => {
801
1060
  const n = parseFloat(e.target.value);
@@ -803,12 +1062,12 @@ function VectorStylePanel({ controller, className }) {
803
1062
  }
804
1063
  }
805
1064
  ),
806
- /* @__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)) }),
807
1066
  mixed("cornerRadius")
808
1067
  ] }),
809
- style && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
810
- /* @__PURE__ */ jsx3("span", { className: "w-16", children: "Opacity" }),
811
- /* @__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(
812
1071
  "input",
813
1072
  {
814
1073
  type: "range",
@@ -821,24 +1080,26 @@ function VectorStylePanel({ controller, className }) {
821
1080
  onChange: (e) => apply({ opacity: parseFloat(e.target.value) })
822
1081
  }
823
1082
  )
824
- ] })
1083
+ ] }),
1084
+ style && /* @__PURE__ */ jsx4(VectorEffectsPanel, { controller })
825
1085
  ] });
826
1086
  }
827
1087
 
828
1088
  // src/vector/VectorEditor.tsx
829
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1089
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
830
1090
  function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
831
- return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
832
- /* @__PURE__ */ jsx4(VectorToolbar, { controller, tools }),
833
- /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
834
- /* @__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 }) }),
835
- 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" })
836
1096
  ] })
837
1097
  ] });
838
1098
  }
839
1099
  export {
840
1100
  VectorCanvas,
841
1101
  VectorEditor,
1102
+ VectorEffectsPanel,
842
1103
  VectorGradientDefs,
843
1104
  VectorSceneNodes,
844
1105
  VectorStylePanel,