@pixerate/editor-react 0.78.0 → 0.79.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.
@@ -1,6 +1,7 @@
1
- import { AnimationEditorController, AnimationEditorState, AnimationEditorOptions, AnimationEditorToolProviderOptions, MotionFrameOptions, MotionOnionSkin, MotionCommandBase } from '@pixerate/editor/animation-editor';
1
+ import { AnimationEditorController, AnimationEditorState, AnimationEditorOptions, AnimationEditorToolProviderOptions, AnimationEffectsToolProviderOptions, MotionFrameOptions, MotionOnionSkin, MotionCommandBase } from '@pixerate/editor/animation-editor';
2
2
  import { ToolRegistry } from '@pixerate/editor/tools';
3
3
  import React__default from 'react';
4
+ import { EffectProvider } from '@pixerate/editor/effects';
4
5
 
5
6
  /** An `AnimationEditorController` created once for the component's lifetime (destroyed on unmount). */
6
7
  declare function useAnimationEditorController(options?: AnimationEditorOptions): AnimationEditorController;
@@ -28,6 +29,12 @@ declare function useAnimationEditor(controllerOrOptions?: AnimationEditorControl
28
29
  * `export_flair_effect`, …) in `registry` while mounted.
29
30
  */
30
31
  declare function useAnimationEditorTools(registry: ToolRegistry | null | undefined, controller: AnimationEditorController, options?: AnimationEditorToolProviderOptions): void;
32
+ /**
33
+ * Registers the animation editor's provider-effect tools (`search_effects`, `apply_effect`,
34
+ * `add_vector_effect`, …) in `registry` while mounted. They work once an effects provider is
35
+ * registered (see `useEffects`).
36
+ */
37
+ declare function useAnimationEffectsTools(registry: ToolRegistry | null | undefined, controller: AnimationEditorController, options?: AnimationEffectsToolProviderOptions): void;
31
38
 
32
39
  interface AnimationStageProps {
33
40
  controller: AnimationEditorController;
@@ -77,6 +84,12 @@ interface AnimationEditorProps {
77
84
  * scrubber and issues only.
78
85
  */
79
86
  editable?: boolean;
87
+ /**
88
+ * An effects provider (see `@pixerate/editor/effects`), registered with the controller while
89
+ * mounted: provider motion applied to the picked element and vector effects, with an Effects
90
+ * panel. Optional: without one the editor is as before.
91
+ */
92
+ effects?: EffectProvider | null;
80
93
  className?: string;
81
94
  }
82
95
  /**
@@ -85,7 +98,7 @@ interface AnimationEditorProps {
85
98
  * playhead scrubber, and panels to edit commands, shots and scene settings by hand. Every edit
86
99
  * is one undo step.
87
100
  */
88
- declare function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues, editable, className }: AnimationEditorProps): React__default.JSX.Element;
101
+ declare function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues, editable, effects, className }: AnimationEditorProps): React__default.JSX.Element;
89
102
 
