@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
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var animation_editor_exports = {};
32
32
  __export(animation_editor_exports, {
33
33
  AnimationEditor: () => AnimationEditor,
34
+ AnimationEffectsPanel: () => AnimationEffectsPanel,
34
35
  AnimationStage: () => AnimationStage,
35
36
  CommandInspector: () => CommandInspector,
36
37
  CommandList: () => CommandList,
@@ -42,7 +43,8 @@ __export(animation_editor_exports, {
42
43
  useAnimationEditor: () => useAnimationEditor,
43
44
  useAnimationEditorController: () => useAnimationEditorController,
44
45
  useAnimationEditorState: () => useAnimationEditorState,
45
- useAnimationEditorTools: () => useAnimationEditorTools
46
+ useAnimationEditorTools: () => useAnimationEditorTools,
47
+ useAnimationEffectsTools: () => useAnimationEffectsTools
46
48
  });
47
49
  module.exports = __toCommonJS(animation_editor_exports);
48
50
 
@@ -90,6 +92,13 @@ function useAnimationEditorTools(registry, controller, options = {}) {
90
92
  return registry.register((0, import_animation_editor.createAnimationEditorToolProvider)(controller, { id, namespace, readOnly, rasterize }));
91
93
  }, [registry, controller, id, namespace, readOnly, rasterize]);
92
94
  }
95
+ function useAnimationEffectsTools(registry, controller, options = {}) {
96
+ const { id, namespace, readOnly } = options;
97
+ (0, import_react.useEffect)(() => {
98
+ if (!registry) return;
99
+ return registry.register((0, import_animation_editor.createAnimationEffectsToolProvider)(controller, { id, namespace, readOnly }));
100
+ }, [registry, controller, id, namespace, readOnly]);
101
+ }
93
102
 
94
103
  // src/animation-editor/AnimationStage.tsx
95
104
  var import_react2 = require("react");
@@ -191,8 +200,8 @@ function ShotTimeline({ controller, className = "" }) {
191
200
  }
192
201
 
193
202
  // src/animation-editor/AnimationEditor.tsx
194
- var import_react9 = require("react");
195
- var import_animation_editor9 = require("@pixerate/editor/animation-editor");
203
+ var import_react15 = require("react");
204
+ var import_animation_editor10 = require("@pixerate/editor/animation-editor");
196
205
 
197
206
  // src/animation-editor/CommandInspector.tsx
198
207
  var import_react4 = __toESM(require("react"), 1);
@@ -966,19 +975,649 @@ function ShotInspector({ controller, shot, className = "" }) {
966
975
  ] });
967
976
  }
968
977
 
969
- // src/animation-editor/AnimationEditor.tsx
978
+ // src/effects/useEffects.ts
979
+ var import_react9 = require("react");
980
+ function useEffects(host, provider) {
981
+ (0, import_react9.useEffect)(() => {
982
+ if (!host || !provider) return;
983
+ return host.useEffects(provider);
984
+ }, [host, provider]);
985
+ }
986
+ function useEffectDescription(provider, effect) {
987
+ const [state, setState] = (0, import_react9.useState)({ key: "", description: null, error: null });
988
+ const key = provider && effect ? `${provider.id}:${effect}` : "";
989
+ (0, import_react9.useEffect)(() => {
990
+ if (!provider || !effect) return;
991
+ let live = true;
992
+ Promise.resolve().then(() => provider.describe(effect)).then(
993
+ (description) => live && setState({ key, description, error: null }),
994
+ (e) => live && setState({ key, description: null, error: e instanceof Error ? e.message : String(e) })
995
+ );
996
+ return () => {
997
+ live = false;
998
+ };
999
+ }, [provider, effect, key]);
1000
+ return state.key === key ? { description: state.description, error: state.error } : { description: null, error: null };
1001
+ }
1002
+
1003
+ // src/effects/EffectThumbnail.tsx
1004
+ var import_react10 = require("react");
1005
+ var import_effects = require("@pixerate/editor/effects");
970
1006
  var import_jsx_runtime10 = require("react/jsx-runtime");
