@pixerate/editor-react 0.65.0 → 0.67.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.
@@ -44,7 +44,7 @@ function useVectorCommands(registry, controller, options = {}) {
44
44
  }
45
45
 
46
46
  // src/vector/VectorCanvas.tsx
47
- import { useEffect as useEffect2, useId, useRef as useRef3 } from "react";
47
+ import React2, { useEffect as useEffect2, useId, useRef as useRef3 } from "react";
48
48
  import {
49
49
  VECTOR_OVERLAY_THEME as THEME2,
50
50
  getVectorOverlayRenderData,
@@ -185,37 +185,95 @@ function VectorGradientAnnotator({ annotator: g, accentColor = THEME2.accent })
185
185
  ] }, s.index))
186
186
  ] });
187
187
  }
188
+ function VectorFilterDefs({ filters }) {
189
+ if (!filters.length) return null;
190
+ const primitive = (p, key) => React2.createElement(p.tag, { key, ...p.attrs }, p.children?.map(primitive));
191
+ return /* @__PURE__ */ jsx2("defs", { children: filters.map((f) => /* @__PURE__ */ jsx2("filter", { id: f.id, filterUnits: "userSpaceOnUse", x: f.x, y: f.y, width: f.width, height: f.height, colorInterpolationFilters: "sRGB", children: f.primitives.map(primitive) }, f.id)) });
192
+ }
193
+ var blendStyle = (blendMode) => blendMode ? { mixBlendMode: blendMode } : void 0;
188
194
  function VectorSceneNodes({ nodes }) {
189
195
  return /* @__PURE__ */ jsx2(Fragment, { children: nodes.map(
190
- (node) => node.kind === "group" ? /* @__PURE__ */ jsx2("g", { "data-vector-id": node.id, transform: node.transform, opacity: node.opacity < 1 ? node.opacity : void 0, children: /* @__PURE__ */ jsx2(VectorSceneNodes, { nodes: node.children }) }, node.id) : node.strokeParts ? /* @__PURE__ */ jsx2(VectorPartedPath, { node }, node.id) : /* @__PURE__ */ jsx2(
191
- "path",
196
+ (node) => node.kind === "group" ? /* @__PURE__ */ jsx2(
197
+ "g",
192
198
  {
193
199
  "data-vector-id": node.id,
194
200
  transform: node.transform,
201
+ opacity: node.opacity < 1 ? node.opacity : void 0,
202
+ filter: node.filter,
203
+ style: blendStyle(node.blendMode),
204
+ children: /* @__PURE__ */ jsx2(VectorSceneNodes, { nodes: node.children })
205
+ },
206
+ node.id
207
+ ) : node.layers ? (
208
+ // Fill and stroke stacks: each layer in order, bottom to top.
209
+ /* @__PURE__ */ jsx2(
210
+ "g",
211
+ {
212
+ "data-vector-id": node.id,
213
+ transform: node.transform,
214
+ opacity: node.opacity < 1 ? node.opacity : void 0,
215
+ filter: node.filter,
216
+ style: blendStyle(node.blendMode),
217
+ children: node.layers.map((layer) => /* @__PURE__ */ jsx2(VectorPaintPath, { paint: layer, d: node.d, fillRule: node.fillRule, layer: `${layer.kind}-${layer.index}` }, `${layer.kind}-${layer.index}`))
218
+ },
219
+ node.id
220
+ )
221
+ ) : /* @__PURE__ */ jsx2(
222
+ VectorPaintPath,
223
+ {
224
+ paint: node,
195
225
  d: node.d,
196
- fill: node.fill,
197
226
  fillRule: node.fillRule,
198
- stroke: node.stroke,
199
- strokeWidth: node.stroke === "none" ? void 0 : node.strokeWidth,
200
- strokeLinecap: node.strokeLinecap,
201
- strokeLinejoin: node.strokeLinejoin,
202
- strokeDasharray: node.strokeDasharray,
203
- opacity: node.opacity < 1 ? node.opacity : void 0
227
+ id: node.id,
228
+ transform: node.transform,
229
+ filter: node.filter,
230
+ blendMode: node.blendMode
204
231
  },
205
232
  node.id
206
233
  )
207
234
  ) });
208
235
  }
209
- function VectorPartedPath({ node }) {
210
- const sp = node.strokeParts;
236
+ function VectorPaintPath({
237
+ paint,
238
+ d,
239
+ fillRule,
240
+ id,
241
+ transform,
242
+ filter,
243
+ blendMode,
244
+ layer
245
+ }) {
246
+ const opacity = paint.opacity < 1 ? paint.opacity : void 0;
247
+ const style = blendStyle(blendMode ?? (layer ? paint.blendMode : void 0));
248
+ const sp = paint.strokeParts;
249
+ if (!sp)
250
+ return /* @__PURE__ */ jsx2(
251
+ "path",
252
+ {
253
+ "data-vector-id": id,
254
+ "data-vector-layer": layer,
255
+ transform,
256
+ d,
257
+ fill: paint.fill,
258
+ fillRule,
259
+ stroke: paint.stroke,
260
+ strokeWidth: paint.stroke === "none" ? void 0 : paint.strokeWidth,
261
+ strokeLinecap: paint.strokeLinecap,
262
+ strokeLinejoin: paint.strokeLinejoin,
263
+ strokeDasharray: paint.strokeDasharray,
264
+ opacity,
265
+ filter,
266
+ style
267
+ }
268
+ );
211
269
  const clip = sp.clip;
212
270
  const b = clip?.bounds;
213
- return /* @__PURE__ */ jsxs2("g", { "data-vector-id": node.id, "data-vector-stroke-align": sp.align, transform: node.transform, opacity: node.opacity < 1 ? node.opacity : void 0, children: [
214
- clip && b && /* @__PURE__ */ jsx2("defs", { children: clip.kind === "clip" ? /* @__PURE__ */ jsx2("clipPath", { id: clip.id, children: /* @__PURE__ */ jsx2("path", { d: node.d, clipRule: node.fillRule }) }) : /* @__PURE__ */ jsxs2("mask", { id: clip.id, maskUnits: "userSpaceOnUse", x: b.x, y: b.y, width: b.width, height: b.height, children: [
271
+ return /* @__PURE__ */ jsxs2("g", { "data-vector-id": id, "data-vector-layer": layer, "data-vector-stroke-align": sp.align, transform, opacity, filter, style, children: [
272
+ clip && b && /* @__PURE__ */ jsx2("defs", { children: clip.kind === "clip" ? /* @__PURE__ */ jsx2("clipPath", { id: clip.id, children: /* @__PURE__ */ jsx2("path", { d, clipRule: fillRule }) }) : /* @__PURE__ */ jsxs2("mask", { id: clip.id, maskUnits: "userSpaceOnUse", x: b.x, y: b.y, width: b.width, height: b.height, children: [
215
273
  /* @__PURE__ */ jsx2("rect", { x: b.x, y: b.y, width: b.width, height: b.height, fill: "#fff" }),
216
- /* @__PURE__ */ jsx2("path", { d: node.d, fill: "#000", fillRule: node.fillRule })
274
+ /* @__PURE__ */ jsx2("path", { d, fill: "#000", fillRule })
217
275
  ] }) }),
218
- /* @__PURE__ */ jsx2("path", { "data-vector-part": "fill", d: node.d, fill: node.fill, fillRule: node.fillRule, stroke: "none" }),
276
+ /* @__PURE__ */ jsx2("path", { "data-vector-part": "fill", d, fill: paint.fill, fillRule, stroke: "none" }),
219
277
  sp.outline !== void 0 ? (
220
278
  // A variable-width stroke: its outline, filled.
221
279
  /* @__PURE__ */ jsx2(
@@ -223,7 +281,7 @@ function VectorPartedPath({ node }) {
223
281
  {
224
282
  "data-vector-part": "stroke-outline",
225
283
  d: sp.outline,
226
- fill: node.stroke,
284
+ fill: paint.stroke,
227
285
  clipPath: clip?.kind === "clip" ? `url(#${clip.id})` : void 0,
228
286
  mask: clip?.kind === "mask" ? `url(#${clip.id})` : void 0
229
287
  }
@@ -234,16 +292,16 @@ function VectorPartedPath({ node }) {
234
292
  "data-vector-part": "stroke",
235
293
  d: sp.d,
236
294
  fill: "none",
237
- stroke: node.stroke,
295
+ stroke: paint.stroke,
238
296
  strokeWidth: sp.width,
239
- strokeLinecap: node.strokeLinecap,
240
- strokeLinejoin: node.strokeLinejoin,
241
- strokeDasharray: node.strokeDasharray,
297
+ strokeLinecap: paint.strokeLinecap,
298
+ strokeLinejoin: paint.strokeLinejoin,
299
+ strokeDasharray: paint.strokeDasharray,
242
300
  clipPath: clip?.kind === "clip" ? `url(#${clip.id})` : void 0,
243
301
  mask: clip?.kind === "mask" ? `url(#${clip.id})` : void 0
244
302
  }
245
303
  ),
246
- sp.heads.map((head, i) => /* @__PURE__ */ jsx2("path", { "data-vector-part": "head", d: head, fill: node.stroke }, i))
304
+ sp.heads.map((head, i) => /* @__PURE__ */ jsx2("path", { "data-vector-part": "head", d: head, fill: paint.stroke }, i))
247
305
  ] });
248
306
  }
249
307
  function VectorCanvas({
@@ -349,6 +407,7 @@ function VectorCanvas({
349
407
  /* @__PURE__ */ jsx2("rect", { x: 8, y: 8, width: 8, height: 8, fill: "#e5e7eb" })
350
408
  ] }) }),
351
409
  /* @__PURE__ */ jsx2(VectorGradientDefs, { defs: scene.defs }),
410
+ /* @__PURE__ */ jsx2(VectorFilterDefs, { filters: scene.filters }),
352
411
  /* @__PURE__ */ jsx2(
353
412
  "rect",
354
413
  {
@@ -541,7 +600,7 @@ import {
541
600
  VECTOR_ARROWHEAD_LABELS,
542
601
  VECTOR_CORNER_KINDS,
543
602
  VECTOR_CORNER_KIND_LABELS,
544
- VECTOR_STROKE_ALIGN_LABELS,
603
+ VECTOR_STROKE_ALIGN_LABELS as VECTOR_STROKE_ALIGN_LABELS2,
545
604
  VECTOR_WIDTH_PRESETS,
546
605
  VECTOR_BLEND_SPACINGS,
547
606
  VECTOR_BLEND_SPACING_LABELS,
@@ -697,14 +756,227 @@ function VectorEffectsPanel({ controller, className }) {
697
756
  ] });
698
757
  }
699
758
 
700
- // src/vector/VectorEnvelopePanel.tsx
701
- import React3 from "react";
702
- import { VECTOR_ENVELOPE_STYLE_OPTIONS, getVectorEnvelopePanel } from "@pixerate/editor/vector";
759
+ // src/vector/VectorAppearancePanel.tsx
760
+ import {
761
+ VECTOR_BLEND_MODE_MENU,
762
+ VECTOR_FILTER_MENU,
763
+ VECTOR_STROKE_ALIGN_LABELS,
764
+ getVectorAppearancePanel
765
+ } from "@pixerate/editor/vector";
703
766
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
704
767
  var buttonClass2 = "inline-flex h-7 min-w-7 items-center justify-center rounded-md border border-border bg-background px-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40";
705
768
  var numberClass2 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
706
769
  var selectClass2 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
770
+ var headingClass = "text-[11px] font-medium uppercase tracking-wide text-muted-foreground";
707
771
  var labelClass2 = "w-full truncate text-[11px] text-muted-foreground";
772
+ function VectorAppearancePanel({ controller, className }) {
773
+ const state = useVectorEditorState(controller);
774
+ const panel = getVectorAppearancePanel(state);
775
+ if (!panel) return null;
776
+ const end = () => controller.endMergedEdit();
777
+ const ids = panel.pathIds;
778
+ const update = (l, patch, merge = false) => controller.updatePaintLayer(l.kind, l.index, patch, { ids, merge });
779
+ const layerRow = (l, count) => {
780
+ const name = `${l.kind === "fill" ? "Fill" : "Stroke"} ${l.index + 1}`;
781
+ if (l.unknownKind)
782
+ return /* @__PURE__ */ jsxs4("div", { className: "text-[11px] text-muted-foreground", "data-vector-layer": `${l.kind}-${l.index}`, children: [
783
+ name,
784
+ ": ",
785
+ l.unknownKind,
786
+ " (not drawn by this version)"
787
+ ] }, `${l.kind}-${l.index}`);
788
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-1 rounded border border-border p-1", "data-vector-layer": `${l.kind}-${l.index}`, "data-vector-layer-editing": l.editing ? "" : void 0, children: [
789
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
790
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", "aria-label": `${name} visible`, title: "Show", checked: l.visible, onChange: (e) => update(l, { visible: e.target.checked }) }),
791
+ l.gradient ? /* @__PURE__ */ jsx4(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ "aria-label": `${name} gradient`,
796
+ title: "Edit this gradient",
797
+ className: "h-7 w-10 rounded border border-border",
798
+ style: { background: l.swatch },
799
+ onClick: () => controller.setGradientTarget(l.kind, l.index)
800
+ }
801
+ ) : /* @__PURE__ */ jsx4(
802
+ "input",
803
+ {
804
+ type: "color",
805
+ "aria-label": `${name} colour`,
806
+ className: "h-7 w-10 cursor-pointer rounded border border-border bg-background",
807
+ value: l.color ?? "#000000",
808
+ onChange: (e) => update(l, { paint: e.target.value }, true),
809
+ onBlur: end
810
+ }
811
+ ),
812
+ /* @__PURE__ */ jsx4(
813
+ "input",
814
+ {
815
+ type: "number",
816
+ min: 0,
817
+ max: 100,
818
+ step: 5,
819
+ "aria-label": `${name} opacity`,
820
+ title: "Opacity %",
821
+ className: numberClass2,
822
+ value: Math.round(l.opacity * 100),
823
+ onChange: (e) => {
824
+ const n = parseFloat(e.target.value);
825
+ if (Number.isFinite(n)) update(l, { opacity: Math.min(100, Math.max(0, n)) / 100 }, true);
826
+ },
827
+ onBlur: end
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx4("select", { "aria-label": `${name} blend mode`, className: selectClass2, value: l.blendMode, onChange: (e) => update(l, { blendMode: e.target.value }), children: VECTOR_BLEND_MODE_MENU.map((m) => /* @__PURE__ */ jsx4("option", { value: m.mode, children: m.label }, m.mode)) })
831
+ ] }),
832
+ l.kind === "stroke" && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
833
+ /* @__PURE__ */ jsx4(
834
+ "input",
835
+ {
836
+ type: "number",
837
+ min: 0,
838
+ step: 0.5,
839
+ "aria-label": `${name} width`,
840
+ title: "Width",
841
+ className: numberClass2,
842
+ value: l.strokeWidth ?? 1,
843
+ onChange: (e) => {
844
+ const n = parseFloat(e.target.value);
845
+ if (Number.isFinite(n) && n >= 0) update(l, { strokeWidth: n }, true);
846
+ },
847
+ onBlur: end
848
+ }
849
+ ),
850
+ /* @__PURE__ */ jsx4("select", { "aria-label": `${name} alignment`, className: selectClass2, value: l.strokeAlign ?? "center", onChange: (e) => update(l, { 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)) })
851
+ ] }),
852
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
853
+ /* @__PURE__ */ jsx4("span", { className: "flex-1" }),
854
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Edit gradient", "aria-label": `Edit ${name} gradient`, "aria-pressed": l.editing, onClick: () => controller.setGradientTarget(l.kind, l.index), children: "\u270E" }),
855
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Move up", "aria-label": `Move ${name} up`, disabled: l.index === count - 1, onClick: () => controller.movePaintLayer(l.kind, l.index, l.index + 1, { ids }), children: "\u2191" }),
856
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Move down", "aria-label": `Move ${name} down`, disabled: l.index === 0, onClick: () => controller.movePaintLayer(l.kind, l.index, l.index - 1, { ids }), children: "\u2193" }),
857
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Remove", "aria-label": `Remove ${name}`, onClick: () => controller.removePaintLayer(l.kind, l.index, { ids }), children: "\xD7" })
858
+ ] })
859
+ ] }, `${l.kind}-${l.index}`);
860
+ };
861
+ const stack = (kind, layers, mixed) => /* @__PURE__ */ jsxs4("div", { className: "space-y-1", "data-vector-stack": kind, children: [
862
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
863
+ /* @__PURE__ */ jsx4("span", { className: `flex-1 ${headingClass}`, children: kind === "fill" ? "Fills" : "Strokes" }),
864
+ mixed && /* @__PURE__ */ jsx4("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }),
865
+ /* @__PURE__ */ jsx4(
866
+ "button",
867
+ {
868
+ type: "button",
869
+ className: buttonClass2,
870
+ title: kind === "fill" ? "Add fill" : "Add stroke",
871
+ "aria-label": kind === "fill" ? "Add fill" : "Add stroke",
872
+ disabled: !ids.length,
873
+ onClick: () => controller.addPaintLayer(kind, {}, { ids }),
874
+ children: "+"
875
+ }
876
+ )
877
+ ] }),
878
+ [...layers].reverse().map((l) => layerRow(l, layers.length))
879
+ ] });
880
+ const filterNode = panel.filterNodeId;
881
+ const setFilter = (f, key, value, merge) => {
882
+ if (filterNode) controller.updateFilter(filterNode, f.id, { params: { [key]: value } }, { merge });
883
+ };
884
+ const filterParam = (f, spec) => {
885
+ const label = `${f.label} ${spec.label}`;
886
+ if (spec.kind === "color")
887
+ return /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-filter-param": spec.key, children: [
888
+ /* @__PURE__ */ jsx4("span", { className: "w-14 truncate text-[11px] text-muted-foreground", children: spec.label }),
889
+ /* @__PURE__ */ jsx4(
890
+ "input",
891
+ {
892
+ type: "color",
893
+ "aria-label": label,
894
+ className: "h-7 w-10 cursor-pointer rounded border border-border bg-background",
895
+ value: /^#[0-9a-f]{6}$/i.test(String(f.params[spec.key])) ? String(f.params[spec.key]) : "#000000",
896
+ onChange: (e) => setFilter(f, spec.key, e.target.value, true),
897
+ onBlur: end
898
+ }
899
+ )
900
+ ] }, spec.key);
901
+ if (spec.kind !== "number") return null;
902
+ const value = Number(f.params[spec.key] ?? 0);
903
+ const onChange = (e) => {
904
+ const n = parseFloat(e.target.value);
905
+ if (Number.isFinite(n)) setFilter(f, spec.key, n, true);
906
+ };
907
+ return /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-filter-param": spec.key, children: [
908
+ /* @__PURE__ */ jsx4("span", { className: labelClass2, children: spec.label }),
909
+ spec.slider && /* @__PURE__ */ jsx4(
910
+ "input",
911
+ {
912
+ type: "range",
913
+ min: spec.min,
914
+ max: spec.max,
915
+ step: spec.step,
916
+ "aria-label": `${label} slider`,
917
+ className: "h-4 min-w-0 flex-1 accent-primary",
918
+ value,
919
+ onChange,
920
+ onPointerUp: end,
921
+ onKeyUp: end,
922
+ onBlur: end
923
+ }
924
+ ),
925
+ /* @__PURE__ */ jsx4("input", { type: "number", min: spec.min, max: spec.max, step: spec.step, "aria-label": label, className: numberClass2, value: Math.round(value * 1e3) / 1e3, onChange, onBlur: end })
926
+ ] }, spec.key);
927
+ };
928
+ return /* @__PURE__ */ jsxs4("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-appearance": "", "aria-label": "Appearance", children: [
929
+ ids.length > 0 && stack("stroke", panel.strokes, panel.mixed.strokes),
930
+ ids.length > 0 && stack("fill", panel.fills, panel.mixed.fills),
931
+ /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "blendMode", children: [
932
+ /* @__PURE__ */ jsx4("span", { className: "w-16", children: "Blend" }),
933
+ /* @__PURE__ */ jsx4("select", { "aria-label": "Blend mode", className: selectClass2, value: panel.blendMode, onChange: (e) => controller.setBlendMode(e.target.value, { ids: panel.nodeIds }), children: VECTOR_BLEND_MODE_MENU.map((m) => /* @__PURE__ */ jsx4("option", { value: m.mode, children: m.label }, m.mode)) }),
934
+ panel.mixed.blendMode && /* @__PURE__ */ jsx4("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
935
+ ] }),
936
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", "data-vector-filters": "", children: [
937
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
938
+ /* @__PURE__ */ jsx4("span", { className: `w-16 ${headingClass}`, children: "Filters" }),
939
+ /* @__PURE__ */ jsxs4(
940
+ "select",
941
+ {
942
+ "aria-label": "Add filter",
943
+ className: selectClass2,
944
+ value: "",
945
+ onChange: (e) => {
946
+ if (e.target.value) controller.addFilter(e.target.value, { ids: panel.nodeIds });
947
+ },
948
+ children: [
949
+ /* @__PURE__ */ jsx4("option", { value: "", children: "Add filter\u2026" }),
950
+ VECTOR_FILTER_MENU.map((m) => /* @__PURE__ */ jsx4("option", { value: m.type, children: m.label }, m.type))
951
+ ]
952
+ }
953
+ )
954
+ ] }),
955
+ !filterNode && /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-muted-foreground", children: "Select one object to edit its filters." }),
956
+ filterNode && panel.filters.map((f, i) => /* @__PURE__ */ jsxs4("div", { className: "space-y-1 rounded border border-border p-1.5", "data-vector-filter": f.id, "data-vector-filter-type": f.type, children: [
957
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
958
+ /* @__PURE__ */ jsxs4("label", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-xs font-medium text-foreground", children: [
959
+ /* @__PURE__ */ jsx4("input", { type: "checkbox", "aria-label": `${f.label} on`, checked: f.enabled, onChange: (e) => controller.updateFilter(filterNode, f.id, { enabled: e.target.checked }) }),
960
+ /* @__PURE__ */ jsx4("span", { className: "truncate", children: f.label })
961
+ ] }),
962
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Move up", "aria-label": `Move ${f.label} up`, disabled: i === 0, onClick: () => controller.moveFilter(filterNode, f.id, i - 1), children: "\u2191" }),
963
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Move down", "aria-label": `Move ${f.label} down`, disabled: i === panel.filters.length - 1, onClick: () => controller.moveFilter(filterNode, f.id, i + 1), children: "\u2193" }),
964
+ /* @__PURE__ */ jsx4("button", { type: "button", className: buttonClass2, title: "Remove", "aria-label": `Remove ${f.label}`, onClick: () => controller.removeFilter(filterNode, f.id), children: "\xD7" })
965
+ ] }),
966
+ f.specs.map((spec) => filterParam(f, spec))
967
+ ] }, f.id))
968
+ ] })
969
+ ] });
970
+ }
971
+
972
+ // src/vector/VectorEnvelopePanel.tsx
973
+ import React3 from "react";
974
+ import { VECTOR_ENVELOPE_STYLE_OPTIONS, getVectorEnvelopePanel } from "@pixerate/editor/vector";
975
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
976
+ var buttonClass3 = "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";
977
+ var numberClass3 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
978
+ var selectClass3 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
979
+ var labelClass3 = "w-full truncate text-[11px] text-muted-foreground";
708
980
  var SLIDERS = [
709
981
  { key: "bend", label: "Bend %" },
710
982
  { key: "horizontal", label: "Horizontal distortion %" },
@@ -727,16 +999,16 @@ function VectorEnvelopePanel({ controller, className }) {
727
999
  if (env) set({ [key]: n });
728
1000
  else setSize({ ...size, [key]: n });
729
1001
  };
730
- return /* @__PURE__ */ jsxs4("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-envelope": "", "aria-label": "Envelope", children: [
731
- /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", children: [
732
- /* @__PURE__ */ jsx4("span", { className: "w-16 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Envelope" }),
733
- /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-envelope-action": "warp", className: buttonClass2, title: "Make with Warp", "aria-label": "Make with Warp", disabled: !panel.canMake || !!env, onClick: () => set({ style: "arc", bend: 50 }), children: "Warp" }),
734
- /* @__PURE__ */ jsx4(
1002
+ return /* @__PURE__ */ jsxs5("div", { className: `space-y-2 ${className ?? ""}`, "data-vector-envelope": "", "aria-label": "Envelope", children: [
1003
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-1", children: [
1004
+ /* @__PURE__ */ jsx5("span", { className: "w-16 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "Envelope" }),
1005
+ /* @__PURE__ */ jsx5("button", { type: "button", "data-vector-envelope-action": "warp", className: buttonClass3, title: "Make with Warp", "aria-label": "Make with Warp", disabled: !panel.canMake || !!env, onClick: () => set({ style: "arc", bend: 50 }), children: "Warp" }),
1006
+ /* @__PURE__ */ jsx5(
735
1007
  "button",
736
1008
  {
737
1009
  type: "button",
738
1010
  "data-vector-envelope-action": "mesh",
739
- className: buttonClass2,
1011
+ className: buttonClass3,
740
1012
  title: "Make with Mesh",
741
1013
  "aria-label": "Make with Mesh",
742
1014
  disabled: !panel.canMake || !!env,
@@ -744,12 +1016,12 @@ function VectorEnvelopePanel({ controller, className }) {
744
1016
  children: "Mesh"
745
1017
  }
746
1018
  ),
747
- /* @__PURE__ */ jsx4(
1019
+ /* @__PURE__ */ jsx5(
748
1020
  "button",
749
1021
  {
750
1022
  type: "button",
751
1023
  "data-vector-envelope-action": "top",
752
- className: buttonClass2,
1024
+ className: buttonClass3,
753
1025
  title: "Make with Top Object",
754
1026
  "aria-label": "Make with Top Object",
755
1027
  disabled: !panel.canMakeWithTopObject,
@@ -757,12 +1029,12 @@ function VectorEnvelopePanel({ controller, className }) {
757
1029
  children: "Top"
758
1030
  }
759
1031
  ),
760
- /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-envelope-action": "release", className: buttonClass2, title: "Release Envelope", "aria-label": "Release Envelope", disabled: !panel.canRelease, onClick: () => controller.releaseEnvelope(), children: "Release" }),
761
- /* @__PURE__ */ jsx4("button", { type: "button", "data-vector-envelope-action": "expand", className: buttonClass2, title: "Expand Envelope", "aria-label": "Expand Envelope", disabled: !panel.canRelease, onClick: () => controller.expandAppearance(), children: "Expand" })
1032
+ /* @__PURE__ */ jsx5("button", { type: "button", "data-vector-envelope-action": "release", className: buttonClass3, title: "Release Envelope", "aria-label": "Release Envelope", disabled: !panel.canRelease, onClick: () => controller.releaseEnvelope(), children: "Release" }),
1033
+ /* @__PURE__ */ jsx5("button", { type: "button", "data-vector-envelope-action": "expand", className: buttonClass3, title: "Expand Envelope", "aria-label": "Expand Envelope", disabled: !panel.canRelease, onClick: () => controller.expandAppearance(), children: "Expand" })
762
1034
  ] }),
763
- env && /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": "style", children: [
764
- /* @__PURE__ */ jsx4("span", { className: labelClass2, children: "Warp" }),
765
- /* @__PURE__ */ jsx4("select", { "aria-label": "Envelope warp", className: selectClass2, value: env.params.style, onChange: (e) => set({ style: e.target.value }), children: VECTOR_ENVELOPE_STYLE_OPTIONS.map((o) => /* @__PURE__ */ jsx4("option", { value: o.value, children: o.label }, o.value)) })
1035
+ env && /* @__PURE__ */ jsxs5("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": "style", children: [
1036
+ /* @__PURE__ */ jsx5("span", { className: labelClass3, children: "Warp" }),
1037
+ /* @__PURE__ */ jsx5("select", { "aria-label": "Envelope warp", className: selectClass3, value: env.params.style, onChange: (e) => set({ style: e.target.value }), children: VECTOR_ENVELOPE_STYLE_OPTIONS.map((o) => /* @__PURE__ */ jsx5("option", { value: o.value, children: o.label }, o.value)) })
766
1038
  ] }),
767
1039
  env && env.params.style !== "mesh" && SLIDERS.map((s) => {
768
1040
  const value = env.params[s.key];
@@ -770,9 +1042,9 @@ function VectorEnvelopePanel({ controller, className }) {
770
1042
  const n = parseFloat(ev.target.value);
771
1043
  if (Number.isFinite(n)) set({ [s.key]: n }, true);
772
1044
  };
773
- return /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": s.key, children: [
774
- /* @__PURE__ */ jsx4("span", { className: labelClass2, children: s.label }),
775
- /* @__PURE__ */ jsx4(
1045
+ return /* @__PURE__ */ jsxs5("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": s.key, children: [
1046
+ /* @__PURE__ */ jsx5("span", { className: labelClass3, children: s.label }),
1047
+ /* @__PURE__ */ jsx5(
776
1048
  "input",
777
1049
  {
778
1050
  type: "range",
@@ -788,23 +1060,23 @@ function VectorEnvelopePanel({ controller, className }) {
788
1060
  onBlur: end
789
1061
  }
790
1062
  ),
791
- /* @__PURE__ */ jsx4("input", { type: "number", min: -100, max: 100, step: 1, "aria-label": `Envelope ${s.label}`, className: numberClass2, value, onChange, onBlur: end })
1063
+ /* @__PURE__ */ jsx5("input", { type: "number", min: -100, max: 100, step: 1, "aria-label": `Envelope ${s.label}`, className: numberClass3, value, onChange, onBlur: end })
792
1064
  ] }, s.key);
793
1065
  }),
794
- env && env.params.style !== "mesh" && /* @__PURE__ */ jsxs4("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": "orientation", children: [
795
- /* @__PURE__ */ jsx4("span", { className: labelClass2, children: "Orientation" }),
796
- /* @__PURE__ */ jsxs4("select", { "aria-label": "Envelope orientation", className: selectClass2, value: env.params.orientation, onChange: (e) => set({ orientation: e.target.value }), children: [
797
- /* @__PURE__ */ jsx4("option", { value: "horizontal", children: "Horizontal" }),
798
- /* @__PURE__ */ jsx4("option", { value: "vertical", children: "Vertical" })
1066
+ env && env.params.style !== "mesh" && /* @__PURE__ */ jsxs5("label", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-foreground", "data-vector-envelope-param": "orientation", children: [
1067
+ /* @__PURE__ */ jsx5("span", { className: labelClass3, children: "Orientation" }),
1068
+ /* @__PURE__ */ jsxs5("select", { "aria-label": "Envelope orientation", className: selectClass3, value: env.params.orientation, onChange: (e) => set({ orientation: e.target.value }), children: [
1069
+ /* @__PURE__ */ jsx5("option", { value: "horizontal", children: "Horizontal" }),
1070
+ /* @__PURE__ */ jsx5("option", { value: "vertical", children: "Vertical" })
799
1071
  ] })
800
1072
  ] }),
801
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-envelope-param": "mesh", children: [
802
- /* @__PURE__ */ jsx4("span", { className: "min-w-0 flex-1 truncate text-[11px] text-muted-foreground", children: "Mesh rows \xD7 columns" }),
803
- /* @__PURE__ */ jsx4("input", { type: "number", min: 1, max: 16, step: 1, "aria-label": "Envelope mesh rows", className: numberClass2, value: rows, onChange: (e) => setCount("rows", e.target.value) }),
804
- /* @__PURE__ */ jsx4("input", { type: "number", min: 1, max: 16, step: 1, "aria-label": "Envelope mesh columns", className: numberClass2, value: columns, onChange: (e) => setCount("columns", e.target.value) })
1073
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-envelope-param": "mesh", children: [
1074
+ /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate text-[11px] text-muted-foreground", children: "Mesh rows \xD7 columns" }),
1075
+ /* @__PURE__ */ jsx5("input", { type: "number", min: 1, max: 16, step: 1, "aria-label": "Envelope mesh rows", className: numberClass3, value: rows, onChange: (e) => setCount("rows", e.target.value) }),
1076
+ /* @__PURE__ */ jsx5("input", { type: "number", min: 1, max: 16, step: 1, "aria-label": "Envelope mesh columns", className: numberClass3, value: columns, onChange: (e) => setCount("columns", e.target.value) })
805
1077
  ] }),
806
- panel.count > 1 && !panel.canMakeWithTopObject && /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-muted-foreground", children: "Select one object to edit its envelope." }),
807
- env && /* @__PURE__ */ jsx4("p", { className: "text-[11px] text-muted-foreground", children: "Edit the mesh with the Envelope tool (U)." })
1078
+ panel.count > 1 && !panel.canMakeWithTopObject && /* @__PURE__ */ jsx5("p", { className: "text-[11px] text-muted-foreground", children: "Select one object to edit its envelope." }),
1079
+ env && /* @__PURE__ */ jsx5("p", { className: "text-[11px] text-muted-foreground", children: "Edit the mesh with the Envelope tool (U)." })
808
1080
  ] });
809
1081
  }
810
1082
 
@@ -821,10 +1093,10 @@ import {
821
1093
  getVectorGradientEditor,
822
1094
  vectorPaintToHex
823
1095
  } from "@pixerate/editor/vector";
824
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
825
- var selectClass3 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
826
- var numberClass3 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
827
- var buttonClass3 = "inline-flex h-7 min-w-7 items-center justify-center rounded border border-border bg-background px-1.5 text-xs text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
1096
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1097
+ var selectClass4 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
1098
+ var numberClass4 = "h-7 w-14 rounded border border-border bg-background px-1.5 text-xs";
1099
+ var buttonClass4 = "inline-flex h-7 min-w-7 items-center justify-center rounded border border-border bg-background px-1.5 text-xs text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
828
1100
  var percent = (v) => Math.round(v * 1e3) / 10;
829
1101
  function VectorGradientEditor({ controller, className }) {
830
1102
  const state = useVectorEditorState(controller);
@@ -839,23 +1111,23 @@ function VectorGradientEditor({ controller, className }) {
839
1111
  controller.setStyle({ [ed.target]: vectorPaintToHex(first?.color) ?? first?.color ?? "#000000" }, void 0, { label: "Solid Colour" });
840
1112
  } else controller.setGradient({ type: value });
841
1113
  };
842
- return /* @__PURE__ */ jsxs5("div", { className: `space-y-1.5 ${className ?? ""}`, "data-vector-gradient-editor": ed.target, "aria-label": "Gradient", children: [
843
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1.5", children: [
844
- /* @__PURE__ */ jsx5("span", { className: "w-16 text-xs text-foreground", children: "Gradient" }),
845
- ["fill", "stroke"].map((t) => /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-pressed": ed.target === t, onClick: () => controller.setGradientTarget(t), children: t === "fill" ? "Fill" : "Stroke" }, t)),
846
- ed.mixed && /* @__PURE__ */ jsx5("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
1114
+ return /* @__PURE__ */ jsxs6("div", { className: `space-y-1.5 ${className ?? ""}`, "data-vector-gradient-editor": ed.target, "aria-label": "Gradient", children: [
1115
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5", children: [
1116
+ /* @__PURE__ */ jsx6("span", { className: "w-16 text-xs text-foreground", children: "Gradient" }),
1117
+ ["fill", "stroke"].map((t) => /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass4, "aria-pressed": ed.target === t, onClick: () => controller.setGradientTarget(t), children: t === "fill" ? "Fill" : "Stroke" }, t)),
1118
+ ed.mixed && /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
847
1119
  ] }),
848
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-1.5", children: [
849
- /* @__PURE__ */ jsx5("span", { className: "w-16 text-xs text-foreground", children: "Type" }),
850
- /* @__PURE__ */ jsxs5("select", { "aria-label": "Gradient type", className: selectClass3, value: ed.type ?? "solid", onChange: (e) => setType(e.target.value), children: [
851
- /* @__PURE__ */ jsx5("option", { value: "solid", children: "Solid" }),
852
- VECTOR_GRADIENT_TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: VECTOR_GRADIENT_TYPE_LABELS[t] }, t))
1120
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5", children: [
1121
+ /* @__PURE__ */ jsx6("span", { className: "w-16 text-xs text-foreground", children: "Type" }),
1122
+ /* @__PURE__ */ jsxs6("select", { "aria-label": "Gradient type", className: selectClass4, value: ed.type ?? "solid", onChange: (e) => setType(e.target.value), children: [
1123
+ /* @__PURE__ */ jsx6("option", { value: "solid", children: "Solid" }),
1124
+ VECTOR_GRADIENT_TYPES.map((t) => /* @__PURE__ */ jsx6("option", { value: t, children: VECTOR_GRADIENT_TYPE_LABELS[t] }, t))
853
1125
  ] }),
854
- /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
1126
+ /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass4, "aria-label": "Reverse gradient", title: "Reverse gradient", disabled: !g, onClick: () => controller.reverseGradient(), children: "\u21C4" })
855
1127
  ] }),
856
- g && /* @__PURE__ */ jsxs5(Fragment2, { children: [
857
- /* @__PURE__ */ jsxs5("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
858
- /* @__PURE__ */ jsx5(
1128
+ g && /* @__PURE__ */ jsxs6(Fragment2, { children: [
1129
+ /* @__PURE__ */ jsxs6("div", { className: "relative h-8", "data-vector-gradient-ramp": "", children: [
1130
+ /* @__PURE__ */ jsx6(
859
1131
  "div",
860
1132
  {
861
1133
  role: "button",
@@ -869,7 +1141,7 @@ function VectorGradientEditor({ controller, className }) {
869
1141
  }
870
1142
  }
871
1143
  ),
872
- ed.stops.map((s) => /* @__PURE__ */ jsx5(
1144
+ ed.stops.map((s) => /* @__PURE__ */ jsx6(
873
1145
  "button",
874
1146
  {
875
1147
  type: "button",
@@ -883,8 +1155,8 @@ function VectorGradientEditor({ controller, className }) {
883
1155
  s.index
884
1156
  ))
885
1157
  ] }),
886
- sel && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-1.5", "data-vector-gradient-selected-stop": sel.index, children: [
887
- /* @__PURE__ */ jsx5(
1158
+ sel && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-1.5", "data-vector-gradient-selected-stop": sel.index, children: [
1159
+ /* @__PURE__ */ jsx6(
888
1160
  "input",
889
1161
  {
890
1162
  type: "color",
@@ -895,9 +1167,9 @@ function VectorGradientEditor({ controller, className }) {
895
1167
  onBlur: end
896
1168
  }
897
1169
  ),
898
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
1170
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
899
1171
  "Opacity",
900
- /* @__PURE__ */ jsx5(
1172
+ /* @__PURE__ */ jsx6(
901
1173
  "input",
902
1174
  {
903
1175
  type: "number",
@@ -905,7 +1177,7 @@ function VectorGradientEditor({ controller, className }) {
905
1177
  max: 100,
906
1178
  step: 5,
907
1179
  "aria-label": "Stop opacity",
908
- className: numberClass3,
1180
+ className: numberClass4,
909
1181
  value: percent(sel.opacity ?? 1),
910
1182
  onChange: (e) => {
911
1183
  const n = parseFloat(e.target.value);
@@ -914,9 +1186,9 @@ function VectorGradientEditor({ controller, className }) {
914
1186
  }
915
1187
  )
916
1188
  ] }),
917
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
1189
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
918
1190
  "At",
919
- /* @__PURE__ */ jsx5(
1191
+ /* @__PURE__ */ jsx6(
920
1192
  "input",
921
1193
  {
922
1194
  type: "number",
@@ -924,7 +1196,7 @@ function VectorGradientEditor({ controller, className }) {
924
1196
  max: 100,
925
1197
  step: 1,
926
1198
  "aria-label": "Stop position",
927
- className: numberClass3,
1199
+ className: numberClass4,
928
1200
  value: percent(sel.offset),
929
1201
  onChange: (e) => {
930
1202
  const n = parseFloat(e.target.value);
@@ -933,9 +1205,9 @@ function VectorGradientEditor({ controller, className }) {
933
1205
  }
934
1206
  )
935
1207
  ] }),
936
- !sel.last && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
1208
+ !sel.last && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
937
1209
  "Midpoint",
938
- /* @__PURE__ */ jsx5(
1210
+ /* @__PURE__ */ jsx6(
939
1211
  "input",
940
1212
  {
941
1213
  type: "number",
@@ -943,7 +1215,7 @@ function VectorGradientEditor({ controller, className }) {
943
1215
  max: 95,
944
1216
  step: 1,
945
1217
  "aria-label": "Stop midpoint",
946
- className: numberClass3,
1218
+ className: numberClass4,
947
1219
  value: percent(sel.midpoint ?? 0.5),
948
1220
  onChange: (e) => {
949
1221
  const n = parseFloat(e.target.value);
@@ -952,15 +1224,15 @@ function VectorGradientEditor({ controller, className }) {
952
1224
  }
953
1225
  )
954
1226
  ] }),
955
- /* @__PURE__ */ jsx5("button", { type: "button", className: buttonClass3, "aria-label": "Delete stop", title: "Delete stop", disabled: ed.stops.length <= 2, onClick: () => controller.removeGradientStop(sel.index), children: "\u2715" })
1227
+ /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass4, "aria-label": "Delete stop", title: "Delete stop", disabled: ed.stops.length <= 2, onClick: () => controller.removeGradientStop(sel.index), children: "\u2715" })
956
1228
  ] }),
957
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
958
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Colours" }),
959
- /* @__PURE__ */ jsx5("select", { "aria-label": "Gradient interpolation", className: selectClass3, value: ed.interpolation, onChange: (e) => controller.setGradient({ interpolation: e.target.value }), children: VECTOR_GRADIENT_INTERPOLATIONS.map((i) => /* @__PURE__ */ jsx5("option", { value: i, children: VECTOR_GRADIENT_INTERPOLATION_LABELS[i] }, i)) })
1229
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
1230
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Colours" }),
1231
+ /* @__PURE__ */ jsx6("select", { "aria-label": "Gradient interpolation", className: selectClass4, value: ed.interpolation, onChange: (e) => controller.setGradient({ interpolation: e.target.value }), children: VECTOR_GRADIENT_INTERPOLATIONS.map((i) => /* @__PURE__ */ jsx6("option", { value: i, children: VECTOR_GRADIENT_INTERPOLATION_LABELS[i] }, i)) })
960
1232
  ] }),
961
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
962
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Smooth" }),
963
- /* @__PURE__ */ jsx5(
1233
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
1234
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Smooth" }),
1235
+ /* @__PURE__ */ jsx6(
964
1236
  "input",
965
1237
  {
966
1238
  type: "range",
@@ -976,23 +1248,23 @@ function VectorGradientEditor({ controller, className }) {
976
1248
  }
977
1249
  )
978
1250
  ] }),
979
- /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
980
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "Spread" }),
981
- /* @__PURE__ */ jsx5("select", { "aria-label": "Gradient spread", className: selectClass3, value: ed.spread, onChange: (e) => controller.setGradient({ spread: e.target.value }), children: VECTOR_GRADIENT_SPREADS.map((sp) => /* @__PURE__ */ jsx5("option", { value: sp, children: VECTOR_GRADIENT_SPREAD_LABELS[sp] }, sp)) })
1251
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
1252
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Spread" }),
1253
+ /* @__PURE__ */ jsx6("select", { "aria-label": "Gradient spread", className: selectClass4, value: ed.spread, onChange: (e) => controller.setGradient({ spread: e.target.value }), children: VECTOR_GRADIENT_SPREADS.map((sp) => /* @__PURE__ */ jsx6("option", { value: sp, children: VECTOR_GRADIENT_SPREAD_LABELS[sp] }, sp)) })
982
1254
  ] }),
983
- ed.target === "stroke" && /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
984
- /* @__PURE__ */ jsx5("span", { className: "w-16", children: "On stroke" }),
985
- /* @__PURE__ */ jsx5("select", { "aria-label": "Gradient on stroke", className: selectClass3, value: ed.strokeMode, onChange: (e) => controller.setGradient({ strokeMode: e.target.value }), children: VECTOR_STROKE_GRADIENT_MODES.map((m) => /* @__PURE__ */ jsx5("option", { value: m, children: VECTOR_STROKE_GRADIENT_MODE_LABELS[m] }, m)) })
1255
+ ed.target === "stroke" && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1.5 text-xs text-foreground", children: [
1256
+ /* @__PURE__ */ jsx6("span", { className: "w-16", children: "On stroke" }),
1257
+ /* @__PURE__ */ jsx6("select", { "aria-label": "Gradient on stroke", className: selectClass4, value: ed.strokeMode, onChange: (e) => controller.setGradient({ strokeMode: e.target.value }), children: VECTOR_STROKE_GRADIENT_MODES.map((m) => /* @__PURE__ */ jsx6("option", { value: m, children: VECTOR_STROKE_GRADIENT_MODE_LABELS[m] }, m)) })
986
1258
  ] })
987
1259
  ] })
