@pixerate/editor-react 0.78.1 → 0.80.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.
Files changed (38) hide show
  1. package/dist/animation-editor/index.cjs +679 -37
  2. package/dist/animation-editor/index.cjs.map +1 -1
  3. package/dist/animation-editor/index.d.cts +29 -3
  4. package/dist/animation-editor/index.d.ts +29 -3
  5. package/dist/animation-editor/index.js +128 -37
  6. package/dist/animation-editor/index.js.map +1 -1
  7. package/dist/{chunk-S5SSL3ZO.js → chunk-VKWTKESA.js} +5 -5
  8. package/dist/{chunk-7RMNM3KC.js → chunk-WOQ3H7KA.js} +4 -4
  9. package/dist/chunk-WOQ3H7KA.js.map +1 -0
  10. package/dist/effects/index.cjs +3 -3
  11. package/dist/effects/index.cjs.map +1 -1
  12. package/dist/effects/index.d.cts +4 -2
  13. package/dist/effects/index.d.ts +4 -2
  14. package/dist/effects/index.js +1 -1
  15. package/dist/image-editor/index.cjs +3 -3
  16. package/dist/image-editor/index.cjs.map +1 -1
  17. package/dist/image-editor/index.js +3 -3
  18. package/dist/index.cjs +3 -3
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +3 -3
  21. package/dist/paint-editor/index.cjs +734 -19
  22. package/dist/paint-editor/index.cjs.map +1 -1
  23. package/dist/paint-editor/index.d.cts +22 -3
  24. package/dist/paint-editor/index.d.ts +22 -3
  25. package/dist/paint-editor/index.js +173 -19
  26. package/dist/paint-editor/index.js.map +1 -1
  27. package/dist/vector/index.cjs +649 -9
  28. package/dist/vector/index.cjs.map +1 -1
  29. package/dist/vector/index.d.cts +27 -3
  30. package/dist/vector/index.d.ts +27 -3
  31. package/dist/vector/index.js +98 -9
  32. package/dist/vector/index.js.map +1 -1
  33. package/dist/video-editor/index.cjs +3 -3
  34. package/dist/video-editor/index.cjs.map +1 -1
  35. package/dist/video-editor/index.js +1 -1
  36. package/package.json +3 -3
  37. package/dist/chunk-7RMNM3KC.js.map +0 -1
  38. /package/dist/{chunk-S5SSL3ZO.js.map → chunk-VKWTKESA.js.map} +0 -0
@@ -44,6 +44,7 @@ __export(vector_exports, {
44
44
  VectorGradientEditor: () => VectorGradientEditor,
45
45
  VectorMaskPanel: () => VectorMaskPanel,
46
46
  VectorMaskedNodes: () => VectorMaskedNodes,
47
+ VectorProviderEffectsPanel: () => VectorProviderEffectsPanel,
47
48
  VectorSceneNodes: () => VectorSceneNodes,
48
49
  VectorShadingPanel: () => VectorShadingPanel,
49
50
  VectorStylePanel: () => VectorStylePanel,
@@ -57,7 +58,8 @@ __export(vector_exports, {
57
58
  useVectorCommands: () => useVectorCommands,
58
59
  useVectorEditor: () => useVectorEditor,
59
60
  useVectorEditorController: () => useVectorEditorController,
60
- useVectorEditorState: () => useVectorEditorState
61
+ useVectorEditorState: () => useVectorEditorState,
62
+ useVectorEffectsTools: () => useVectorEffectsTools
61
63
  });
62
64
  module.exports = __toCommonJS(vector_exports);
63
65
 
@@ -102,6 +104,13 @@ function useVectorCommands(registry, controller, options = {}) {
102
104
  return registry.register((0, import_vector.createVectorCommands)(controller, { prefix, group, scope }));
103
105
  }, [registry, controller, prefix, group, scope]);
104
106
  }
107
+ function useVectorEffectsTools(registry, controller, options = {}) {
108
+ const { id, namespace, readOnly } = options;
109
+ (0, import_react.useEffect)(() => {
110
+ if (!registry) return;
111
+ return registry.register((0, import_vector.createVectorEffectsToolProvider)(controller, { id, namespace, readOnly }));
112
+ }, [registry, controller, id, namespace, readOnly]);
113
+ }
105
114
 
106
115
  // src/vector/VectorCanvas.tsx
107
116
  var import_react3 = __toESM(require("react"), 1);
@@ -2750,14 +2759,643 @@ function VectorStylePanel({ controller, className }) {
2750
2759
  ] });
2751
2760
  }
2752
2761
 