90
103
  interface PlayheadScrubberProps {
91
104
  controller: AnimationEditorController;
@@ -166,4 +179,17 @@ interface SceneSettingsProps {
166
179
  */
167
180
  declare function SceneSettings({ controller, className }: SceneSettingsProps): React__default.JSX.Element;
168
181
 
169
- export { AnimationEditor, type AnimationEditorProps, AnimationStage, type AnimationStageProps, CommandInspector, type CommandInspectorProps, CommandList, type CommandListProps, EasePicker, type EasePickerProps, PlayheadScrubber, type PlayheadScrubberProps, SceneSettings, type SceneSettingsProps, ShotInspector, type ShotInspectorProps, ShotTimeline, type ShotTimelineProps, type UseAnimationEditorResult, useAnimationEditor, useAnimationEditorController, useAnimationEditorState, useAnimationEditorTools };
182
+ interface AnimationEffectsPanelProps {
183
+ controller: AnimationEditorController;
184
+ /** The node motion effects are applied to (e.g. the element picked on the stage). */
185
+ node?: string | null;
186
+ className?: string;
187
+ }
188
+ /**
189
+ * Provider effects in an animation: apply a motion effect to a node (its changes become commands
190
+ * in a new shot), add a vector effect with its own picture, and change or remove the effects the
191
+ * scene holds. Every change is one undo step (a slider drag is one).
192
+ */
193
+ declare function AnimationEffectsPanel({ controller, node, className }: AnimationEffectsPanelProps): React__default.JSX.Element;
194
+
195
+ export { AnimationEditor, type AnimationEditorProps, AnimationEffectsPanel, type AnimationEffectsPanelProps, AnimationStage, type AnimationStageProps, CommandInspector, type CommandInspectorProps, CommandList, type CommandListProps, EasePicker, type EasePickerProps, PlayheadScrubber, type PlayheadScrubberProps, SceneSettings, type SceneSettingsProps, ShotInspector, type ShotInspectorProps, ShotTimeline, type ShotTimelineProps, type UseAnimationEditorResult, useAnimationEditor, useAnimationEditorController, useAnimationEditorState, useAnimationEditorTools, useAnimationEffectsTools };
@@ -1,6 +1,7 @@
1
- import { AnimationEditorController, AnimationEditorState, AnimationEditorOptions, AnimationEditorToolProviderOptions, MotionFrameOptions, MotionOnionSkin, MotionCommandBase } from '@pixerate/editor/animation-editor';
1
+ import { AnimationEditorController, AnimationEditorState, AnimationEditorOptions, AnimationEditorToolProviderOptions, AnimationEffectsToolProviderOptions, MotionFrameOptions, MotionOnionSkin, MotionCommandBase } from '@pixerate/editor/animation-editor';
2
2
  import { ToolRegistry } from '@pixerate/editor/tools';
3
3
  import React__default from 'react';
4
+ import { EffectProvider } from '@pixerate/editor/effects';
4
5
 
5
6
  /** An `AnimationEditorController` created once for the component's lifetime (destroyed on unmount). */
6
7
  declare function useAnimationEditorController(options?: AnimationEditorOptions): AnimationEditorController;
@@ -28,6 +29,12 @@ declare function useAnimationEditor(controllerOrOptions?: AnimationEditorControl
28
29
  * `export_flair_effect`, …) in `registry` while mounted.
29
30
  */
30
31
  declare function useAnimationEditorTools(registry: ToolRegistry | null | undefined, controller: AnimationEditorController, options?: AnimationEditorToolProviderOptions): void;
32
+ /**
33
+ * Registers the animation editor's provider-effect tools (`search_effects`, `apply_effect`,
34
+ * `add_vector_effect`, …) in `registry` while mounted. They work once an effects provider is
35
+ * registered (see `useEffects`).
36
+ */
37
+ declare function useAnimationEffectsTools(registry: ToolRegistry | null | undefined, controller: AnimationEditorController, options?: AnimationEffectsToolProviderOptions): void;
31
38
 
32
39
  interface AnimationStageProps {
33
40
  controller: AnimationEditorController;
@@ -77,6 +84,12 @@ interface AnimationEditorProps {
77
84
  * scrubber and issues only.
78
85
  */
79
86
  editable?: boolean;
87
+ /**
88
+ * An effects provider (see `@pixerate/editor/effects`), registered with the controller while
89
+ * mounted: provider motion applied to the picked element and vector effects, with an Effects
90
+ * panel. Optional: without one the editor is as before.
91
+ */
92
+ effects?: EffectProvider | null;
80
93
  className?: string;
81
94
  }
82
95
  /**
@@ -85,7 +98,7 @@ interface AnimationEditorProps {
85
98
  * playhead scrubber, and panels to edit commands, shots and scene settings by hand. Every edit
86
99
  * is one undo step.
87
100
  */
88
- declare function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues, editable, className }: AnimationEditorProps): React__default.JSX.Element;
101
+ declare function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues, editable, effects, className }: AnimationEditorProps): React__default.JSX.Element;
89
102
 