1007
+ function EffectThumbnail({ provider, effect, width = 96, height = 54, className = "" }) {
1008
+ const ref = (0, import_react10.useRef)(null);
1009
+ const [failed, setFailed] = (0, import_react10.useState)(effect.kind === "motion");
1010
+ (0, import_react10.useEffect)(() => {
1011
+ if (effect.kind === "motion") return;
1012
+ let live = true;
1013
+ (0, import_effects.renderEffectThumbnail)(provider, effect, { width, height }).then(
1014
+ (pixels) => {
1015
+ const ctx = live ? ref.current?.getContext("2d") : null;
1016
+ if (!ctx || typeof ImageData === "undefined") return;
1017
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), 0, 0);
1018
+ },
1019
+ () => live && setFailed(true)
1020
+ );
1021
+ return () => {
1022
+ live = false;
1023
+ };
1024
+ }, [provider, effect, width, height]);
1025
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1026
+ "div",
1027
+ {
1028
+ className: `relative overflow-hidden rounded border border-border bg-[repeating-conic-gradient(#8884_0_25%,transparent_0_50%)] bg-[length:12px_12px] ${className}`,
1029
+ style: { aspectRatio: `${width} / ${height}` },
1030
+ "data-effect-thumbnail": effect.id,
1031
+ children: failed ? /* @__PURE__ */ (0, import_jsx_runtime10.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_runtime10.jsx)("canvas", { ref, width, height, className: "h-full w-full", "aria-hidden": "true" })
1032
+ }
1033
+ );
1034
+ }
1035
+
1036
+ // src/effects/EffectsBrowser.tsx
1037
+ var import_react11 = require("react");
1038
+ var import_effects2 = require("@pixerate/editor/effects");
1039
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1040
+ function EffectsBrowser({ provider, uses, kinds, onPick, thumbnails = true, title = "Effects", className = "" }) {
1041
+ const [all, setAll] = (0, import_react11.useState)([]);
1042
+ const [query, setQuery] = (0, import_react11.useState)("");
1043
+ const [category, setCategory] = (0, import_react11.useState)("");
1044
+ const [use, setUse] = (0, import_react11.useState)("");
1045
+ const [error, setError] = (0, import_react11.useState)(null);
1046
+ (0, import_react11.useEffect)(() => {
1047
+ if (!provider) return setAll([]);
1048
+ let live = true;
1049
+ Promise.resolve().then(() => provider.list()).then(
1050
+ (list) => live && (setAll(list), setError(null)),
1051
+ (e) => live && setError(e instanceof Error ? e.message : String(e))
1052
+ );
1053
+ return () => {
1054
+ live = false;
1055
+ };
1056
+ }, [provider]);
1057
+ const [found, setFound] = (0, import_react11.useState)([]);
1058
+ (0, import_react11.useEffect)(() => {
1059
+ if (!provider) return setFound([]);
1060
+ let live = true;
1061
+ Promise.resolve().then(() => provider.search(query, { ...category ? { category } : {}, ...use ? { use } : {} })).then((list) => live && setFound(list), () => live && setFound([]));
1062
+ return () => {
1063
+ live = false;
1064
+ };
1065
+ }, [provider, query, category, use]);
1066
+ 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)));
1067
+ const categories = (0, import_react11.useMemo)(() => [...new Set(all.filter(allowed).map((e) => e.category))], [all, uses, kinds]);
1068
+ const shown = found.filter(allowed);
1069
+ if (!provider)
1070
+ return /* @__PURE__ */ (0, import_jsx_runtime11.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." });
1071
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { className: `flex flex-col gap-2 ${className}`, "data-effects-browser": "", "aria-label": "Effects browser", children: [
1072
+ title && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-xs font-semibold", children: title }),
1073
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1074
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1075
+ "input",
1076
+ {
1077
+ type: "search",
1078
+ className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-0.5 text-xs",
1079
+ placeholder: "Search effects",
1080
+ "aria-label": "Search effects",
1081
+ value: query,
1082
+ onChange: (e) => setQuery(e.currentTarget.value)
1083
+ }
1084
+ ),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime11.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: [
1086
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: "", children: "All categories" }),
1087
+ categories.map((c) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: c, children: c }, c))
1088
+ ] }),
1089
+ uses && uses.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime11.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: [
1090
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: "", children: "Any use" }),
1091
+ uses.map((u) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: u, children: import_effects2.EFFECT_USE_LABELS[u] }, u))
1092
+ ] })
1093
+ ] }),
1094
+ error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: error }),
1095
+ /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1096
+ "button",
1097
+ {
1098
+ type: "button",
1099
+ className: "flex w-full flex-col gap-1 rounded border border-transparent p-1 text-left hover:border-border focus-visible:border-border",
1100
+ title: e.description ?? e.name,
1101
+ "data-effect-id": e.id,
1102
+ onClick: () => onPick(e),
1103
+ children: [
1104
+ thumbnails && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(EffectThumbnail, { provider, effect: e }),
1105
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate text-[11px] font-medium", children: e.name }),
1106
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate text-[10px] text-muted-foreground", children: e.uses.map((u) => import_effects2.EFFECT_USE_LABELS[u]).join(" \xB7 ") })
1107
+ ]
1108
+ }
1109
+ ) }, e.id)) }),
1110
+ !shown.length && !error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects match." })
1111
+ ] });
1112
+ }
1113
+
1114
+ // src/effects/EffectParameters.tsx
1115
+ var import_react13 = __toESM(require("react"), 1);
1116
+ var import_effects3 = require("@pixerate/editor/effects");
1117
+ var import_parameters2 = require("@pixerate/editor/parameters");
1118
+
1119
+ // src/parameters/fields.tsx
1120
+ var import_react12 = require("react");
1121
+ var import_parameters = require("@pixerate/editor/parameters");
1122
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1123
+ 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";
1124
+ 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";
1125
+ var rangeClass = "h-4 w-full min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50";
1126
+ function ParameterCommitInput({ value, onCommit, multiline, className, onKeyDown, ...rest }) {
1127
+ const commit = (el) => {
1128
+ if (el.value !== value) onCommit(el.value, el);
1129
+ };
1130
+ const keyDown = (e) => {
1131
+ onKeyDown?.(e);
1132
+ if (e.defaultPrevented) return;
1133
+ if (e.key === "Enter" && (!multiline || e.metaKey || e.ctrlKey)) {
1134
+ e.preventDefault();
1135
+ commit(e.currentTarget);
1136
+ }
1137
+ if (e.key === "Escape") e.currentTarget.value = value;
1138
+ };
1139
+ const props = {
1140
+ defaultValue: value,
1141
+ className: className ?? parameterInputClass,
1142
+ onBlur: (e) => commit(e.currentTarget),
1143
+ onKeyDown: keyDown,
1144
+ ...rest
1145
+ };
1146
+ return multiline ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("textarea", { ...props, className: `${props.className} h-auto min-h-[3.5rem] py-1` }, value) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("input", { type: "text", ...props }, value);
1147
+ }
1148
+ var plainNumber = (field, value) => (0, import_parameters.formatParameterNumber)({ ...field, unit: void 0 }, value);
1149
+ function ParameterNumberInput({
1150
+ field,
1151
+ value,
1152
+ onChange,
1153
+ disabled,
1154
+ id,
1155
+ "aria-label": ariaLabel
1156
+ }) {
1157
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "relative min-w-0 flex-1", children: [
1158
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1159
+ ParameterCommitInput,
1160
+ {
1161
+ id,
1162
+ inputMode: "decimal",
1163
+ "aria-label": ariaLabel ?? (0, import_parameters.parameterLabel)(field),
1164
+ value: plainNumber(field, value),
1165
+ disabled: disabled || field.readOnly,
1166
+ className: `${parameterInputClass} ${field.unit ? "pr-6" : ""}`,
1167
+ onCommit: (text, input4) => {
1168
+ const n = (0, import_parameters.parseParameterNumberInput)(field, text);
1169
+ if (n === null || n === value) input4.value = plainNumber(field, value);
1170
+ else onChange(n);
1171
+ },
1172
+ onKeyDown: (e) => {
1173
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
1174
+ e.preventDefault();
1175
+ const step = (0, import_parameters.parameterStep)(field) * (e.shiftKey ? 10 : 1);
1176
+ onChange((0, import_parameters.clampParameterNumber)(field, value + (e.key === "ArrowUp" ? step : -step)));
1177
+ }
1178
+ }
1179
+ ),
1180
+ field.unit && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground", children: field.unit })
1181
+ ] });
1182
+ }
1183
+ function ParameterSlider(props) {
1184
+ const { field, value, onChange, onGestureStart, onGestureEnd, disabled } = props;
1185
+ const dragging = (0, import_react12.useRef)(false);
1186
+ const end = () => {
1187
+ if (!dragging.current) return;
1188
+ dragging.current = false;
1189
+ onGestureEnd?.();
1190
+ };
1191
+ if (!(0, import_parameters.isSliderField)(field)) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterNumberInput, { ...props });
1192
+ const step = (0, import_parameters.parameterStep)(field);
1193
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2", children: [
1194
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1195
+ "input",
1196
+ {
1197
+ type: "range",
1198
+ "aria-label": (0, import_parameters.parameterLabel)(field),
1199
+ "aria-valuetext": (0, import_parameters.formatParameterNumber)(field, value),
1200
+ min: field.min,
1201
+ max: field.max,
1202
+ step,
1203
+ value,
1204
+ disabled: disabled || field.readOnly,
1205
+ className: rangeClass,
1206
+ onPointerDown: () => {
1207
+ if (dragging.current) return;
1208
+ dragging.current = true;
1209
+ onGestureStart?.();
1210
+ },
1211
+ onPointerUp: end,
1212
+ onPointerCancel: end,
1213
+ onBlur: end,
1214
+ onChange: (e) => onChange((0, import_parameters.clampParameterNumber)(field, Number(e.target.value)))
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterNumberInput, { ...props, id: props.id, "aria-label": `${(0, import_parameters.parameterLabel)(field)} value` }) })
1218
+ ] });
1219
+ }
1220
+ function ParameterToggle({ field, value, onChange, disabled, id }) {
1221
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1222
+ "button",
1223
+ {
1224
+ type: "button",
1225
+ role: "switch",
1226
+ id,
1227
+ "aria-checked": value,
1228
+ "aria-label": (0, import_parameters.parameterLabel)(field),
1229
+ disabled: disabled || field.readOnly,
1230
+ onClick: () => onChange(!value),
1231
+ 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"}`,
1232
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1233
+ "span",
1234
+ {
1235
+ className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow transition-transform ${value ? "translate-x-4" : "translate-x-0.5"}`
1236
+ }
1237
+ )
1238
+ }
1239
+ );
1240
+ }
1241
+ function ParameterSelect({ field, value, onChange, disabled, id }) {
1242
+ const options = (0, import_parameters.getChoiceOptions)(field);
1243
+ const off = disabled || field.readOnly;
1244
+ if (field.control === "segmented") {
1245
+ return /* @__PURE__ */ (0, import_jsx_runtime12.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_runtime12.jsx)(
1246
+ "button",
1247
+ {
1248
+ type: "button",
1249
+ role: "radio",
1250
+ "aria-checked": o.value === value,
1251
+ title: o.description,
1252
+ disabled: off,
1253
+ onClick: () => onChange(o.value),
1254
+ 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"}`,
1255
+ children: (0, import_parameters.choiceOptionLabel)(o)
1256
+ },
1257
+ String(o.value)
1258
+ )) });
1259
+ }
1260
+ const index = options.findIndex((o) => o.value === value);
1261
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1262
+ "select",
1263
+ {
1264
+ id,
1265
+ "aria-label": (0, import_parameters.parameterLabel)(field),
1266
+ value: String(index),
1267
+ disabled: off,
1268
+ onChange: (e) => {
1269
+ const option = options[Number(e.target.value)];
1270
+ if (option) onChange(option.value);
1271
+ },
1272
+ className: parameterInputClass,
1273
+ children: options.map((o, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("option", { value: String(i), title: o.description, children: (0, import_parameters.choiceOptionLabel)(o) }, String(o.value)))
1274
+ }
1275
+ );
1276
+ }
1277
+ function ParameterColor({ field, value, onChange, onGestureStart, onGestureEnd, disabled, id }) {
1278
+ const picking = (0, import_react12.useRef)(false);
1279
+ const alpha = !!field.alpha;
1280
+ const hex = (0, import_parameters.normalizeHexColor)(value, true) ?? "#000000ff";
1281
+ const rgb = hex.slice(0, 7);
1282
+ const a = hex.slice(7);
1283
+ const off = disabled || field.readOnly;
1284
+ const start = () => {
1285
+ if (picking.current) return;
1286
+ picking.current = true;
1287
+ onGestureStart?.();
1288
+ };
1289
+ const end = () => {
1290
+ if (!picking.current) return;
1291
+ picking.current = false;
1292
+ onGestureEnd?.();
1293
+ };
1294
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-1.5", children: [
1295
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2", children: [
1296
+ /* @__PURE__ */ (0, import_jsx_runtime12.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: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "absolute inset-0", style: { background: alpha ? hex : rgb } }),
1298
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1299
+ "input",
1300
+ {
1301
+ type: "color",
1302
+ "aria-label": (0, import_parameters.parameterLabel)(field),
1303
+ value: rgb,
1304
+ disabled: off,
1305
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed",
1306
+ onChange: (e) => {
1307
+ start();
1308
+ onChange(alpha ? `${e.target.value}${a}` : e.target.value);
1309
+ },
1310
+ onBlur: end
1311
+ }
1312
+ )
1313
+ ] }),
1314
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1315
+ ParameterCommitInput,
1316
+ {
1317
+ id,
1318
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} hex`,
1319
+ value: alpha ? hex : rgb,
1320
+ disabled: off,
1321
+ spellCheck: false,
1322
+ className: `${parameterInputClass} font-mono`,
1323
+ onCommit: (text) => {
1324
+ const next = (0, import_parameters.normalizeHexColor)(text, alpha);
1325
+ if (next) onChange(next);
1326
+ }
1327
+ }
1328
+ )
1329
+ ] }),
1330
+ alpha && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1331
+ "input",
1332
+ {
1333
+ type: "range",
1334
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} opacity`,
1335
+ min: 0,
1336
+ max: 255,
1337
+ step: 1,
1338
+ value: parseInt(a, 16),
1339
+ disabled: off,
1340
+ className: rangeClass,
1341
+ onPointerDown: start,
1342
+ onPointerUp: end,
1343
+ onPointerCancel: end,
1344
+ onBlur: end,
1345
+ onChange: (e) => onChange(`${rgb}${Number(e.target.value).toString(16).padStart(2, "0")}`)
1346
+ }
1347
+ ),
1348
+ field.swatches && field.swatches.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex flex-wrap gap-1", children: field.swatches.map((s) => {
1349
+ const swatch = (0, import_parameters.normalizeHexColor)(s, alpha);
1350
+ if (!swatch) return null;
1351
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1352
+ "button",
1353
+ {
1354
+ type: "button",
1355
+ title: swatch,
1356
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} ${swatch}`,
1357
+ "aria-pressed": swatch === (alpha ? hex : rgb),
1358
+ disabled: off,
1359
+ onClick: () => onChange(swatch),
1360
+ className: "h-5 w-5 rounded border border-input aria-pressed:ring-1 aria-pressed:ring-primary",
1361
+ style: { background: swatch }
1362
+ },
1363
+ s
1364
+ );
1365
+ }) })
1366
+ ] });
1367
+ }
1368
+ function ParameterVector({ field, value, onChange, disabled, id }) {
1369
+ const size = (0, import_parameters.vectorSize)(field);
1370
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))` }, children: Array.from({ length: size }, (_, i) => {
1371
+ const component = (0, import_parameters.vectorComponentField)(field, i);
1372
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("label", { className: "flex min-w-0 items-center gap-1", children: [
1373
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "w-3 shrink-0 text-[10px] uppercase text-muted-foreground", children: component.label }),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1375
+ ParameterNumberInput,
1376
+ {
1377
+ field: component,
1378
+ value: value[i] ?? 0,
1379
+ id: i === 0 ? id : void 0,
1380
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} ${component.label}`,
1381
+ disabled: disabled || field.readOnly,
1382
+ onChange: (n) => onChange(Array.from({ length: size }, (_2, j) => j === i ? n : value[j] ?? 0))
1383
+ }
1384
+ )
1385
+ ] }, i);
1386
+ }) });
1387
+ }
1388
+ function ParameterImage({ field, value, onChange, disabled, id, resolveFile }) {
1389
+ const off = disabled || field.readOnly;
1390
+ const name = (0, import_parameters.parameterLabel)(field);
1391
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-1.5", children: [
1392
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2", children: [
1393
+ /* @__PURE__ */ (0, import_jsx_runtime12.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_runtime12.jsx)("img", { src: value, alt: name, className: "h-full w-full object-cover" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "None" }) }),
1394
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
1395
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1396
+ ParameterCommitInput,
1397
+ {
1398
+ id,
1399
+ "aria-label": `${name} URL`,
1400
+ placeholder: "Image URL",
1401
+ value: value.startsWith("data:") ? "" : value,
1402
+ disabled: off,
1403
+ onCommit: (text) => onChange(text.trim())
1404
+ }
1405
+ ),
1406
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex gap-1", children: [
1407
+ resolveFile && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("label", { className: `${parameterButtonClass} cursor-pointer ${off ? "pointer-events-none opacity-50" : ""}`, children: [
1408
+ "Choose file\u2026",
1409
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1410
+ "input",
1411
+ {
1412
+ type: "file",
1413
+ className: "sr-only",
1414
+ accept: field.accept ?? "image/*",
1415
+ disabled: off,
1416
+ "aria-label": `${name} file`,
1417
+ onChange: async (e) => {
1418
+ const file = e.target.files?.[0];
1419
+ e.target.value = "";
1420
+ if (file) onChange(await resolveFile(file));
1421
+ }
1422
+ }
1423
+ )
1424
+ ] }),
1425
+ value && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: parameterButtonClass, disabled: off, onClick: () => onChange(""), "aria-label": `Clear ${name}`, children: "Clear" })
1426
+ ] })
1427
+ ] })
1428
+ ] }),
1429
+ field.options && field.options.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex flex-wrap gap-1", children: field.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1430
+ "button",
1431
+ {
1432
+ type: "button",
1433
+ title: o.label ?? o.url,
1434
+ "aria-label": o.label ?? o.url,
1435
+ "aria-pressed": o.url === value,
1436
+ disabled: off,
1437
+ onClick: () => onChange(o.url),
1438
+ className: "h-9 w-12 overflow-hidden rounded border border-input aria-pressed:ring-2 aria-pressed:ring-primary",
1439
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("img", { src: o.url, alt: "", className: "h-full w-full object-cover" })
1440
+ },
1441
+ o.url
1442
+ )) })
1443
+ ] });
1444
+ }
1445
+ function ParameterText({ field, value, onChange, disabled, id }) {
1446
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1447
+ ParameterCommitInput,
1448
+ {
1449
+ id,
1450
+ "aria-label": (0, import_parameters.parameterLabel)(field),
1451
+ multiline: field.multiline,
1452
+ placeholder: field.placeholder,
1453
+ maxLength: field.maxLength,
1454
+ value,
1455
+ disabled: disabled || field.readOnly,
1456
+ onCommit: onChange
1457
+ }
1458
+ );
1459
+ }
1460
+ function ParameterFieldControl({ field, value, onChange, resolveFile, ...rest }) {
1461
+ const set = onChange;
1462
+ switch (field.type) {
1463
+ case "number":
1464
+ case "int":
1465
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterSlider, { field, value, onChange: set, ...rest });
1466
+ case "boolean":
1467
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterToggle, { field, value, onChange: set, ...rest });
1468
+ case "choice":
1469
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterSelect, { field, value, onChange: set, ...rest });
1470
+ case "color":
1471
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterColor, { field, value, onChange: set, ...rest });
1472
+ case "vector":
1473
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterVector, { field, value, onChange: set, ...rest });
1474
+ case "image":
1475
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterImage, { field, value, onChange: set, resolveFile, ...rest });
1476
+ case "text":
1477
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ParameterText, { field, value, onChange: set, ...rest });
1478
+ }
1479
+ }
1480
+
1481
+ // src/effects/EffectParameters.tsx
1482
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1483
+ function EffectParameters({ description, values, onChange, onReset, keyframed, onPreset, disabled = false, className = "" }) {
1484
+ const fields = (0, import_react13.useMemo)(() => (0, import_effects3.effectParameterFields)(description), [description]);
1485
+ const idBase = (0, import_react13.useId)();
1486
+ const gesture = import_react13.default.useRef(null);
1487
+ const count = import_react13.default.useRef(0);
1488
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: `space-y-2 ${className}`, "data-effect-parameters": description.id, children: [
1489
+ onPreset && description.presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1490
+ "select",
1491
+ {
1492
+ className: "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs",
1493
+ "aria-label": `${description.name} presets`,
1494
+ value: "",
1495
+ disabled,
1496
+ onChange: (e) => {
1497
+ const preset = description.presets.find((p) => p.name === e.currentTarget.value);
1498
+ if (preset) onPreset({ ...description.defaults, ...preset.values });
1499
+ },
1500
+ children: [
1501
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("option", { value: "", children: "Presets\u2026" }),
1502
+ description.presets.map((p) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("option", { value: p.name, children: p.name }, p.name))
1503
+ ]
1504
+ }
1505
+ ),
1506
+ fields.map((field) => {
1507
+ const label = (0, import_parameters2.parameterLabel)(field);
1508
+ const inputId = `${idBase}-${field.id}`;
1509
+ const keyed = keyframed?.has(field.id) ?? false;
1510
+ const value = (0, import_effects3.effectFieldValue)(field, values[field.id]);
1511
+ const changed = !keyed && !(0, import_parameters2.parameterValuesEqual)(value, field.default);
1512
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "space-y-1", "data-effect-control": field.id, children: [
1513
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-1.5", children: [
1514
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("label", { htmlFor: inputId, className: "min-w-0 flex-1 truncate text-xs font-medium", title: field.description, children: label }),
1515
+ keyed && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "keyframed" }),
1516
+ onReset && changed && !disabled && /* @__PURE__ */ (0, import_jsx_runtime13.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" })
1517
+ ] }),
1518
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1519
+ ParameterFieldControl,
1520
+ {
1521
+ field,
1522
+ value,
1523
+ id: inputId,
1524
+ disabled: disabled || keyed,
1525
+ onChange: (v) => onChange(field.id, v, gesture.current),
1526
+ onGestureStart: () => gesture.current = `${idBase}:${field.id}:${++count.current}`,
1527
+ onGestureEnd: () => gesture.current = null
1528
+ }
1529
+ )
1530
+ ] }, field.id);
1531
+ }),
1532
+ !fields.length && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No controls." })
1533
+ ] });
1534
+ }
1535
+
1536
+ // src/animation-editor/AnimationEffectsPanel.tsx
1537
+ var import_react14 = require("react");
1538
+ var import_animation_editor9 = require("@pixerate/editor/animation-editor");
1539
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1540
+ var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
1541
+ function Controls({ controller, instance, effect, values }) {
1542
+ const provider = controller.effectsProvider;
1543
+ const { description, error } = useEffectDescription(provider, effect);
1544
+ const [message, setMessage] = (0, import_react14.useState)(null);
1545
+ const report = (r) => void r.then((x) => setMessage(x.ok ? null : x.error));
1546
+ if (!provider) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider: its commands are kept as they are." });
1547
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-amber-500", children: error });
1548
+ if (!description) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "Loading\u2026" });
1549
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
1550
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1551
+ EffectParameters,
1552
+ {
1553
+ description,
1554
+ values,
1555
+ onChange: (k, v, gesture) => report(controller.updateMotionEffect(instance, { [k]: v }, { merge: gesture ?? void 0 })),
1556
+ onReset: (k) => report(controller.updateMotionEffect(instance, { [k]: null })),
1557
+ onPreset: (v) => report(controller.updateMotionEffect(instance, v))
1558
+ }
1559
+ ),
1560
+ message && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
1561
+ ] });
1562
+ }
1563
+ function AnimationEffectsPanel({ controller, node, className = "" }) {
1564
+ const state = useAnimationEditorState(controller);
1565
+ const provider = state.effectsProvider;
1566
+ const nodes = (0, import_react14.useMemo)(() => [...(0, import_animation_editor9.indexMotionNodes)(state.scene.artwork.nodes).keys()], [state.scene.artwork]);
1567
+ const [chosen, setChosen] = (0, import_react14.useState)("");
1568
+ const target = node && nodes.includes(node) ? node : chosen && nodes.includes(chosen) ? chosen : "";
1569
+ const [browse, setBrowse] = (0, import_react14.useState)(null);
1570
+ const [open, setOpen] = (0, import_react14.useState)(null);
1571
+ const [message, setMessage] = (0, import_react14.useState)(null);
1572
+ const report = (r) => void Promise.resolve(r).then((x) => setMessage(x.ok ? x.notes.length ? x.notes.join(" ") : null : x.error));
1573
+ const applied = (0, import_react14.useMemo)(() => controller.appliedEffects(), [controller, state.scene]);
1574
+ const disabled = state.readOnly;
1575
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("section", { className: `flex flex-col gap-2 text-xs ${className}`, "aria-label": "Effects", "data-animation-effects": "", children: [
1576
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
1577
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "flex-1 font-semibold", children: "Effects" }),
1578
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-[11px]", "aria-label": "Node to animate", value: target, disabled: !!node, onChange: (e) => setChosen(e.currentTarget.value), children: [
1579
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("option", { value: "", children: "Node\u2026" }),
1580
+ nodes.map((id) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("option", { value: id, children: id }, id))
1581
+ ] }),
1582
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: btn4, disabled: disabled || !provider || !target, "aria-pressed": browse === "motion", onClick: () => setBrowse(browse === "motion" ? null : "motion"), children: "Animate\u2026" }),
1583
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: btn4, disabled: disabled || !provider, "aria-pressed": browse === "vector", onClick: () => setBrowse(browse === "vector" ? null : "vector"), children: "Vector effect\u2026" })
1584
+ ] }),
1585
+ !provider && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider is registered. Commands effects made are kept." }),
1586
+ browse === "motion" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(EffectsBrowser, { provider, uses: ["motion"], kinds: ["motion"], title: `Motion for ${target}`, onPick: (e) => (setBrowse(null), report(controller.applyMotionEffect(e.id, [target]))) }),
1587
+ browse === "vector" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(EffectsBrowser, { provider, kinds: ["vector"], title: "Vector effects", onPick: (e) => (setBrowse(null), report(controller.addVectorEffect(e.id))) }),
1588
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ol", { className: "space-y-1", children: applied.map((e) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("li", { className: "rounded border border-border p-1", "data-motion-effect": e.instance, children: [
1589
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex items-center gap-1", children: [
1590
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("button", { type: "button", className: "min-w-0 flex-1 truncate text-left", "aria-expanded": open === e.instance, onClick: () => setOpen(open === e.instance ? null : e.instance), children: [
1591
+ e.name ?? e.effect,
1592
+ " ",
1593
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "text-muted-foreground", children: [
1594
+ "\xB7 ",
1595
+ e.kind === "vector" ? "picture" : e.targets.join(", ")
1596
+ ] })
1597
+ ] }),
1598
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: btn4, disabled, "aria-label": `Remove ${e.name ?? e.effect}`, onClick: () => report(controller.removeMotionEffect(e.instance)), children: "\u2715" })
1599
+ ] }),
1600
+ open === e.instance && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "mt-1", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Controls, { controller, instance: e.instance, effect: e.effect, values: e.values }) })
1601
+ ] }, e.instance)) }),
1602
+ !applied.length && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No provider effects in this scene." }),
1603
+ message && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { role: "status", className: "text-[11px] text-muted-foreground", "data-effects-message": "", children: message })
1604
+ ] });
1605
+ }
1606
+
1607
+ // src/animation-editor/AnimationEditor.tsx
1608
+ var import_jsx_runtime15 = require("react/jsx-runtime");
971
1609
  var ONION = { before: 3, after: 1 };
972
- var btn4 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
973
- function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues = true, editable = true, className = "" }) {
1610
+ var btn5 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
1611
+ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues = true, editable = true, effects, className = "" }) {
974
1612
  const state = useAnimationEditorState(controller);
1613
+ useEffects(controller, effects);
975
1614
  const { issues, scene, selection, readOnly } = state;
976
- const [picked, setPicked] = (0, import_react9.useState)(null);
977
- const [onion, setOnion] = (0, import_react9.useState)(false);
978
- const [paths, setPaths] = (0, import_react9.useState)(false);
1615
+ const [picked, setPicked] = (0, import_react15.useState)(null);
1616
+ const [onion, setOnion] = (0, import_react15.useState)(false);
1617
+ const [paths, setPaths] = (0, import_react15.useState)(false);
979
1618
  const command = selection?.command !== void 0 ? scene.shots.find((s) => s.id === selection.shot)?.run[selection.command] : void 0;
980
1619
  const targets = command && "targets" in command ? command.targets : [];
981
- const nodes = (0, import_react9.useMemo)(() => (0, import_animation_editor9.indexMotionNodes)(scene.artwork.nodes), [scene.artwork]);
1620
+ const nodes = (0, import_react15.useMemo)(() => (0, import_animation_editor10.indexMotionNodes)(scene.artwork.nodes), [scene.artwork]);
982
1621
  const pickedValid = picked && nodes.has(picked) ? picked : null;
983
1622
  const onPick = (id, event) => {
984
1623
  if (event.shiftKey && id && command && "targets" in command && selection?.command !== void 0 && !readOnly) {
@@ -988,7 +1627,7 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
988
1627
  }
989
1628
  setPicked(id);
990
1629
  };
991
- const stage = /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1630
+ const stage = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
992
1631
  AnimationStage,
993
1632
  {
994
1633
  controller,
@@ -1000,14 +1639,14 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
1000
1639
  className: "rounded-md border border-border bg-muted/40"
1001
1640
  }
1002
1641
  );
1003
- const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "space-y-0.5 text-xs", "aria-label": "Animation issues", children: issues.map((issue, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", children: (0, import_animation_editor9.motionIssueLine)(issue) }, i)) });
1642
+ const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "space-y-0.5 text-xs", "aria-label": "Animation issues", children: issues.map((issue, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", children: (0, import_animation_editor10.motionIssueLine)(issue) }, i)) });
1004
1643
  if (!editable)
1005
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
1644
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
1006
1645
  stage,
1007
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PlayheadScrubber, { controller }),
1646
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PlayheadScrubber, { controller }),
1008
1647
  issueList
1009
1648
  ] });
1010
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1649
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1011
1650
  "div",
1012
1651
  {
1013
1652
  className: `grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] ${className}`,
@@ -1020,39 +1659,40 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
1020
1659
  else controller.undo();
1021
1660
  },
1022
1661
  children: [
1023
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
1662
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
1024
1663
  stage,
1025
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PlayheadScrubber, { controller }),
1026
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
1027
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: btn4, onClick: () => controller.undo(), disabled: !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
1028
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: btn4, onClick: () => controller.redo(), disabled: !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" }),
1029
- onionSkin === void 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex items-center gap-1", children: [
1030
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "checkbox", checked: onion, onChange: (e) => setOnion(e.target.checked) }),
1664
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PlayheadScrubber, { controller }),
1665
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
1666
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: btn5, onClick: () => controller.undo(), disabled: !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
1667
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: btn5, onClick: () => controller.redo(), disabled: !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" }),
1668
+ onionSkin === void 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("label", { className: "flex items-center gap-1", children: [
1669
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("input", { type: "checkbox", checked: onion, onChange: (e) => setOnion(e.target.checked) }),
1031
1670
  " Onion skin"
1032
1671
  ] }),
1033
- motionPaths === void 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex items-center gap-1", children: [
1034
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "checkbox", checked: paths, onChange: (e) => setPaths(e.target.checked) }),
1672
+ motionPaths === void 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("label", { className: "flex items-center gap-1", children: [
1673
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("input", { type: "checkbox", checked: paths, onChange: (e) => setPaths(e.target.checked) }),
1035
1674
  " Motion paths"
1036
1675
  ] }),
1037
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ml-auto", "data-picked-element": pickedValid ?? void 0, children: pickedValid ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1676
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "ml-auto", "data-picked-element": pickedValid ?? void 0, children: pickedValid ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
1038
1677
  "Picked ",
1039
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { className: "text-rose-500", children: pickedValid }),
1678
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("code", { className: "text-rose-500", children: pickedValid }),
1040
1679
  " ",
1041
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "underline", onClick: () => setPicked(null), children: "clear" })
1680
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "underline", onClick: () => setPicked(null), children: "clear" })
1042
1681
  ] }) : "Click the stage to pick an element; shift-click to toggle it as a target." })
1043
1682
  ] }),
1044
1683
  issueList
1045
1684
  ] }),
1046
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-col gap-3", children: [
1047
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(CommandList, { controller, targets: pickedValid ? [pickedValid] : void 0 }),
1048
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(CommandInspector, { controller, picked: pickedValid, className: "border-t border-border pt-2" }),
1049
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { open: true, className: "border-t border-border pt-2", children: [
1050
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("summary", { className: "cursor-pointer text-xs font-medium", children: "Shot" }),
1051
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShotInspector, { controller, className: "mt-1" })
1685
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex min-w-0 flex-col gap-3", children: [
1686
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CommandList, { controller, targets: pickedValid ? [pickedValid] : void 0 }),
1687
+ (state.effectsProvider || controller.appliedEffects().length > 0) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AnimationEffectsPanel, { controller, node: pickedValid, className: "border-t border-border pt-2" }),
1688
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CommandInspector, { controller, picked: pickedValid, className: "border-t border-border pt-2" }),
1689
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("details", { open: true, className: "border-t border-border pt-2", children: [
1690
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("summary", { className: "cursor-pointer text-xs font-medium", children: "Shot" }),
1691
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ShotInspector, { controller, className: "mt-1" })
1052
1692
  ] }),
1053
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { className: "border-t border-border pt-2", children: [
1054
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("summary", { className: "cursor-pointer text-xs font-medium", children: "Scene settings" }),
1055
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SceneSettings, { controller, className: "mt-1" })
1693
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("details", { className: "border-t border-border pt-2", children: [
1694
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("summary", { className: "cursor-pointer text-xs font-medium", children: "Scene settings" }),
1695
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SceneSettings, { controller, className: "mt-1" })
1056
1696
  ] })
1057
1697
  ] })
1058
1698
  ]
@@ -1062,6 +1702,7 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
1062
1702
  // Annotate the CommonJS export names for ESM import in node:
1063
1703
  0 && (module.exports = {
1064
1704
  AnimationEditor,
1705
+ AnimationEffectsPanel,
1065
1706
  AnimationStage,
1066
1707
  CommandInspector,
1067
1708
  CommandList,
@@ -1073,6 +1714,7 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
1073
1714
  useAnimationEditor,
1074
1715
  useAnimationEditorController,
1075
1716
  useAnimationEditorState,
1076
- useAnimationEditorTools
1717
+ useAnimationEditorTools,
1718
+ useAnimationEffectsTools
1077
1719
  });
1078
1720
  //# sourceMappingURL=index.cjs.map