988
1260
  ] });
989
1261
  }
990
1262
 
991
1263
  // src/vector/VectorToolbar.tsx
992
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1264
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
993
1265
  var VECTOR_SNAP_TITLE = "Snap to objects";
994
1266
  var modKeyLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "") ? "\u2318" : "Ctrl";
995
- var buttonClass4 = "inline-flex h-8 min-w-8 items-center justify-center rounded-md border border-border bg-background px-2 text-xs font-medium text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground";
1267
+ var buttonClass5 = "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";
996
1268
  function VectorToolbar({
997
1269
  controller,
998
1270
  tools = VECTOR_TOOLS,
@@ -1011,14 +1283,14 @@ function VectorToolbar({
1011
1283
  const groupSelected = state.selection.some((id) => controller.getNode(id)?.type === "group");
1012
1284
  const [live, setLive] = React4.useState(false);
1013
1285
  const booleanDisabled = live ? count < 1 : count < 2;
1014
- const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass4, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
1015
- return /* @__PURE__ */ jsxs6("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
1016
- /* @__PURE__ */ jsx6("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx6(
1286
+ const act = (label, title, run, disabled = false) => /* @__PURE__ */ jsx7("button", { type: "button", className: buttonClass5, title, "aria-label": title, disabled, onClick: () => void run(), children: label }, label);
1287
+ return /* @__PURE__ */ jsxs7("div", { role: "toolbar", "aria-label": "Vector tools", className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: [
1288
+ /* @__PURE__ */ jsx7("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Tools", children: tools.map((tool) => /* @__PURE__ */ jsx7(
1017
1289
  "button",
1018
1290
  {
1019
1291
  type: "button",
1020
1292
  "data-vector-tool": tool,
1021
- className: buttonClass4,
1293
+ className: buttonClass5,
1022
1294
  "aria-pressed": state.tool === tool,
1023
1295
  title: `${vectorToolLabel(tool)} (${VECTOR_TOOL_SHORTCUTS[tool]})`,
1024
1296
  "aria-label": vectorToolLabel(tool),
@@ -1027,16 +1299,16 @@ function VectorToolbar({
1027
1299
  },
1028
1300
  tool
1029
1301
  )) }),
1030
- showHistory && /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
1302
+ showHistory && /* @__PURE__ */ jsxs7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "History", children: [
1031
1303
  act("\u21B6", state.canUndo ? `Undo ${state.undoLabel}` : "Undo", () => controller.undo(), !state.canUndo),
1032
1304
  act("\u21B7", state.canRedo ? `Redo ${state.redoLabel}` : "Redo", () => controller.redo(), !state.canRedo)
1033
1305
  ] }),
1034
- showSnap && /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx6(
1306
+ showSnap && /* @__PURE__ */ jsx7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Snapping", children: /* @__PURE__ */ jsx7(
1035
1307
  "button",
1036
1308
  {
1037
1309
  type: "button",
1038
1310
  "data-vector-snap": "",
1039
- className: buttonClass4,
1311
+ className: buttonClass5,
1040
1312
  "aria-pressed": state.snap.objects,
1041
1313
  title: `${VECTOR_SNAP_TITLE} (hold ${modKeyLabel()} to bypass)`,
1042
1314
  "aria-label": VECTOR_SNAP_TITLE,
@@ -1044,13 +1316,13 @@ function VectorToolbar({
1044
1316
  children: "Snap"
1045
1317
  }
1046
1318
  ) }),
1047
- showPathEdit && /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
1048
- VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx6(
1319
+ showPathEdit && /* @__PURE__ */ jsxs7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Path editing", children: [
1320
+ VECTOR_PATH_ACTIONS.map((a) => /* @__PURE__ */ jsx7(
1049
1321
  "button",
1050
1322
  {
1051
1323
  type: "button",
1052
1324
  "data-vector-path-action": a.id,
1053
- className: buttonClass4,
1325
+ className: buttonClass5,
1054
1326
  title: a.shortcut ? `${a.title} (${modKeyLabel()}+${a.shortcut})` : a.title,
1055
1327
  "aria-label": a.title,
1056
1328
  disabled: !a.enabled(count, points),
@@ -1059,12 +1331,12 @@ function VectorToolbar({
1059
1331
  },
1060
1332
  a.id
1061
1333
  )),
1062
- VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx6(
1334
+ VECTOR_EDIT_TOGGLES.map((t) => /* @__PURE__ */ jsx7(
1063
1335
  "button",
1064
1336
  {
1065
1337
  type: "button",
1066
1338
  "data-vector-edit": t.key,
1067
- className: buttonClass4,
1339
+ className: buttonClass5,
1068
1340
  "aria-pressed": editing[t.key],
1069
1341
  title: t.title,
1070
1342
  "aria-label": t.title,
@@ -1073,9 +1345,9 @@ function VectorToolbar({
1073
1345
  },
1074
1346
  t.key
1075
1347
  )),
1076
- state.tool === "pencil" && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1348
+ state.tool === "pencil" && /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1077
1349
  "Smoothing",
1078
- /* @__PURE__ */ jsx6(
1350
+ /* @__PURE__ */ jsx7(
1079
1351
  "input",
1080
1352
  {
1081
1353
  type: "range",
@@ -1089,9 +1361,9 @@ function VectorToolbar({
1089
1361
  }
1090
1362
  )
1091
1363
  ] }),
1092
- state.tool === "eraser" && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1364
+ state.tool === "eraser" && /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-1 text-xs text-foreground", children: [
1093
1365
  "Size",
1094
- /* @__PURE__ */ jsx6(
1366
+ /* @__PURE__ */ jsx7(
1095
1367
  "input",
1096
1368
  {
1097
1369
  type: "number",
@@ -1107,12 +1379,12 @@ function VectorToolbar({
1107
1379
  }
1108
1380
  )
1109
1381
  ] }),
1110
- state.tool === "paintBucket" && /* @__PURE__ */ jsx6(
1382
+ state.tool === "paintBucket" && /* @__PURE__ */ jsx7(
1111
1383
  "button",
1112
1384
  {
1113
1385
  type: "button",
1114
1386
  "data-vector-fill-gap": "",
1115
- className: buttonClass4,
1387
+ className: buttonClass5,
1116
1388
  "aria-pressed": editing.fillGap > 0,
1117
1389
  title: "Close small gaps between path ends when filling",
1118
1390
  "aria-label": "Close gaps",
@@ -1121,12 +1393,12 @@ function VectorToolbar({
1121
1393
  }
1122
1394
  )
1123
1395
  ] }),
1124
- showDistort && /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Distort", children: /* @__PURE__ */ jsx6(
1396
+ showDistort && /* @__PURE__ */ jsx7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Distort", children: /* @__PURE__ */ jsx7(
1125
1397
  "button",
1126
1398
  {
1127
1399
  type: "button",
1128
1400
  "data-vector-free-distort": "",
1129
- className: buttonClass4,
1401
+ className: buttonClass5,
1130
1402
  "aria-pressed": state.freeDistort,
1131
1403
  title: `Free Distort: drag a corner of the selection box to distort in perspective (Shift+Alt+${modKeyLabel()}: symmetric perspective; ${modKeyLabel()}-drag a side to skew)`,
1132
1404
  "aria-label": "Free Distort",
@@ -1134,15 +1406,15 @@ function VectorToolbar({
1134
1406
  children: "Distort"
1135
1407
  }
1136
1408
  ) }),
1137
- showBlend && /* @__PURE__ */ jsx6(VectorBlendControls, { controller }),
1138
- showArrange && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1139
- /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
1140
- /* @__PURE__ */ jsx6(
1409
+ showBlend && /* @__PURE__ */ jsx7(VectorBlendControls, { controller }),
1410
+ showArrange && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1411
+ /* @__PURE__ */ jsxs7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Combine", children: [
1412
+ /* @__PURE__ */ jsx7(
1141
1413
  "button",
1142
1414
  {
1143
1415
  type: "button",
1144
1416
  "data-vector-live-boolean": "",
1145
- className: buttonClass4,
1417
+ className: buttonClass5,
1146
1418
  "aria-pressed": live,
1147
1419
  title: "Live booleans: keep the shapes editable",
1148
1420
  "aria-label": "Live booleans",
@@ -1157,12 +1429,12 @@ function VectorToolbar({
1157
1429
  act("G", "Group", () => controller.group(), count < 1),
1158
1430
  act("U", "Ungroup", () => controller.ungroup(), !groupSelected)
1159
1431
  ] }),
1160
- /* @__PURE__ */ jsx6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx6(
1432
+ /* @__PURE__ */ jsx7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Pathfinder", children: VECTOR_PATHFINDER_ACTIONS.map((a) => /* @__PURE__ */ jsx7(
1161
1433
  "button",
1162
1434
  {
1163
1435
  type: "button",
1164
1436
  "data-vector-pathfinder": a.id,
1165
- className: buttonClass4,
1437
+ className: buttonClass5,
1166
1438
  title: a.title,
1167
1439
  "aria-label": a.title,
1168
1440
  disabled: count < a.min,
@@ -1171,7 +1443,7 @@ function VectorToolbar({
1171
1443
  },
1172
1444
  a.id
1173
1445
  )) }),
1174
- /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
1446
+ /* @__PURE__ */ jsxs7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Arrange", children: [
1175
1447
  act("\u21C8", "Bring to Front", () => controller.arrange("front"), count < 1),
1176
1448
  act("\u21CA", "Send to Back", () => controller.arrange("back"), count < 1),
1177
1449
  act("\u21E4", "Align Left", () => controller.align("left"), count < 1),
@@ -1189,19 +1461,19 @@ function VectorBlendControls({ controller }) {
1189
1461
  const blend = getVectorBlendControls(state);
1190
1462
  const o = blend.options;
1191
1463
  const selected = blend.ids.length > 0;
1192
- return /* @__PURE__ */ jsxs6("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
1193
- /* @__PURE__ */ jsx6("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass4, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
1194
- /* @__PURE__ */ jsx6(
1464
+ return /* @__PURE__ */ jsxs7("div", { className: "ml-1 flex items-center gap-1", role: "group", "aria-label": "Blend", "data-vector-blend-controls": "", children: [
1465
+ /* @__PURE__ */ jsx7("button", { type: "button", "data-vector-blend-action": "make", className: buttonClass5, title: "Make Blend", "aria-label": "Make Blend", disabled: !blend.canBlend, onClick: () => controller.blend(), children: "\u29D3" }),
1466
+ /* @__PURE__ */ jsx7(
1195
1467
  "select",
1196
1468
  {
1197
1469
  "aria-label": "Blend spacing",
1198
1470
  className: "h-7 rounded border border-border bg-background px-1 text-xs",
1199
1471
  value: o.spacing,
1200
1472
  onChange: (e) => applyVectorBlendOptions(controller, { spacing: e.target.value }),
1201
- children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx6("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
1473
+ children: VECTOR_BLEND_SPACINGS.map((sp) => /* @__PURE__ */ jsx7("option", { value: sp, children: VECTOR_BLEND_SPACING_LABELS[sp] }, sp))
1202
1474
  }
1203
1475
  ),
1204
- o.spacing !== "smoothColor" && /* @__PURE__ */ jsx6(
1476
+ o.spacing !== "smoothColor" && /* @__PURE__ */ jsx7(
1205
1477
  "input",
1206
1478
  {
1207
1479
  type: "number",
@@ -1217,12 +1489,12 @@ function VectorBlendControls({ controller }) {
1217
1489
  }
1218
1490
  }
1219
1491
  ),
1220
- /* @__PURE__ */ jsx6(
1492
+ /* @__PURE__ */ jsx7(
1221
1493
  "button",
1222
1494
  {
1223
1495
  type: "button",
1224
1496
  "data-vector-blend-action": "orient",
1225
- className: buttonClass4,
1497
+ className: buttonClass5,
1226
1498
  "aria-pressed": o.orientToSpine,
1227
1499
  title: "Orient steps to the spine",
1228
1500
  "aria-label": "Orient to spine",
@@ -1230,12 +1502,12 @@ function VectorBlendControls({ controller }) {
1230
1502
  children: "\u293F"
1231
1503
  }
1232
1504
  ),
1233
- /* @__PURE__ */ jsx6(
1505
+ /* @__PURE__ */ jsx7(
1234
1506
  "button",
1235
1507
  {
1236
1508
  type: "button",
1237
1509
  "data-vector-blend-action": "spine",
1238
- className: buttonClass4,
1510
+ className: buttonClass5,
1239
1511
  title: "Replace Spine (select a blend and a path)",
1240
1512
  "aria-label": "Replace Spine",
1241
1513
  disabled: !blend.canReplaceSpine,
@@ -1243,12 +1515,12 @@ function VectorBlendControls({ controller }) {
1243
1515
  children: "\u2307"
1244
1516
  }
1245
1517
  ),
1246
- /* @__PURE__ */ jsx6("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass4, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
1247
- /* @__PURE__ */ jsx6("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass4, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
1518
+ /* @__PURE__ */ jsx7("button", { type: "button", "data-vector-blend-action": "expand", className: buttonClass5, title: "Expand Blend", "aria-label": "Expand Blend", disabled: !selected, onClick: () => controller.expandBlend(), children: "\u29C9" }),
1519
+ /* @__PURE__ */ jsx7("button", { type: "button", "data-vector-blend-action": "release", className: buttonClass5, title: "Release Blend", "aria-label": "Release Blend", disabled: !selected, onClick: () => controller.releaseBlend(), children: "\u29C4" })
1248
1520
  ] });
1249
1521
  }
1250
- var selectClass4 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
1251
- var numberClass4 = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
1522
+ var selectClass5 = "h-7 min-w-0 flex-1 rounded border border-border bg-background px-1 text-xs";
1523
+ var numberClass5 = "h-7 w-16 rounded border border-border bg-background px-1.5 text-xs";
1252
1524
  function VectorStylePanel({ controller, className }) {
1253
1525
  const state = useVectorEditorState(controller);
1254
1526
  const style = getVectorSelectionStyle(state);
@@ -1279,17 +1551,17 @@ function VectorStylePanel({ controller, className }) {
1279
1551
  if (style) controller.setWidthProfile(id);
1280
1552
  else controller.setDefaults({ strokeWidthProfile: widthProfilePreset(id)?.length ? widthProfilePreset(id) : null });
1281
1553
  };
1282
- const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
1554
+ const mixed = (key) => style?.mixed[key] ? /* @__PURE__ */ jsx7("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" }) : null;
1283
1555
  const arrowRow = (end, label) => {
1284
1556
  const key = end === "start" ? "startArrow" : "endArrow";
1285
1557
  const scaleKey = end === "start" ? "startArrowScale" : "endArrowScale";
1286
- return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1287
- /* @__PURE__ */ jsx6("span", { className: "w-16 text-xs text-foreground", children: label }),
1288
- /* @__PURE__ */ jsxs6("select", { "aria-label": `${label} arrowhead`, className: selectClass4, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
1289
- /* @__PURE__ */ jsx6("option", { value: "", children: "None" }),
1290
- VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx6("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
1558
+ return /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1559
+ /* @__PURE__ */ jsx7("span", { className: "w-16 text-xs text-foreground", children: label }),
1560
+ /* @__PURE__ */ jsxs7("select", { "aria-label": `${label} arrowhead`, className: selectClass5, value: source[key] ?? "", onChange: (e) => apply({ [key]: e.target.value || null }), children: [
1561
+ /* @__PURE__ */ jsx7("option", { value: "", children: "None" }),
1562
+ VECTOR_ARROWHEADS.map((h) => /* @__PURE__ */ jsx7("option", { value: h, children: VECTOR_ARROWHEAD_LABELS[h] }, h))
1291
1563
  ] }),
1292
- /* @__PURE__ */ jsx6(
1564
+ /* @__PURE__ */ jsx7(
1293
1565
  "input",
1294
1566
  {
1295
1567
  type: "number",
@@ -1297,7 +1569,7 @@ function VectorStylePanel({ controller, className }) {
1297
1569
  step: 0.25,
1298
1570
  "aria-label": `${label} arrowhead scale`,
1299
1571
  title: "Scale (\xD7 stroke width)",
1300
- className: numberClass4,
1572
+ className: numberClass5,
1301
1573
  value: source[scaleKey],
1302
1574
  onChange: (e) => {
1303
1575
  const n = parseFloat(e.target.value);
@@ -1315,12 +1587,12 @@ function VectorStylePanel({ controller, className }) {
1315
1587
  const paint = source[key];
1316
1588
  const hex = vectorPaintToHex2(paint);
1317
1589
  const on = paint !== null && paint !== "none";
1318
- return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1319
- /* @__PURE__ */ jsxs6("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
1320
- /* @__PURE__ */ jsx6("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
1590
+ return /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", "data-vector-style": key, children: [
1591
+ /* @__PURE__ */ jsxs7("label", { className: "flex w-16 items-center gap-1.5 text-xs text-foreground", children: [
1592
+ /* @__PURE__ */ jsx7("input", { type: "checkbox", checked: on, "aria-label": `${label} on`, onChange: (e) => apply({ [key]: e.target.checked ? hex ?? "#000000" : null }) }),
1321
1593
  label
1322
1594
  ] }),
1323
- /* @__PURE__ */ jsx6(
1595
+ /* @__PURE__ */ jsx7(
1324
1596
  "input",
1325
1597
  {
1326
1598
  type: "color",
@@ -1331,18 +1603,18 @@ function VectorStylePanel({ controller, className }) {
1331
1603
  onChange: (e) => apply({ [key]: e.target.value })
1332
1604
  }
1333
1605
  ),
1334
- on && !hex && /* @__PURE__ */ jsx6("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
1335
- style?.mixed[key] && /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
1606
+ on && !hex && /* @__PURE__ */ jsx7("span", { className: "truncate text-[11px] text-muted-foreground", children: typeof paint === "string" ? paint : "Gradient" }),
1607
+ style?.mixed[key] && /* @__PURE__ */ jsx7("span", { className: "text-[11px] text-muted-foreground", children: "Mixed" })
1336
1608
  ] });
1337
1609
  };
1338
- return /* @__PURE__ */ jsxs6("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
1339
- /* @__PURE__ */ jsx6("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
1610
+ return /* @__PURE__ */ jsxs7("div", { className: `space-y-2 ${className ?? ""}`, "aria-label": style ? "Selection style" : "New shape style", children: [
1611
+ /* @__PURE__ */ jsx7("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: style ? "Selection" : "New shapes" }),
1340
1612
  paintRow("fill", "Fill"),
1341
1613
  paintRow("stroke", "Stroke"),
1342
- style && /* @__PURE__ */ jsx6(VectorGradientEditor, { controller }),
1343
- /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1344
- /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Width" }),
1345
- /* @__PURE__ */ jsx6(
1614
+ style && /* @__PURE__ */ jsx7(VectorGradientEditor, { controller }),
1615
+ /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1616
+ /* @__PURE__ */ jsx7("span", { className: "w-16", children: "Width" }),
1617
+ /* @__PURE__ */ jsx7(
1346
1618
  "input",
1347
1619
  {
1348
1620
  type: "number",
@@ -1358,33 +1630,33 @@ function VectorStylePanel({ controller, className }) {
1358
1630
  }
1359
1631
  )
1360
1632
  ] }),
1361
- /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
1362
- /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Align" }),
1363
- /* @__PURE__ */ jsx6("select", { "aria-label": "Stroke alignment", className: selectClass4, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS).map((a) => /* @__PURE__ */ jsx6("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS[a] }, a)) }),
1633
+ /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "strokeAlign", children: [
1634
+ /* @__PURE__ */ jsx7("span", { className: "w-16", children: "Align" }),
1635
+ /* @__PURE__ */ jsx7("select", { "aria-label": "Stroke alignment", className: selectClass5, value: source.strokeAlign, onChange: (e) => apply({ strokeAlign: e.target.value }), children: Object.keys(VECTOR_STROKE_ALIGN_LABELS2).map((a) => /* @__PURE__ */ jsx7("option", { value: a, children: VECTOR_STROKE_ALIGN_LABELS2[a] }, a)) }),
1364
1636
  mixed("strokeAlign")
1365
1637
  ] }),
1366
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
1367
- /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Profile" }),
1368
- /* @__PURE__ */ jsxs6(
1638
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "widthProfile", children: [
1639
+ /* @__PURE__ */ jsx7("span", { className: "w-16", children: "Profile" }),
1640
+ /* @__PURE__ */ jsxs7(
1369
1641
  "select",
1370
1642
  {
1371
1643
  "aria-label": "Width profile",
1372
- className: selectClass4,
1644
+ className: selectClass5,
1373
1645
  value: source.widthProfile,
1374
1646
  onChange: (e) => {
1375
1647
  if (e.target.value !== "custom") setProfile(e.target.value);
1376
1648
  },
1377
1649
  children: [
1378
- VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx6("option", { value: p.id, children: p.label }, p.id)),
1379
- source.widthProfile === "custom" && /* @__PURE__ */ jsx6("option", { value: "custom", disabled: true, children: "Custom" })
1650
+ VECTOR_WIDTH_PRESETS.map((p) => /* @__PURE__ */ jsx7("option", { value: p.id, children: p.label }, p.id)),
1651
+ source.widthProfile === "custom" && /* @__PURE__ */ jsx7("option", { value: "custom", disabled: true, children: "Custom" })
1380
1652
  ]
1381
1653
  }
1382
1654
  ),
1383
- /* @__PURE__ */ jsx6(
1655
+ /* @__PURE__ */ jsx7(
1384
1656
  "button",
1385
1657
  {
1386
1658
  type: "button",
1387
- className: buttonClass4,
1659
+ className: buttonClass5,
1388
1660
  title: "Reset to uniform width",
1389
1661
  "aria-label": "Reset to uniform width",
1390
1662
  disabled: source.widthProfile === "uniform" && !style?.mixed.widthProfile,
@@ -1394,27 +1666,27 @@ function VectorStylePanel({ controller, className }) {
1394
1666
  ),
1395
1667
  mixed("widthProfile")
1396
1668
  ] }),
1397
- source.hasOpenPath && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1669
+ source.hasOpenPath && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1398
1670
  arrowRow("start", "Start"),
1399
1671
  arrowRow("end", "End"),
1400
- (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
1401
- /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Heads" }),
1402
- /* @__PURE__ */ jsxs6("select", { "aria-label": "Arrowhead alignment", className: selectClass4, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
1403
- /* @__PURE__ */ jsx6("option", { value: "extend", children: "Tip past end" }),
1404
- /* @__PURE__ */ jsx6("option", { value: "tip", children: "Tip at end" })
1672
+ (source.startArrow || source.endArrow) && /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2 text-xs text-foreground", "data-vector-style": "arrowAlign", children: [
1673
+ /* @__PURE__ */ jsx7("span", { className: "w-16", children: "Heads" }),
1674
+ /* @__PURE__ */ jsxs7("select", { "aria-label": "Arrowhead alignment", className: selectClass5, value: source.arrowAlign, onChange: (e) => apply({ arrowAlign: e.target.value }), children: [
1675
+ /* @__PURE__ */ jsx7("option", { value: "extend", children: "Tip past end" }),
1676
+ /* @__PURE__ */ jsx7("option", { value: "tip", children: "Tip at end" })
1405
1677
  ] })
1406
1678
  ] })
1407
1679
  ] }),
1408
- source.corners > 0 && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
1409
- /* @__PURE__ */ jsx6("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
1410
- /* @__PURE__ */ jsx6(
1680
+ source.corners > 0 && /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", "data-vector-style": "corners", children: [
1681
+ /* @__PURE__ */ jsx7("span", { className: "w-16 text-xs text-foreground", children: "Corners" }),
1682
+ /* @__PURE__ */ jsx7(
1411
1683
  "input",
1412
1684
  {
1413
1685
  type: "number",
1414
1686
  min: 0,
1415
1687
  step: 1,
1416
1688
  "aria-label": "Corner radius",
1417
- className: numberClass4,
1689
+ className: numberClass5,
1418
1690
  value: Math.round(source.cornerRadius * 100) / 100,
1419
1691
  onChange: (e) => {
1420
1692
  const n = parseFloat(e.target.value);
@@ -1422,12 +1694,12 @@ function VectorStylePanel({ controller, className }) {
1422
1694
  }
1423
1695
  }
1424
1696
  ),
1425
- /* @__PURE__ */ jsx6("select", { "aria-label": "Corner kind", className: selectClass4, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx6("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
1697
+ /* @__PURE__ */ jsx7("select", { "aria-label": "Corner kind", className: selectClass5, value: source.cornerKind, onChange: (e) => setCorners({ kind: e.target.value }), children: VECTOR_CORNER_KINDS.map((k) => /* @__PURE__ */ jsx7("option", { value: k, children: VECTOR_CORNER_KIND_LABELS[k] }, k)) }),
1426
1698
  mixed("cornerRadius")
1427
1699
  ] }),
1428
- style && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1429
- /* @__PURE__ */ jsx6("span", { className: "w-16", children: "Opacity" }),
1430
- /* @__PURE__ */ jsx6(
1700
+ style && /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2 text-xs text-foreground", children: [
1701
+ /* @__PURE__ */ jsx7("span", { className: "w-16", children: "Opacity" }),
1702
+ /* @__PURE__ */ jsx7(
1431
1703
  "input",
1432
1704
  {
1433
1705
  type: "range",
@@ -1441,27 +1713,30 @@ function VectorStylePanel({ controller, className }) {
1441
1713
  }
1442
1714
  )
1443
1715
  ] }),
1444
- style && /* @__PURE__ */ jsx6(VectorEffectsPanel, { controller }),
1445
- style && /* @__PURE__ */ jsx6(VectorEnvelopePanel, { controller })
1716
+ style && /* @__PURE__ */ jsx7(VectorAppearancePanel, { controller }),
1717
+ style && /* @__PURE__ */ jsx7(VectorEffectsPanel, { controller }),
1718
+ style && /* @__PURE__ */ jsx7(VectorEnvelopePanel, { controller })
1446
1719
  ] });
1447
1720
  }
1448
1721
 
1449
1722
  // src/vector/VectorEditor.tsx
1450
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1723
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1451
1724
  function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
1452
- return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
1453
- /* @__PURE__ */ jsx7(VectorToolbar, { controller, tools }),
1454
- /* @__PURE__ */ jsxs7("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
1455
- /* @__PURE__ */ jsx7("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx7(VectorCanvas, { controller, ...canvasProps }) }),
1456
- showStylePanel && /* @__PURE__ */ jsx7(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
1725
+ return /* @__PURE__ */ jsxs8("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
1726
+ /* @__PURE__ */ jsx8(VectorToolbar, { controller, tools }),
1727
+ /* @__PURE__ */ jsxs8("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
1728
+ /* @__PURE__ */ jsx8("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ jsx8(VectorCanvas, { controller, ...canvasProps }) }),
1729
+ showStylePanel && /* @__PURE__ */ jsx8(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
1457
1730
  ] })
1458
1731
  ] });
1459
1732
  }
1460
1733
  export {
1734
+ VectorAppearancePanel,
1461
1735
  VectorCanvas,
1462
1736
  VectorEditor,
1463
1737
  VectorEffectsPanel,
1464
1738
  VectorEnvelopePanel,
1739
+ VectorFilterDefs,
1465
1740
  VectorGradientAnnotator,
1466
1741
  VectorGradientDefs,
1467
1742
  VectorGradientEditor,