90
103
  interface PlayheadScrubberProps {
91
104
  controller: AnimationEditorController;
@@ -166,4 +179,17 @@ interface SceneSettingsProps {
166
179
  */
167
180
  declare function SceneSettings({ controller, className }: SceneSettingsProps): React__default.JSX.Element;
168
181
 
169
- export { AnimationEditor, type AnimationEditorProps, AnimationStage, type AnimationStageProps, CommandInspector, type CommandInspectorProps, CommandList, type CommandListProps, EasePicker, type EasePickerProps, PlayheadScrubber, type PlayheadScrubberProps, SceneSettings, type SceneSettingsProps, ShotInspector, type ShotInspectorProps, ShotTimeline, type ShotTimelineProps, type UseAnimationEditorResult, useAnimationEditor, useAnimationEditorController, useAnimationEditorState, useAnimationEditorTools };
182
+ interface AnimationEffectsPanelProps {
183
+ controller: AnimationEditorController;
184
+ /** The node motion effects are applied to (e.g. the element picked on the stage). */
185
+ node?: string | null;
186
+ className?: string;
187
+ }
188
+ /**
189
+ * Provider effects in an animation: apply a motion effect to a node (its changes become commands
190
+ * in a new shot), add a vector effect with its own picture, and change or remove the effects the
191
+ * scene holds. Every change is one undo step (a slider drag is one).
192
+ */
193
+ declare function AnimationEffectsPanel({ controller, node, className }: AnimationEffectsPanelProps): React__default.JSX.Element;
194
+
195
+ export { AnimationEditor, type AnimationEditorProps, AnimationEffectsPanel, type AnimationEffectsPanelProps, AnimationStage, type AnimationStageProps, CommandInspector, type CommandInspectorProps, CommandList, type CommandListProps, EasePicker, type EasePickerProps, PlayheadScrubber, type PlayheadScrubberProps, SceneSettings, type SceneSettingsProps, ShotInspector, type ShotInspectorProps, ShotTimeline, type ShotTimelineProps, type UseAnimationEditorResult, useAnimationEditor, useAnimationEditorController, useAnimationEditorState, useAnimationEditorTools, useAnimationEffectsTools };
@@ -1,8 +1,17 @@
1
+ import {
2
+ EffectParameters,
3
+ EffectsBrowser,
4
+ useEffectDescription,
5
+ useEffects
6
+ } from "../chunk-WOQ3H7KA.js";
7
+ import "../chunk-ZQZAKUUM.js";
8
+
1
9
  // src/animation-editor/useAnimationEditor.ts
2
10
  import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
11
  import {
4
12
  AnimationEditorController,
5
- createAnimationEditorToolProvider
13
+ createAnimationEditorToolProvider,
14
+ createAnimationEffectsToolProvider
6
15
  } from "@pixerate/editor/animation-editor";
7
16
  function useAnimationEditorController(options = {}) {
8
17
  const ref = useRef(null);
@@ -45,6 +54,13 @@ function useAnimationEditorTools(registry, controller, options = {}) {
45
54
  return registry.register(createAnimationEditorToolProvider(controller, { id, namespace, readOnly, rasterize }));
46
55
  }, [registry, controller, id, namespace, readOnly, rasterize]);
47
56
  }
57
+ function useAnimationEffectsTools(registry, controller, options = {}) {
58
+ const { id, namespace, readOnly } = options;
59
+ useEffect(() => {
60
+ if (!registry) return;
61
+ return registry.register(createAnimationEffectsToolProvider(controller, { id, namespace, readOnly }));
62
+ }, [registry, controller, id, namespace, readOnly]);
63
+ }
48
64
 
49
65
  // src/animation-editor/AnimationStage.tsx
50
66
  import { useMemo as useMemo2 } from "react";
@@ -146,8 +162,8 @@ function ShotTimeline({ controller, className = "" }) {
146
162
  }
147
163
 
148
164
  // src/animation-editor/AnimationEditor.tsx
149
- import { useMemo as useMemo6, useState as useState6 } from "react";
150
- import { indexMotionNodes, motionIssueLine } from "@pixerate/editor/animation-editor";
165
+ import { useMemo as useMemo7, useState as useState7 } from "react";
166
+ import { indexMotionNodes as indexMotionNodes2, motionIssueLine } from "@pixerate/editor/animation-editor";
151
167
 
152
168
  // src/animation-editor/CommandInspector.tsx
153
169
  import React3, { useMemo as useMemo4, useState } from "react";
@@ -931,19 +947,91 @@ function ShotInspector({ controller, shot, className = "" }) {
931
947
  ] });