2753
- // src/vector/VectorEditor.tsx
2762
+ // src/effects/useEffects.ts
2763
+ var import_react11 = require("react");
2764
+ function useEffects(host, provider) {
2765
+ (0, import_react11.useEffect)(() => {
2766
+ if (!host || !provider) return;
2767
+ return host.useEffects(provider);
2768
+ }, [host, provider]);
2769
+ }
2770
+ function useEffectDescription(provider, effect) {
2771
+ const [state, setState] = (0, import_react11.useState)({ key: "", description: null, error: null });
2772
+ const key = provider && effect ? `${provider.id}:${effect}` : "";
2773
+ (0, import_react11.useEffect)(() => {
2774
+ if (!provider || !effect) return;
2775
+ let live = true;
2776
+ Promise.resolve().then(() => provider.describe(effect)).then(
2777
+ (description) => live && setState({ key, description, error: null }),
2778
+ (e) => live && setState({ key, description: null, error: e instanceof Error ? e.message : String(e) })
2779
+ );
2780
+ return () => {
2781
+ live = false;
2782
+ };
2783
+ }, [provider, effect, key]);
2784
+ return state.key === key ? { description: state.description, error: state.error } : { description: null, error: null };
2785
+ }
2786
+
2787
+ // src/effects/EffectThumbnail.tsx
2788
+ var import_react12 = require("react");
2789
+ var import_effects = require("@pixerate/editor/effects");
2754
2790
  var import_jsx_runtime17 = require("react/jsx-runtime");