932
948
  }
933
949
 
934
- // src/animation-editor/AnimationEditor.tsx
950
+ // src/animation-editor/AnimationEffectsPanel.tsx
951
+ import { useMemo as useMemo6, useState as useState6 } from "react";
952
+ import { indexMotionNodes } from "@pixerate/editor/animation-editor";
935
953
  import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
954
+ var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
955
+ function Controls({ controller, instance, effect, values }) {
956
+ const provider = controller.effectsProvider;
957
+ const { description, error } = useEffectDescription(provider, effect);
958
+ const [message, setMessage] = useState6(null);
959
+ const report = (r) => void r.then((x) => setMessage(x.ok ? null : x.error));
960
+ if (!provider) return /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider: its commands are kept as they are." });
961
+ if (error) return /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-amber-500", children: error });
962
+ if (!description) return /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground", children: "Loading\u2026" });
963
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
964
+ /* @__PURE__ */ jsx10(
965
+ EffectParameters,
966
+ {
967
+ description,
968
+ values,
969
+ onChange: (k, v, gesture) => report(controller.updateMotionEffect(instance, { [k]: v }, { merge: gesture ?? void 0 })),
970
+ onReset: (k) => report(controller.updateMotionEffect(instance, { [k]: null })),
971
+ onPreset: (v) => report(controller.updateMotionEffect(instance, v))
972
+ }
973
+ ),
974
+ message && /* @__PURE__ */ jsx10("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
975
+ ] });
976
+ }
977
+ function AnimationEffectsPanel({ controller, node, className = "" }) {
978
+ const state = useAnimationEditorState(controller);
979
+ const provider = state.effectsProvider;
980
+ const nodes = useMemo6(() => [...indexMotionNodes(state.scene.artwork.nodes).keys()], [state.scene.artwork]);
981
+ const [chosen, setChosen] = useState6("");
982
+ const target = node && nodes.includes(node) ? node : chosen && nodes.includes(chosen) ? chosen : "";
983
+ const [browse, setBrowse] = useState6(null);
984
+ const [open, setOpen] = useState6(null);
985
+ const [message, setMessage] = useState6(null);
986
+ const report = (r) => void Promise.resolve(r).then((x) => setMessage(x.ok ? x.notes.length ? x.notes.join(" ") : null : x.error));
987
+ const applied = useMemo6(() => controller.appliedEffects(), [controller, state.scene]);
988
+ const disabled = state.readOnly;
989
+ return /* @__PURE__ */ jsxs8("section", { className: `flex flex-col gap-2 text-xs ${className}`, "aria-label": "Effects", "data-animation-effects": "", children: [
990
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-1", children: [
991
+ /* @__PURE__ */ jsx10("span", { className: "flex-1 font-semibold", children: "Effects" }),
992
+ /* @__PURE__ */ jsxs8("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: [
993
+ /* @__PURE__ */ jsx10("option", { value: "", children: "Node\u2026" }),
994
+ nodes.map((id) => /* @__PURE__ */ jsx10("option", { value: id, children: id }, id))
995
+ ] }),
996
+ /* @__PURE__ */ jsx10("button", { type: "button", className: btn4, disabled: disabled || !provider || !target, "aria-pressed": browse === "motion", onClick: () => setBrowse(browse === "motion" ? null : "motion"), children: "Animate\u2026" }),
997
+ /* @__PURE__ */ jsx10("button", { type: "button", className: btn4, disabled: disabled || !provider, "aria-pressed": browse === "vector", onClick: () => setBrowse(browse === "vector" ? null : "vector"), children: "Vector effect\u2026" })
998
+ ] }),
999
+ !provider && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider is registered. Commands effects made are kept." }),
1000
+ browse === "motion" && /* @__PURE__ */ jsx10(EffectsBrowser, { provider, uses: ["motion"], kinds: ["motion"], title: `Motion for ${target}`, onPick: (e) => (setBrowse(null), report(controller.applyMotionEffect(e.id, [target]))) }),
1001
+ browse === "vector" && /* @__PURE__ */ jsx10(EffectsBrowser, { provider, kinds: ["vector"], title: "Vector effects", onPick: (e) => (setBrowse(null), report(controller.addVectorEffect(e.id))) }),
1002
+ /* @__PURE__ */ jsx10("ol", { className: "space-y-1", children: applied.map((e) => /* @__PURE__ */ jsxs8("li", { className: "rounded border border-border p-1", "data-motion-effect": e.instance, children: [
1003
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-1", children: [
1004
+ /* @__PURE__ */ jsxs8("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: [
1005
+ e.name ?? e.effect,
1006
+ " ",
1007
+ /* @__PURE__ */ jsxs8("span", { className: "text-muted-foreground", children: [
1008
+ "\xB7 ",
1009
+ e.kind === "vector" ? "picture" : e.targets.join(", ")
1010
+ ] })
1011
+ ] }),
1012
+ /* @__PURE__ */ jsx10("button", { type: "button", className: btn4, disabled, "aria-label": `Remove ${e.name ?? e.effect}`, onClick: () => report(controller.removeMotionEffect(e.instance)), children: "\u2715" })
1013
+ ] }),
1014
+ open === e.instance && /* @__PURE__ */ jsx10("div", { className: "mt-1", children: /* @__PURE__ */ jsx10(Controls, { controller, instance: e.instance, effect: e.effect, values: e.values }) })
1015
+ ] }, e.instance)) }),
1016
+ !applied.length && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground", children: "No provider effects in this scene." }),
1017
+ message && /* @__PURE__ */ jsx10("p", { role: "status", className: "text-[11px] text-muted-foreground", "data-effects-message": "", children: message })
1018
+ ] });
1019
+ }
1020
+
1021
+ // src/animation-editor/AnimationEditor.tsx
1022
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
936
1023
  var ONION = { before: 3, after: 1 };
937
- var btn4 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
938
- function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues = true, editable = true, className = "" }) {
1024
+ var btn5 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
1025
+ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues = true, editable = true, effects, className = "" }) {
939
1026
  const state = useAnimationEditorState(controller);
1027
+ useEffects(controller, effects);
940
1028
  const { issues, scene, selection, readOnly } = state;
941
- const [picked, setPicked] = useState6(null);
942
- const [onion, setOnion] = useState6(false);
943
- const [paths, setPaths] = useState6(false);
1029
+ const [picked, setPicked] = useState7(null);
1030
+ const [onion, setOnion] = useState7(false);
1031
+ const [paths, setPaths] = useState7(false);
944
1032
  const command = selection?.command !== void 0 ? scene.shots.find((s) => s.id === selection.shot)?.run[selection.command] : void 0;
945
1033
  const targets = command && "targets" in command ? command.targets : [];
946
- const nodes = useMemo6(() => indexMotionNodes(scene.artwork.nodes), [scene.artwork]);
1034
+ const nodes = useMemo7(() => indexMotionNodes2(scene.artwork.nodes), [scene.artwork]);
947
1035
  const pickedValid = picked && nodes.has(picked) ? picked : null;
948
1036
  const onPick = (id, event) => {
949
1037
  if (event.shiftKey && id && command && "targets" in command && selection?.command !== void 0 && !readOnly) {
@@ -953,7 +1041,7 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
953
1041
  }
954
1042
  setPicked(id);
955
1043
  };
956
- const stage = /* @__PURE__ */ jsx10(
1044
+ const stage = /* @__PURE__ */ jsx11(
957
1045
  AnimationStage,
958
1046
  {
959
1047
  controller,
@@ -965,14 +1053,14 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
965
1053
  className: "rounded-md border border-border bg-muted/40"
966
1054
  }
967
1055
  );
968
- const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx10("ul", { className: "space-y-0.5 text-xs", "aria-label": "Animation issues", children: issues.map((issue, i) => /* @__PURE__ */ jsx10("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", children: motionIssueLine(issue) }, i)) });
1056
+ const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx11("ul", { className: "space-y-0.5 text-xs", "aria-label": "Animation issues", children: issues.map((issue, i) => /* @__PURE__ */ jsx11("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", children: motionIssueLine(issue) }, i)) });
969
1057
  if (!editable)