2755
- function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, className }) {
2756
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
2757
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VectorToolbar, { controller, tools }),
2758
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
2759
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VectorCanvas, { controller, ...canvasProps }) }),
2760
- showStylePanel && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(VectorStylePanel, { controller, className: "w-full shrink-0 rounded-md border border-border p-3 md:w-56" })
2791
+ function EffectThumbnail({ provider, effect, width = 96, height = 54, className = "" }) {
2792
+ const ref = (0, import_react12.useRef)(null);
2793
+ const [failed, setFailed] = (0, import_react12.useState)(effect.kind === "motion");
2794
+ (0, import_react12.useEffect)(() => {
2795
+ if (effect.kind === "motion") return;
2796
+ let live = true;
2797
+ (0, import_effects.renderEffectThumbnail)(provider, effect, { width, height }).then(
2798
+ (pixels) => {
2799
+ const ctx = live ? ref.current?.getContext("2d") : null;
2800
+ if (!ctx || typeof ImageData === "undefined") return;
2801
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), 0, 0);
2802
+ },
2803
+ () => live && setFailed(true)
2804
+ );
2805
+ return () => {
2806
+ live = false;
2807
+ };
2808
+ }, [provider, effect, width, height]);
2809
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2810
+ "div",
2811
+ {
2812
+ className: `relative overflow-hidden rounded border border-border bg-[repeating-conic-gradient(#8884_0_25%,transparent_0_50%)] bg-[length:12px_12px] ${className}`,
2813
+ style: { aspectRatio: `${width} / ${height}` },
2814
+ "data-effect-thumbnail": effect.id,
2815
+ children: failed ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "absolute inset-0 flex items-center justify-center text-[10px] text-muted-foreground", children: effect.kind === "motion" ? "Motion" : "No preview" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("canvas", { ref, width, height, className: "h-full w-full", "aria-hidden": "true" })
2816
+ }
2817
+ );
2818
+ }
2819
+
2820
+ // src/effects/EffectsBrowser.tsx
2821
+ var import_react13 = require("react");
2822
+ var import_effects2 = require("@pixerate/editor/effects");
2823
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2824
+ function EffectsBrowser({ provider, uses, kinds, onPick, thumbnails = true, title = "Effects", className = "" }) {
2825
+ const [all, setAll] = (0, import_react13.useState)([]);
2826
+ const [query, setQuery] = (0, import_react13.useState)("");
2827
+ const [category, setCategory] = (0, import_react13.useState)("");
2828
+ const [use, setUse] = (0, import_react13.useState)("");
2829
+ const [error, setError] = (0, import_react13.useState)(null);
2830
+ (0, import_react13.useEffect)(() => {
2831
+ if (!provider) return setAll([]);
2832
+ let live = true;
2833
+ Promise.resolve().then(() => provider.list()).then(
2834
+ (list) => live && (setAll(list), setError(null)),
2835
+ (e) => live && setError(e instanceof Error ? e.message : String(e))
2836
+ );
2837
+ return () => {
2838
+ live = false;
2839
+ };
2840
+ }, [provider]);
2841
+ const [found, setFound] = (0, import_react13.useState)([]);
2842
+ (0, import_react13.useEffect)(() => {
2843
+ if (!provider) return setFound([]);
2844
+ let live = true;
2845
+ Promise.resolve().then(() => provider.search(query, { ...category ? { category } : {}, ...use ? { use } : {} })).then((list) => live && setFound(list), () => live && setFound([]));
2846
+ return () => {
2847
+ live = false;
2848
+ };
2849
+ }, [provider, query, category, use]);
2850
+ const allowed = (e) => (!kinds || kinds.includes(e.kind)) && (uses ? e.kind !== "motion" || uses.includes("motion") : e.kind !== "motion") && (!uses || e.uses.some((u) => uses.includes(u)));
2851
+ const categories = (0, import_react13.useMemo)(() => [...new Set(all.filter(allowed).map((e) => e.category))], [all, uses, kinds]);
2852
+ const shown = found.filter(allowed);
2853
+ if (!provider)
2854
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: `rounded-md border border-dashed border-border p-3 text-xs text-muted-foreground ${className}`, "data-effects-browser": "", "data-effects-unavailable": "", children: "No effects provider is registered, so effects cannot be added. Effects already in the document are kept." });
2855
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: `flex flex-col gap-2 ${className}`, "data-effects-browser": "", "aria-label": "Effects browser", children: [
2856
+ title && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { className: "text-xs font-semibold", children: title }),
2857
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
2858
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2859
+ "input",
2860
+ {
2861
+ type: "search",
2862
+ className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-0.5 text-xs",
2863
+ placeholder: "Search effects",
2864
+ "aria-label": "Search effects",
2865
+ value: query,
2866
+ onChange: (e) => setQuery(e.currentTarget.value)
2867
+ }
2868
+ ),
2869
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", "aria-label": "Category", value: category, onChange: (e) => setCategory(e.currentTarget.value), children: [
2870
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: "", children: "All categories" }),
2871
+ categories.map((c) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: c, children: c }, c))
2872
+ ] }),
2873
+ uses && uses.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", "aria-label": "Use", value: use, onChange: (e) => setUse(e.currentTarget.value), children: [
2874
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: "", children: "Any use" }),
2875
+ uses.map((u) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: u, children: import_effects2.EFFECT_USE_LABELS[u] }, u))
2876
+ ] })
2877
+ ] }),
2878
+ error && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: error }),
2879
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ul", { className: "grid max-h-96 grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] gap-1.5 overflow-y-auto", role: "list", children: shown.map((e) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
2880
+ "button",
2881
+ {
2882
+ type: "button",
2883
+ className: "flex w-full flex-col gap-1 rounded border border-transparent p-1 text-left hover:border-border focus-visible:border-border",
2884
+ title: e.description ?? e.name,
2885
+ "data-effect-id": e.id,
2886
+ onClick: () => onPick(e),
2887
+ children: [
2888
+ thumbnails && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(EffectThumbnail, { provider, effect: e }),
2889
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-[11px] font-medium", children: e.name }),
2890
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-[10px] text-muted-foreground", children: e.uses.map((u) => import_effects2.EFFECT_USE_LABELS[u]).join(" \xB7 ") })
2891
+ ]
2892
+ }
2893
+ ) }, e.id)) }),
2894
+ !shown.length && !error && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects match." })
2895
+ ] });
2896
+ }
2897
+
2898
+ // src/effects/EffectParameters.tsx
2899
+ var import_react15 = __toESM(require("react"), 1);
2900
+ var import_effects3 = require("@pixerate/editor/effects");
2901
+ var import_parameters2 = require("@pixerate/editor/parameters");
2902
+
2903
+ // src/parameters/fields.tsx
2904
+ var import_react14 = require("react");
2905
+ var import_parameters = require("@pixerate/editor/parameters");
2906
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2907
+ var parameterInputClass = "h-7 w-full min-w-0 rounded-md border border-input bg-background px-2 text-xs text-foreground tabular-nums focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50";
2908
+ var parameterButtonClass = "inline-flex h-7 items-center justify-center gap-1 rounded-md border border-input bg-background px-2 text-xs text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
2909
+ var rangeClass = "h-4 w-full min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50";
2910
+ function ParameterCommitInput({ value, onCommit, multiline, className, onKeyDown, ...rest }) {
2911
+ const commit = (el) => {
2912
+ if (el.value !== value) onCommit(el.value, el);
2913
+ };
2914
+ const keyDown = (e) => {
2915
+ onKeyDown?.(e);
2916
+ if (e.defaultPrevented) return;
2917
+ if (e.key === "Enter" && (!multiline || e.metaKey || e.ctrlKey)) {
2918
+ e.preventDefault();
2919
+ commit(e.currentTarget);
2920
+ }
2921
+ if (e.key === "Escape") e.currentTarget.value = value;
2922
+ };
2923
+ const props = {
2924
+ defaultValue: value,
2925
+ className: className ?? parameterInputClass,
2926
+ onBlur: (e) => commit(e.currentTarget),
2927
+ onKeyDown: keyDown,
2928
+ ...rest
2929
+ };
2930
+ return multiline ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("textarea", { ...props, className: `${props.className} h-auto min-h-[3.5rem] py-1` }, value) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("input", { type: "text", ...props }, value);
2931
+ }
2932
+ var plainNumber = (field, value) => (0, import_parameters.formatParameterNumber)({ ...field, unit: void 0 }, value);
2933
+ function ParameterNumberInput({
2934
+ field,
2935
+ value,
2936
+ onChange,
2937
+ disabled,
2938
+ id,
2939
+ "aria-label": ariaLabel
2940
+ }) {
2941
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "relative min-w-0 flex-1", children: [
2942
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2943
+ ParameterCommitInput,
2944
+ {
2945
+ id,
2946
+ inputMode: "decimal",
2947
+ "aria-label": ariaLabel ?? (0, import_parameters.parameterLabel)(field),
2948
+ value: plainNumber(field, value),
2949
+ disabled: disabled || field.readOnly,
2950
+ className: `${parameterInputClass} ${field.unit ? "pr-6" : ""}`,
2951
+ onCommit: (text, input) => {
2952
+ const n = (0, import_parameters.parseParameterNumberInput)(field, text);
2953
+ if (n === null || n === value) input.value = plainNumber(field, value);
2954
+ else onChange(n);
2955
+ },
2956
+ onKeyDown: (e) => {
2957
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
2958
+ e.preventDefault();
2959
+ const step = (0, import_parameters.parameterStep)(field) * (e.shiftKey ? 10 : 1);
2960
+ onChange((0, import_parameters.clampParameterNumber)(field, value + (e.key === "ArrowUp" ? step : -step)));
2961
+ }
2962
+ }
2963
+ ),
2964
+ field.unit && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground", children: field.unit })
2965
+ ] });
2966
+ }
2967
+ function ParameterSlider(props) {
2968
+ const { field, value, onChange, onGestureStart, onGestureEnd, disabled } = props;
2969
+ const dragging = (0, import_react14.useRef)(false);
2970
+ const end = () => {
2971
+ if (!dragging.current) return;
2972
+ dragging.current = false;
2973
+ onGestureEnd?.();
2974
+ };
2975
+ if (!(0, import_parameters.isSliderField)(field)) return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterNumberInput, { ...props });
2976
+ const step = (0, import_parameters.parameterStep)(field);
2977
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
2978
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2979
+ "input",
2980
+ {
2981
+ type: "range",
2982
+ "aria-label": (0, import_parameters.parameterLabel)(field),
2983
+ "aria-valuetext": (0, import_parameters.formatParameterNumber)(field, value),
2984
+ min: field.min,
2985
+ max: field.max,
2986
+ step,
2987
+ value,
2988
+ disabled: disabled || field.readOnly,
2989
+ className: rangeClass,
2990
+ onPointerDown: () => {
2991
+ if (dragging.current) return;
2992
+ dragging.current = true;
2993
+ onGestureStart?.();
2994
+ },
2995
+ onPointerUp: end,
2996
+ onPointerCancel: end,
2997
+ onBlur: end,
2998
+ onChange: (e) => onChange((0, import_parameters.clampParameterNumber)(field, Number(e.target.value)))
2999
+ }
3000
+ ),
3001
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterNumberInput, { ...props, id: props.id, "aria-label": `${(0, import_parameters.parameterLabel)(field)} value` }) })
3002
+ ] });
3003
+ }
3004
+ function ParameterToggle({ field, value, onChange, disabled, id }) {
3005
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3006
+ "button",
3007
+ {
3008
+ type: "button",
3009
+ role: "switch",
3010
+ id,
3011
+ "aria-checked": value,
3012
+ "aria-label": (0, import_parameters.parameterLabel)(field),
3013
+ disabled: disabled || field.readOnly,
3014
+ onClick: () => onChange(!value),
3015
+ className: `relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50 ${value ? "bg-primary" : "bg-muted"}`,
3016
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3017
+ "span",
3018
+ {
3019
+ className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow transition-transform ${value ? "translate-x-4" : "translate-x-0.5"}`
3020
+ }
3021
+ )
3022
+ }
3023
+ );
3024
+ }
3025
+ function ParameterSelect({ field, value, onChange, disabled, id }) {
3026
+ const options = (0, import_parameters.getChoiceOptions)(field);
3027
+ const off = disabled || field.readOnly;
3028
+ if (field.control === "segmented") {
3029
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { role: "radiogroup", "aria-label": (0, import_parameters.parameterLabel)(field), className: "inline-flex flex-wrap rounded-md border border-input bg-muted p-0.5", children: options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3030
+ "button",
3031
+ {
3032
+ type: "button",
3033
+ role: "radio",
3034
+ "aria-checked": o.value === value,
3035
+ title: o.description,
3036
+ disabled: off,
3037
+ onClick: () => onChange(o.value),
3038
+ className: `rounded px-2 py-0.5 text-xs disabled:opacity-50 ${o.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
3039
+ children: (0, import_parameters.choiceOptionLabel)(o)
3040
+ },
3041
+ String(o.value)
3042
+ )) });
3043
+ }
3044
+ const index = options.findIndex((o) => o.value === value);
3045
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3046
+ "select",
3047
+ {
3048
+ id,
3049
+ "aria-label": (0, import_parameters.parameterLabel)(field),
3050
+ value: String(index),
3051
+ disabled: off,
3052
+ onChange: (e) => {
3053
+ const option = options[Number(e.target.value)];
3054
+ if (option) onChange(option.value);
3055
+ },
3056
+ className: parameterInputClass,
3057
+ children: options.map((o, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("option", { value: String(i), title: o.description, children: (0, import_parameters.choiceOptionLabel)(o) }, String(o.value)))
3058
+ }
3059
+ );
3060
+ }
3061
+ function ParameterColor({ field, value, onChange, onGestureStart, onGestureEnd, disabled, id }) {
3062
+ const picking = (0, import_react14.useRef)(false);
3063
+ const alpha = !!field.alpha;
3064
+ const hex = (0, import_parameters.normalizeHexColor)(value, true) ?? "#000000ff";
3065
+ const rgb = hex.slice(0, 7);
3066
+ const a = hex.slice(7);
3067
+ const off = disabled || field.readOnly;
3068
+ const start = () => {
3069
+ if (picking.current) return;
3070
+ picking.current = true;
3071
+ onGestureStart?.();
3072
+ };
3073
+ const end = () => {
3074
+ if (!picking.current) return;
3075
+ picking.current = false;
3076
+ onGestureEnd?.();
3077
+ };
3078
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-1.5", children: [
3079
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
3080
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "relative h-7 w-9 shrink-0 overflow-hidden rounded-md border border-input bg-[repeating-conic-gradient(#d4d4d8_0_25%,#fff_0_50%)] bg-[length:8px_8px]", children: [
3081
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "absolute inset-0", style: { background: alpha ? hex : rgb } }),
3082
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3083
+ "input",
3084
+ {
3085
+ type: "color",
3086
+ "aria-label": (0, import_parameters.parameterLabel)(field),
3087
+ value: rgb,
3088
+ disabled: off,
3089
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed",
3090
+ onChange: (e) => {
3091
+ start();
3092
+ onChange(alpha ? `${e.target.value}${a}` : e.target.value);
3093
+ },
3094
+ onBlur: end
3095
+ }
3096
+ )
3097
+ ] }),
3098
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3099
+ ParameterCommitInput,
3100
+ {
3101
+ id,
3102
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} hex`,
3103
+ value: alpha ? hex : rgb,
3104
+ disabled: off,
3105
+ spellCheck: false,
3106
+ className: `${parameterInputClass} font-mono`,
3107
+ onCommit: (text) => {
3108
+ const next = (0, import_parameters.normalizeHexColor)(text, alpha);
3109
+ if (next) onChange(next);
3110
+ }
3111
+ }
3112
+ )
3113
+ ] }),
3114
+ alpha && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3115
+ "input",
3116
+ {
3117
+ type: "range",
3118
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} opacity`,
3119
+ min: 0,
3120
+ max: 255,
3121
+ step: 1,
3122
+ value: parseInt(a, 16),
3123
+ disabled: off,
3124
+ className: rangeClass,
3125
+ onPointerDown: start,
3126
+ onPointerUp: end,
3127
+ onPointerCancel: end,
3128
+ onBlur: end,
3129
+ onChange: (e) => onChange(`${rgb}${Number(e.target.value).toString(16).padStart(2, "0")}`)
3130
+ }
3131
+ ),
3132
+ field.swatches && field.swatches.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex flex-wrap gap-1", children: field.swatches.map((s) => {
3133
+ const swatch = (0, import_parameters.normalizeHexColor)(s, alpha);
3134
+ if (!swatch) return null;
3135
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3136
+ "button",
3137
+ {
3138
+ type: "button",
3139
+ title: swatch,
3140
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} ${swatch}`,
3141
+ "aria-pressed": swatch === (alpha ? hex : rgb),
3142
+ disabled: off,
3143
+ onClick: () => onChange(swatch),
3144
+ className: "h-5 w-5 rounded border border-input aria-pressed:ring-1 aria-pressed:ring-primary",
3145
+ style: { background: swatch }
3146
+ },
3147
+ s
3148
+ );
3149
+ }) })
3150
+ ] });
3151
+ }
3152
+ function ParameterVector({ field, value, onChange, disabled, id }) {
3153
+ const size = (0, import_parameters.vectorSize)(field);
3154
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))` }, children: Array.from({ length: size }, (_, i) => {
3155
+ const component = (0, import_parameters.vectorComponentField)(field, i);
3156
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("label", { className: "flex min-w-0 items-center gap-1", children: [
3157
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-3 shrink-0 text-[10px] uppercase text-muted-foreground", children: component.label }),
3158
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3159
+ ParameterNumberInput,
3160
+ {
3161
+ field: component,
3162
+ value: value[i] ?? 0,
3163
+ id: i === 0 ? id : void 0,
3164
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} ${component.label}`,
3165
+ disabled: disabled || field.readOnly,
3166
+ onChange: (n) => onChange(Array.from({ length: size }, (_2, j) => j === i ? n : value[j] ?? 0))
3167
+ }
3168
+ )
3169
+ ] }, i);
3170
+ }) });
3171
+ }
3172
+ function ParameterImage({ field, value, onChange, disabled, id, resolveFile }) {
3173
+ const off = disabled || field.readOnly;
3174
+ const name = (0, import_parameters.parameterLabel)(field);
3175
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-1.5", children: [
3176
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
3177
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex h-12 w-16 shrink-0 items-center justify-center overflow-hidden rounded-md border border-input bg-muted", children: value ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("img", { src: value, alt: name, className: "h-full w-full object-cover" }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "None" }) }),
3178
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
3179
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3180
+ ParameterCommitInput,
3181
+ {
3182
+ id,
3183
+ "aria-label": `${name} URL`,
3184
+ placeholder: "Image URL",
3185
+ value: value.startsWith("data:") ? "" : value,
3186
+ disabled: off,
3187
+ onCommit: (text) => onChange(text.trim())
3188
+ }
3189
+ ),
3190
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex gap-1", children: [
3191
+ resolveFile && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("label", { className: `${parameterButtonClass} cursor-pointer ${off ? "pointer-events-none opacity-50" : ""}`, children: [
3192
+ "Choose file\u2026",
3193
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3194
+ "input",
3195
+ {
3196
+ type: "file",
3197
+ className: "sr-only",
3198
+ accept: field.accept ?? "image/*",
3199
+ disabled: off,
3200
+ "aria-label": `${name} file`,
3201
+ onChange: async (e) => {
3202
+ const file = e.target.files?.[0];
3203
+ e.target.value = "";
3204
+ if (file) onChange(await resolveFile(file));
3205
+ }
3206
+ }
3207
+ )
3208
+ ] }),
3209
+ value && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("button", { type: "button", className: parameterButtonClass, disabled: off, onClick: () => onChange(""), "aria-label": `Clear ${name}`, children: "Clear" })
3210
+ ] })
3211
+ ] })
3212
+ ] }),
3213
+ field.options && field.options.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex flex-wrap gap-1", children: field.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3214
+ "button",
3215
+ {
3216
+ type: "button",
3217
+ title: o.label ?? o.url,
3218
+ "aria-label": o.label ?? o.url,
3219
+ "aria-pressed": o.url === value,
3220
+ disabled: off,
3221
+ onClick: () => onChange(o.url),
3222
+ className: "h-9 w-12 overflow-hidden rounded border border-input aria-pressed:ring-2 aria-pressed:ring-primary",
3223
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("img", { src: o.url, alt: "", className: "h-full w-full object-cover" })
3224
+ },
3225
+ o.url
3226
+ )) })
3227
+ ] });
3228
+ }
3229
+ function ParameterText({ field, value, onChange, disabled, id }) {
3230
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3231
+ ParameterCommitInput,
3232
+ {
3233
+ id,
3234
+ "aria-label": (0, import_parameters.parameterLabel)(field),
3235
+ multiline: field.multiline,
3236
+ placeholder: field.placeholder,
3237
+ maxLength: field.maxLength,
3238
+ value,
3239
+ disabled: disabled || field.readOnly,
3240
+ onCommit: onChange
3241
+ }
3242
+ );
3243
+ }
3244
+ function ParameterFieldControl({ field, value, onChange, resolveFile, ...rest }) {
3245
+ const set = onChange;
3246
+ switch (field.type) {
3247
+ case "number":
3248
+ case "int":
3249
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterSlider, { field, value, onChange: set, ...rest });
3250
+ case "boolean":
3251
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterToggle, { field, value, onChange: set, ...rest });
3252
+ case "choice":
3253
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterSelect, { field, value, onChange: set, ...rest });
3254
+ case "color":
3255
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterColor, { field, value, onChange: set, ...rest });
3256
+ case "vector":
3257
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterVector, { field, value, onChange: set, ...rest });
3258
+ case "image":
3259
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterImage, { field, value, onChange: set, resolveFile, ...rest });
3260
+ case "text":
3261
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ParameterText, { field, value, onChange: set, ...rest });
3262
+ }
3263
+ }
3264
+
3265
+ // src/effects/EffectParameters.tsx
3266
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3267
+ function EffectParameters({ description, values, onChange, onReset, keyframed, onPreset, disabled = false, className = "" }) {
3268
+ const fields = (0, import_react15.useMemo)(() => (0, import_effects3.effectParameterFields)(description), [description]);
3269
+ const idBase = (0, import_react15.useId)();
3270
+ const gesture = import_react15.default.useRef(null);
3271
+ const count = import_react15.default.useRef(0);
3272
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: `space-y-2 ${className}`, "data-effect-parameters": description.id, children: [
3273
+ onPreset && description.presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3274
+ "select",
3275
+ {
3276
+ className: "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs",
3277
+ "aria-label": `${description.name} presets`,
3278
+ value: "",
3279
+ disabled,
3280
+ onChange: (e) => {
3281
+ const preset = description.presets.find((p) => p.name === e.currentTarget.value);
3282
+ if (preset) onPreset({ ...description.defaults, ...preset.values });
3283
+ },
3284
+ children: [
3285
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("option", { value: "", children: "Presets\u2026" }),
3286
+ description.presets.map((p) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("option", { value: p.name, children: p.name }, p.name))
3287
+ ]
3288
+ }
3289
+ ),
3290
+ fields.map((field) => {
3291
+ const label = (0, import_parameters2.parameterLabel)(field);
3292
+ const inputId = `${idBase}-${field.id}`;
3293
+ const keyed = keyframed?.has(field.id) ?? false;
3294
+ const value = (0, import_effects3.effectFieldValue)(field, values[field.id]);
3295
+ const changed = !keyed && !(0, import_parameters2.parameterValuesEqual)(value, field.default);
3296
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "space-y-1", "data-effect-control": field.id, children: [
3297
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-1.5", children: [
3298
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { htmlFor: inputId, className: "min-w-0 flex-1 truncate text-xs font-medium", title: field.description, children: label }),
3299
+ keyed && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "keyframed" }),
3300
+ onReset && changed && !disabled && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { type: "button", className: "rounded px-1 text-[11px] text-muted-foreground hover:text-foreground", "aria-label": `Reset ${label}`, onClick: () => onReset(field.id), children: "\u21BA" })
3301
+ ] }),
3302
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3303
+ ParameterFieldControl,
3304
+ {
3305
+ field,
3306
+ value,
3307
+ id: inputId,
3308
+ disabled: disabled || keyed,
3309
+ onChange: (v) => onChange(field.id, v, gesture.current),
3310
+ onGestureStart: () => gesture.current = `${idBase}:${field.id}:${++count.current}`,
3311
+ onGestureEnd: () => gesture.current = null
3312
+ }
3313
+ )
3314
+ ] }, field.id);
3315
+ }),
3316
+ !fields.length && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No controls." })
3317
+ ] });
3318
+ }
3319
+
3320
+ // src/vector/VectorProviderEffectsPanel.tsx
3321
+ var import_react16 = require("react");
3322
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3323
+ var btn = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
3324
+ function Controls({ controller, group }) {
3325
+ const provider = controller.effectsProvider;
3326
+ const tag = group.providerEffect;
3327
+ const { description, error } = useEffectDescription(provider, tag.effect);
3328
+ const [message, setMessage] = (0, import_react16.useState)(null);
3329
+ const run = (work) => void work.then(() => setMessage(null), (e) => setMessage(e instanceof Error ? e.message : String(e)));
3330
+ if (!provider) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider: the shapes are kept as they are." });
3331
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-[11px] text-amber-500", children: error });
3332
+ if (!description) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "Loading\u2026" });
3333
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
3334
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3335
+ EffectParameters,
3336
+ {
3337
+ description,
3338
+ values: tag.values,
3339
+ onChange: (k, v) => run(controller.updateVectorEffect(group.id, { [k]: v })),
3340
+ onReset: (k) => run(controller.updateVectorEffect(group.id, { [k]: null })),
3341
+ onPreset: (v) => run(controller.updateVectorEffect(group.id, v))
3342
+ }
3343
+ ),
3344
+ message && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
3345
+ ] });
3346
+ }
3347
+ function VectorProviderEffectsPanel({ controller, className = "" }) {
3348
+ const state = useVectorEditorState(controller);
3349
+ const provider = (0, import_react16.useSyncExternalStore)(
3350
+ (cb) => controller.effects.subscribe(cb),
3351
+ () => controller.effects.provider,
3352
+ () => controller.effects.provider
3353
+ );
3354
+ const [browse, setBrowse] = (0, import_react16.useState)(false);
3355
+ const [message, setMessage] = (0, import_react16.useState)(null);
3356
+ void state;
3357
+ const groups = controller.vectorEffectGroups();
3358
+ const selected = groups.filter((g) => state.selection.includes(g.id));
3359
+ const run = (work) => {
3360
+ setMessage(null);
3361
+ Promise.resolve().then(work).catch((e) => setMessage(e instanceof Error ? e.message : String(e)));
3362
+ };
3363
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { className: `flex flex-col gap-2 text-xs ${className}`, "aria-label": "Vector effects", "data-vector-provider-effects": "", children: [
3364
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
3365
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "flex-1 font-semibold", children: "Effects" }),
3366
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("button", { type: "button", className: btn, disabled: !provider, "aria-pressed": browse, onClick: () => setBrowse(!browse), children: "Add effect\u2026" })
3367
+ ] }),
3368
+ !provider && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider is registered. Effect groups are kept as shapes." }),
3369
+ browse && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EffectsBrowser, { provider, kinds: ["vector"], title: "Vector effects", onPick: (e) => (setBrowse(false), run(() => controller.addVectorEffect(e.id))) }),
3370
+ selected.map((g) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "space-y-1 rounded border border-border p-1", "data-vector-effect-group": g.id, children: [
3371
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
3372
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "min-w-0 flex-1 truncate font-medium", children: g.providerEffect.name ?? g.providerEffect.effect }),
3373
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("button", { type: "button", className: btn, title: "Keep the shapes as a plain group", onClick: () => run(() => controller.detachVectorEffect(g.id)), children: "Detach" })
3374
+ ] }),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Controls, { controller, group: g })
3376
+ ] }, g.id)),
3377
+ groups.length > 0 && !selected.length && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: "text-[11px] text-muted-foreground", children: [
3378
+ "Select an effect group (",
3379
+ groups.length,
3380
+ " in the drawing) to change it."
3381
+ ] }),
3382
+ message && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
3383
+ ] });
3384
+ }
3385
+
3386
+ // src/vector/VectorEditor.tsx
3387
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3388
+ function VectorEditor({ controller, tools, showStylePanel = true, height = 520, canvasProps, effects, className }) {
3389
+ useEffects(controller, effects);
3390
+ const effectsShown = !!effects || controller.vectorEffectGroups().length > 0;
3391
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-vector-editor": "", children: [
3392
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VectorToolbar, { controller, tools }),
3393
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
3394
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "relative min-h-[320px] flex-1 overflow-hidden rounded-md border border-border bg-muted/40", style: { height }, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VectorCanvas, { controller, ...canvasProps }) }),
3395
+ showStylePanel && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex w-full shrink-0 flex-col gap-2 md:w-56", children: [
3396
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VectorStylePanel, { controller, className: "rounded-md border border-border p-3" }),
3397
+ effectsShown && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(VectorProviderEffectsPanel, { controller, className: "rounded-md border border-border p-3" })
3398
+ ] })
2761
3399
  ] })
2762
3400
  ] });
2763
3401
  }
@@ -2777,6 +3415,7 @@ function VectorEditor({ controller, tools, showStylePanel = true, height = 520,
2777
3415
  VectorGradientEditor,
2778
3416
  VectorMaskPanel,
2779
3417
  VectorMaskedNodes,
3418
+ VectorProviderEffectsPanel,
2780
3419
  VectorSceneNodes,
2781
3420
  VectorShadingPanel,
2782
3421
  VectorStylePanel,
@@ -2790,6 +3429,7 @@ function VectorEditor({ controller, tools, showStylePanel = true, height = 520,
2790
3429
  useVectorCommands,
2791
3430
  useVectorEditor,
2792
3431
  useVectorEditorController,
2793
- useVectorEditorState
3432
+ useVectorEditorState,
3433
+ useVectorEffectsTools
2794
3434
  });
2795
3435
  //# sourceMappingURL=index.cjs.map