970
- return /* @__PURE__ */ jsxs8("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
1058
+ return /* @__PURE__ */ jsxs9("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
971
1059
  stage,
972
- /* @__PURE__ */ jsx10(PlayheadScrubber, { controller }),
1060
+ /* @__PURE__ */ jsx11(PlayheadScrubber, { controller }),
973
1061
  issueList
974
1062
  ] });
975
- return /* @__PURE__ */ jsxs8(
1063
+ return /* @__PURE__ */ jsxs9(
976
1064
  "div",
977
1065
  {
978
1066
  className: `grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] ${className}`,
@@ -985,39 +1073,40 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
985
1073
  else controller.undo();
986
1074
  },
987
1075
  children: [
988
- /* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-col gap-2", children: [
1076
+ /* @__PURE__ */ jsxs9("div", { className: "flex min-w-0 flex-col gap-2", children: [
989
1077
  stage,
990
- /* @__PURE__ */ jsx10(PlayheadScrubber, { controller }),
991
- /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
992
- /* @__PURE__ */ jsx10("button", { type: "button", className: btn4, onClick: () => controller.undo(), disabled: !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
993
- /* @__PURE__ */ jsx10("button", { type: "button", className: btn4, onClick: () => controller.redo(), disabled: !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" }),
994
- onionSkin === void 0 && /* @__PURE__ */ jsxs8("label", { className: "flex items-center gap-1", children: [
995
- /* @__PURE__ */ jsx10("input", { type: "checkbox", checked: onion, onChange: (e) => setOnion(e.target.checked) }),
1078
+ /* @__PURE__ */ jsx11(PlayheadScrubber, { controller }),
1079
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
1080
+ /* @__PURE__ */ jsx11("button", { type: "button", className: btn5, onClick: () => controller.undo(), disabled: !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
1081
+ /* @__PURE__ */ jsx11("button", { type: "button", className: btn5, onClick: () => controller.redo(), disabled: !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" }),
1082
+ onionSkin === void 0 && /* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-1", children: [
1083
+ /* @__PURE__ */ jsx11("input", { type: "checkbox", checked: onion, onChange: (e) => setOnion(e.target.checked) }),
996
1084
  " Onion skin"
997
1085
  ] }),
998
- motionPaths === void 0 && /* @__PURE__ */ jsxs8("label", { className: "flex items-center gap-1", children: [
999
- /* @__PURE__ */ jsx10("input", { type: "checkbox", checked: paths, onChange: (e) => setPaths(e.target.checked) }),
1086
+ motionPaths === void 0 && /* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-1", children: [
1087
+ /* @__PURE__ */ jsx11("input", { type: "checkbox", checked: paths, onChange: (e) => setPaths(e.target.checked) }),
1000
1088
  " Motion paths"
1001
1089
  ] }),
1002
- /* @__PURE__ */ jsx10("span", { className: "ml-auto", "data-picked-element": pickedValid ?? void 0, children: pickedValid ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
1090
+ /* @__PURE__ */ jsx11("span", { className: "ml-auto", "data-picked-element": pickedValid ?? void 0, children: pickedValid ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
1003
1091
  "Picked ",
1004
- /* @__PURE__ */ jsx10("code", { className: "text-rose-500", children: pickedValid }),
1092
+ /* @__PURE__ */ jsx11("code", { className: "text-rose-500", children: pickedValid }),
1005
1093
  " ",
1006
- /* @__PURE__ */ jsx10("button", { type: "button", className: "underline", onClick: () => setPicked(null), children: "clear" })
1094
+ /* @__PURE__ */ jsx11("button", { type: "button", className: "underline", onClick: () => setPicked(null), children: "clear" })
1007
1095
  ] }) : "Click the stage to pick an element; shift-click to toggle it as a target." })
1008
1096
  ] }),
1009
1097
  issueList
1010
1098
  ] }),
1011
- /* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-col gap-3", children: [
1012
- /* @__PURE__ */ jsx10(CommandList, { controller, targets: pickedValid ? [pickedValid] : void 0 }),
1013
- /* @__PURE__ */ jsx10(CommandInspector, { controller, picked: pickedValid, className: "border-t border-border pt-2" }),
1014
- /* @__PURE__ */ jsxs8("details", { open: true, className: "border-t border-border pt-2", children: [
1015
- /* @__PURE__ */ jsx10("summary", { className: "cursor-pointer text-xs font-medium", children: "Shot" }),
1016
- /* @__PURE__ */ jsx10(ShotInspector, { controller, className: "mt-1" })
1099
+ /* @__PURE__ */ jsxs9("div", { className: "flex min-w-0 flex-col gap-3", children: [
1100
+ /* @__PURE__ */ jsx11(CommandList, { controller, targets: pickedValid ? [pickedValid] : void 0 }),
1101
+ (state.effectsProvider || controller.appliedEffects().length > 0) && /* @__PURE__ */ jsx11(AnimationEffectsPanel, { controller, node: pickedValid, className: "border-t border-border pt-2" }),
1102
+ /* @__PURE__ */ jsx11(CommandInspector, { controller, picked: pickedValid, className: "border-t border-border pt-2" }),
1103
+ /* @__PURE__ */ jsxs9("details", { open: true, className: "border-t border-border pt-2", children: [
1104
+ /* @__PURE__ */ jsx11("summary", { className: "cursor-pointer text-xs font-medium", children: "Shot" }),
1105
+ /* @__PURE__ */ jsx11(ShotInspector, { controller, className: "mt-1" })
1017
1106
  ] }),
1018
- /* @__PURE__ */ jsxs8("details", { className: "border-t border-border pt-2", children: [
1019
- /* @__PURE__ */ jsx10("summary", { className: "cursor-pointer text-xs font-medium", children: "Scene settings" }),
1020
- /* @__PURE__ */ jsx10(SceneSettings, { controller, className: "mt-1" })
1107
+ /* @__PURE__ */ jsxs9("details", { className: "border-t border-border pt-2", children: [
1108
+ /* @__PURE__ */ jsx11("summary", { className: "cursor-pointer text-xs font-medium", children: "Scene settings" }),
1109
+ /* @__PURE__ */ jsx11(SceneSettings, { controller, className: "mt-1" })
1021
1110
  ] })
1022
1111
  ] })
1023
1112
  ]
@@ -1026,6 +1115,7 @@ function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssue
1026
1115
  }
1027
1116
  export {
1028
1117
  AnimationEditor,
1118
+ AnimationEffectsPanel,
1029
1119
  AnimationStage,
1030
1120
  CommandInspector,
1031
1121
  CommandList,
@@ -1037,6 +1127,7 @@ export {
1037
1127
  useAnimationEditor,
1038
1128
  useAnimationEditorController,
1039
1129
  useAnimationEditorState,
1040
- useAnimationEditorTools
1130
+ useAnimationEditorTools,
1131
+ useAnimationEffectsTools
1041
1132
  };
1042
1133
  //# sourceMappingURL=index.js.map