@pixerate/editor-react 0.44.0 → 0.45.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,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/animation-editor/index.ts
@@ -22,6 +32,12 @@ var animation_editor_exports = {};
22
32
  __export(animation_editor_exports, {
23
33
  AnimationEditor: () => AnimationEditor,
24
34
  AnimationStage: () => AnimationStage,
35
+ CommandInspector: () => CommandInspector,
36
+ CommandList: () => CommandList,
37
+ EasePicker: () => EasePicker,
38
+ PlayheadScrubber: () => PlayheadScrubber,
39
+ SceneSettings: () => SceneSettings,
40
+ ShotInspector: () => ShotInspector,
25
41
  ShotTimeline: () => ShotTimeline,
26
42
  useAnimationEditor: () => useAnimationEditor,
27
43
  useAnimationEditorController: () => useAnimationEditorController,
@@ -77,22 +93,28 @@ function useAnimationEditorTools(registry, controller, options = {}) {
77
93
 
78
94
  // src/animation-editor/AnimationStage.tsx
79
95
  var import_react2 = require("react");
96
+ var import_animation_editor2 = require("@pixerate/editor/animation-editor");
80
97
  var import_jsx_runtime = require("react/jsx-runtime");
81
- function AnimationStage({ controller, values, onionSkin, motionPaths, className = "" }) {
98
+ function AnimationStage({ controller, values, onionSkin, motionPaths, onPick, highlight, className = "" }) {
82
99
  const state = useAnimationEditorState(controller);
83
100
  const { scene, time } = state;
101
+ const key = highlight?.join("\0") ?? "";
84
102
  const svg = (0, import_react2.useMemo)(
85
- () => controller.toSvg(time, { pretty: false, idPrefix: "ae-", values, onionSkin, motionPaths }).replace(/<svg ([^>]*?)width="[^"]*" height="[^"]*"/, '<svg $1width="100%" height="100%"'),
86
- [controller, scene, time, values, onionSkin, motionPaths]
103
+ () => (0, import_animation_editor2.highlightMotionSvg)(
104
+ controller.toSvg(time, { pretty: false, idPrefix: "ae-", values, onionSkin, motionPaths }).replace(/<svg ([^>]*?)width="[^"]*" height="[^"]*"/, '<svg $1width="100%" height="100%"'),
105
+ highlight ?? []
106
+ ),
107
+ [controller, scene, time, values, onionSkin, motionPaths, key]
87
108
  );
88
109
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
89
110
  "div",
90
111
  {
91
- className: `relative overflow-hidden ${className}`,
112
+ className: `relative overflow-hidden ${onPick ? "cursor-crosshair" : ""} ${className}`,
92
113
  style: { aspectRatio: `${scene.artwork.width} / ${scene.artwork.height}` },
93
114
  "data-animation-stage": "",
94
115
  role: "img",
95
116
  "aria-label": `${scene.name} at ${(time / 1e3).toFixed(2)} s`,
117
+ onClick: onPick ? (e) => onPick((0, import_animation_editor2.motionNodeAtElement)(scene, e.target, e.currentTarget), e) : void 0,
96
118
  dangerouslySetInnerHTML: { __html: svg }
97
119
  }
98
120
  );
@@ -169,20 +191,849 @@ function ShotTimeline({ controller, className = "" }) {
169
191
  }
170
192
 
171
193
  // src/animation-editor/AnimationEditor.tsx
172
- var import_animation_editor2 = require("@pixerate/editor/animation-editor");
194
+ var import_react9 = require("react");
195
+ var import_animation_editor9 = require("@pixerate/editor/animation-editor");
196
+
197
+ // src/animation-editor/CommandInspector.tsx
198
+ var import_react4 = __toESM(require("react"), 1);
199
+ var import_animation_editor4 = require("@pixerate/editor/animation-editor");
200
+
201
+ // src/animation-editor/EasePicker.tsx
202
+ var import_react3 = require("react");
203
+ var import_animation_editor3 = require("@pixerate/editor/animation-editor");
173
204
  var import_jsx_runtime3 = require("react/jsx-runtime");
174
- function AnimationEditor({ controller, values, onionSkin, motionPaths, showIssues = true, className = "" }) {
175
- const { issues } = useAnimationEditorState(controller);
176
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
177
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AnimationStage, { controller, values, onionSkin, motionPaths, className: "rounded-md border border-border bg-muted/40" }),
178
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ShotTimeline, { controller }),
179
- showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "space-y-0.5 text-xs", "aria-label": "Animation issues", children: issues.map((issue, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", children: (0, import_animation_editor2.motionIssueLine)(issue) }, i)) })
205
+ var W = 72;
206
+ var H = 44;
207
+ function EasePicker({ value, onChange, disabled, className = "" }) {
208
+ const key = (0, import_animation_editor3.motionEaseKey)(value);
209
+ const curve = (0, import_react3.useMemo)(() => (0, import_animation_editor3.motionEasingCurve)(value ?? "ease-out"), [value]);
210
+ const guides = (0, import_animation_editor3.motionEasingGuides)(curve, H);
211
+ const spring = typeof value === "string" ? value.startsWith("spring") : value?.type === "spring";
212
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex items-center gap-2 ${className}`, "data-ease-picker": "", children: [
213
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
214
+ "select",
215
+ {
216
+ className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
217
+ "aria-label": "Ease",
218
+ value: key,
219
+ disabled,
220
+ onChange: (e) => {
221
+ if (e.target.value === "custom") return;
222
+ onChange(e.target.value ? e.target.value : void 0);
223
+ },
224
+ children: [
225
+ import_animation_editor3.MOTION_EASE_CHOICES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: c.value, children: c.label }, c.value)),
226
+ key === "custom" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "custom", children: "Custom" })
227
+ ]
228
+ }
229
+ ),
230
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: W, height: H, viewBox: `0 0 ${W} ${H}`, className: "shrink-0 rounded border border-border", "data-ease-curve": key || "default", "aria-hidden": "true", children: [
231
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: 0, x2: W, y1: guides.zero, y2: guides.zero, stroke: "currentColor", strokeOpacity: 0.2 }),
232
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: 0, x2: W, y1: guides.one, y2: guides.one, stroke: "currentColor", strokeOpacity: 0.2, strokeDasharray: "2 2" }),
233
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: (0, import_animation_editor3.motionEasingPath)(curve, W, H), fill: "none", stroke: "#f43f5e", strokeWidth: 1.5 })
234
+ ] }),
235
+ spring && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "text-[10px] tabular-nums text-muted-foreground", children: [
236
+ Math.round(curve.durationMs),
237
+ " ms"
238
+ ] })
239
+ ] });
240
+ }
241
+
242
+ // src/animation-editor/CommitInput.tsx
243
+ var import_jsx_runtime4 = require("react/jsx-runtime");
244
+ function CommitInput({ value, onCommit, ...rest }) {
245
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
246
+ "input",
247
+ {
248
+ ...rest,
249
+ defaultValue: value,
250
+ onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(),
251
+ onBlur: (e) => {
252
+ if (e.currentTarget.value !== value) onCommit(e.currentTarget.value);
253
+ }
254
+ },
255
+ value
256
+ );
257
+ }
258
+
259
+ // src/animation-editor/CommandInspector.tsx
260
+ var import_jsx_runtime5 = require("react/jsx-runtime");
261
+ var input = "w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs";
262
+ function CommandInspector({ controller, picked, className = "" }) {
263
+ const state = useAnimationEditorState(controller);
264
+ const { scene, selection, readOnly, error } = state;
265
+ const [local, setLocalState] = (0, import_react4.useState)(null);
266
+ const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
267
+ const elements = (0, import_react4.useMemo)(() => (0, import_animation_editor4.describeMotionElements)(scene), [scene.artwork]);
268
+ const shot = selection ? scene.shots.find((s) => s.id === selection.shot) : void 0;
269
+ const index = selection?.command;
270
+ const command = shot && index !== void 0 ? shot.run[index] : void 0;
271
+ if (!shot || index === void 0 || !command) {
272
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: `text-xs text-muted-foreground ${className}`, "data-command-inspector": "", children: "Select a command to edit it." });
273
+ }
274
+ const values = command;
275
+ const fields = (0, import_animation_editor4.motionCommandFields)(command.type);
276
+ const primitive = (0, import_animation_editor4.motionFlairPrimitive)(command);
277
+ const commit = (field, raw) => {
278
+ const edit = (0, import_animation_editor4.motionFieldEdit)(shot.id, index, field, raw);
279
+ if ("error" in edit) return setLocal(edit.error);
280
+ setLocal(null);
281
+ controller.apply(edit, `Set ${field.label.toLowerCase()}`);
282
+ };
283
+ const name = (id) => elements.find((e) => e.id === id)?.name ?? id;
284
+ const targets = values.targets ?? [];
285
+ const message = (local && local.revision === state.revision ? local.text : null) ?? error;
286
+ const control = (field) => {
287
+ const v = values[field.name];
288
+ const common = { disabled: readOnly, "aria-label": field.label, title: field.description };
289
+ switch (field.kind) {
290
+ case "targets":
291
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-wrap items-center gap-1", "data-targets": "", children: [
292
+ targets.map((id) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex items-center gap-0.5 rounded bg-muted px-1 text-[11px]", "data-target": id, children: [
293
+ name(id),
294
+ !readOnly && targets.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", "aria-label": `Remove ${id}`, onClick: () => commit(field, targets.filter((t) => t !== id)), children: "\xD7" })
295
+ ] }, id)),
296
+ !readOnly && picked && !targets.includes(picked) && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { type: "button", className: "rounded border border-rose-500 px-1 text-[11px] text-rose-500", onClick: () => commit(field, [...targets, picked]), "data-add-picked": "", children: [
297
+ "+ ",
298
+ name(picked)
299
+ ] }),
300
+ !readOnly && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-[11px]", "aria-label": "Add target", value: "", onChange: (e) => e.target.value && commit(field, [...targets, e.target.value]), children: [
301
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "+ target\u2026" }),
302
+ elements.filter((e) => !targets.includes(e.id)).map((e) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: e.id, children: e.name ? `${e.name} (${e.id})` : e.id }, e.id))
303
+ ] })
304
+ ] });
305
+ case "node":
306
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("select", { className: input, ...common, value: typeof v === "string" ? v : "", onChange: (e) => commit(field, e.target.value), children: [
307
+ !field.required && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "\u2014" }),
308
+ elements.filter((e) => field.name !== "path" || e.type === "path").map((e) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("option", { value: e.id, children: [
309
+ e.name ? `${e.name} (${e.id})` : e.id,
310
+ e.hidden ? " \xB7 hidden" : ""
311
+ ] }, e.id))
312
+ ] });
313
+ case "enum":
314
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("select", { className: input, ...common, value: typeof v === "string" ? v : "", onChange: (e) => commit(field, e.target.value), children: [
315
+ !field.required && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "default" }),
316
+ field.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: o, children: o }, o))
317
+ ] });
318
+ case "boolean":
319
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "checkbox", ...common, checked: v === true, onChange: (e) => commit(field, e.target.checked) });
320
+ case "ease":
321
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EasePicker, { value: v, disabled: readOnly, onChange: (ease) => commit(field, ease === void 0 ? "" : ease) });
322
+ case "color":
323
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-1", children: [
324
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "color", disabled: readOnly, "aria-label": `${field.label} swatch`, value: typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v) ? v : "#000000", onChange: (e) => commit(field, e.target.value) }),
325
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CommitInput, { className: input, ...common, value: typeof v === "string" ? v : "", placeholder: "unchanged", onCommit: (raw) => commit(field, raw) })
326
+ ] });
327
+ case "point": {
328
+ const p = v ?? void 0;
329
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex items-center gap-1", children: ["x", "y"].map((axis) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
330
+ CommitInput,
331
+ {
332
+ className: input,
333
+ type: "number",
334
+ disabled: readOnly,
335
+ "aria-label": `${field.label} ${axis}`,
336
+ placeholder: axis,
337
+ value: p ? String(p[axis]) : "",
338
+ onCommit: (raw) => commit(field, axis === "x" ? { x: raw, y: p ? String(p.y) : raw ? "0" : "" } : { x: p ? String(p.x) : raw ? "0" : "", y: raw })
339
+ },
340
+ axis
341
+ )) });
342
+ }
343
+ case "scale":
344
+ case "number":
345
+ case "integer":
346
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-1", children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
348
+ CommitInput,
349
+ {
350
+ className: input,
351
+ type: "number",
352
+ ...common,
353
+ min: field.min,
354
+ max: field.max,
355
+ step: field.kind === "integer" ? 1 : field.unit === "ms" ? 50 : field.unit === "0\u20131" ? 0.05 : "any",
356
+ placeholder: "default",
357
+ value: typeof v === "number" ? String(v) : "",
358
+ onCommit: (raw) => commit(field, raw)
359
+ }
360
+ ),
361
+ field.unit && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "shrink-0 text-[10px] text-muted-foreground", children: field.unit })
362
+ ] });
363
+ default:
364
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CommitInput, { className: input, ...common, value: typeof v === "string" ? v : "", onCommit: (raw) => commit(field, raw) });
365
+ }
366
+ };
367
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `flex flex-col gap-1.5 text-xs ${className}`, "data-command-inspector": "", "data-command-type": command.type, children: [
368
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
369
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "font-medium", children: [
370
+ index + 1,
371
+ ". ",
372
+ command.type
373
+ ] }),
374
+ primitive ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { className: "rounded bg-muted px-1 text-[10px]", "data-flair-primitive": primitive, title: "The Flair motion primitive this command plays", children: primitive }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "editor-only (no Flair primitive)" })
375
+ ] }),
376
+ ["main", "timing"].map((group) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("fieldset", { className: "grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1", children: [
377
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("legend", { className: "mb-1 text-[10px] uppercase tracking-wide text-muted-foreground", children: group === "main" ? "Command" : "Timing" }),
378
+ fields.filter((f) => f.group === group).map((field) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react4.default.Fragment, { children: [
379
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "truncate text-muted-foreground", title: field.description, children: [
380
+ field.label,
381
+ field.required ? " *" : ""
382
+ ] }),
383
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "data-field": field.name, children: control(field) })
384
+ ] }, field.name))
385
+ ] }, group)),
386
+ message && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
387
+ ] });
388
+ }
389
+
390
+ // src/animation-editor/CommandList.tsx
391
+ var import_react5 = require("react");
392
+ var import_animation_editor5 = require("@pixerate/editor/animation-editor");
393
+ var import_jsx_runtime6 = require("react/jsx-runtime");
394
+ var btn = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
395
+ function CommandList({ controller, shot, targets, className = "" }) {
396
+ const state = useAnimationEditorState(controller);
397
+ const { scene, selection, readOnly, error } = state;
398
+ const shotId = shot ?? selection?.shot ?? scene.shots[0]?.id;
399
+ const current = scene.shots.find((s) => s.id === shotId);
400
+ const [type, setType] = (0, import_react5.useState)("enter");
401
+ const [local, setLocalState] = (0, import_react5.useState)(null);
402
+ const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
403
+ if (!current) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: `text-xs text-muted-foreground ${className}`, "data-command-list": "", children: "Add a shot to start." });
404
+ const rows = (0, import_animation_editor5.motionCommandRows)(scene, current.id);
405
+ const selected = selection?.shot === current.id ? selection.command : void 0;
406
+ const run = (edit, label) => {
407
+ setLocal(null);
408
+ return controller.apply(edit, label);
409
+ };
410
+ const add = () => {
411
+ const from = targets?.length ? targets : selected !== void 0 ? current.run[selected].targets ?? [] : [];
412
+ const command = (0, import_animation_editor5.defaultMotionCommand)(type, from, scene);
413
+ if ("error" in command) return setLocal(command.error);
414
+ const at = selected !== void 0 ? selected + 1 : current.run.length;
415
+ const result = run({ op: "add_commands", shot: current.id, commands: [command], at }, `Add ${type}`);
416
+ if (result.ok) controller.select({ shot: current.id, command: at });
417
+ };
418
+ const message = (local && local.revision === state.revision ? local.text : null) ?? error;
419
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col gap-1 text-xs ${className}`, "data-command-list": "", "data-for-shot": current.id, children: [
420
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "font-medium", children: [
421
+ "Commands in ",
422
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-rose-500", children: current.label ?? current.id })
423
+ ] }),
424
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className: "flex flex-col gap-1", "aria-label": "Commands", children: rows.map((row) => {
425
+ const c = row.command;
426
+ const isSelected = row.index === selected;
427
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
428
+ "li",
429
+ {
430
+ className: `flex flex-wrap items-center gap-1 rounded border px-1.5 py-1 ${isSelected ? "border-rose-500 bg-rose-500/10" : "border-border"}`,
431
+ "data-command": row.index,
432
+ "data-selected": isSelected || void 0,
433
+ onClick: () => controller.select({ shot: current.id, command: row.index }),
434
+ children: [
435
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "min-w-0 flex-1 truncate", title: row.summary, children: [
436
+ row.index + 1,
437
+ ". ",
438
+ row.summary
439
+ ] }),
440
+ row.flairPrimitive && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { className: "rounded bg-muted px-1 text-[10px] text-muted-foreground", "data-flair-primitive": row.flairPrimitive, children: row.flairPrimitive }),
441
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "tabular-nums text-[10px] text-muted-foreground", children: [
442
+ ((row.end - row.start) / 1e3).toFixed(2),
443
+ " s"
444
+ ] }),
445
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex w-full items-center gap-1", onClick: (e) => e.stopPropagation(), children: [
446
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
447
+ "select",
448
+ {
449
+ className: "rounded border border-border bg-transparent px-1 py-0.5 text-[11px]",
450
+ "aria-label": "Starts",
451
+ value: c.with ?? "after",
452
+ disabled: readOnly || row.index === 0,
453
+ onChange: (e) => run(
454
+ e.target.value === "after" ? { op: "update_command", shot: current.id, command: row.index, remove: ["with"] } : { op: "update_command", shot: current.id, command: row.index, set: { with: e.target.value } },
455
+ "Change start"
456
+ ),
457
+ children: [
458
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "after", children: "after previous" }),
459
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "previous", children: "with previous" })
460
+ ]
461
+ }
462
+ ),
463
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-0.5 text-muted-foreground", children: [
464
+ "+",
465
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
466
+ "input",
467
+ {
468
+ className: "w-14 rounded border border-border bg-transparent px-1 py-0.5 text-[11px] tabular-nums",
469
+ type: "number",
470
+ min: 0,
471
+ step: 50,
472
+ "aria-label": "Delay",
473
+ placeholder: "0",
474
+ defaultValue: c.delay ?? "",
475
+ disabled: readOnly,
476
+ onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(),
477
+ onBlur: (e) => {
478
+ const raw = e.currentTarget.value.trim();
479
+ if (raw === String(c.delay ?? "")) return;
480
+ const n = Number(raw);
481
+ if (raw && !(Number.isFinite(n) && n >= 0)) return setLocal("Delay must be a number of ms, 0 or more.");
482
+ run(raw && n > 0 ? { op: "update_command", shot: current.id, command: row.index, set: { delay: n } } : { op: "update_command", shot: current.id, command: row.index, remove: ["delay"] }, "Change delay");
483
+ }
484
+ },
485
+ String(c.delay ?? "")
486
+ ),
487
+ "ms"
488
+ ] }),
489
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "ml-auto flex gap-0.5", children: [
490
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn, "aria-label": "Move up", disabled: readOnly || row.index === 0, onClick: () => run({ op: "move_command", shot: current.id, command: row.index, to: row.index - 1 }, "Move command").ok && controller.select({ shot: current.id, command: row.index - 1 }), children: "\u2191" }),
491
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn, "aria-label": "Move down", disabled: readOnly || row.index === rows.length - 1, onClick: () => run({ op: "move_command", shot: current.id, command: row.index, to: row.index + 1 }, "Move command").ok && controller.select({ shot: current.id, command: row.index + 1 }), children: "\u2193" }),
492
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn, "aria-label": "Duplicate", disabled: readOnly, onClick: () => run((0, import_animation_editor5.duplicateMotionCommandEdit)(scene, current.id, row.index), "Duplicate command").ok && controller.select({ shot: current.id, command: row.index + 1 }), children: "\u29C9" }),
493
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn, "aria-label": "Delete", disabled: readOnly, onClick: () => run({ op: "delete_commands", shot: current.id, commands: [row.index] }, "Delete command"), children: "\u2715" })
494
+ ] })
495
+ ] })
496
+ ]
497
+ },
498
+ `${row.index}:${row.summary}`
499
+ );
500
+ }) }),
501
+ !readOnly && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-1", children: [
502
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { className: "flex-1 rounded border border-border bg-transparent px-1 py-0.5", "aria-label": "New command type", value: type, onChange: (e) => setType(e.target.value), children: import_animation_editor5.MOTION_COMMAND_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: t, children: t }, t)) }),
503
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn, onClick: add, "data-add-command": "", children: "Add command" })
504
+ ] }),
505
+ message && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
506
+ ] });
507
+ }
508
+
509
+ // src/animation-editor/PlayheadScrubber.tsx
510
+ var import_react6 = require("react");
511
+ var import_animation_editor6 = require("@pixerate/editor/animation-editor");
512
+ var import_jsx_runtime7 = require("react/jsx-runtime");
513
+ var seconds2 = (ms) => `${(ms / 1e3).toFixed(2)} s`;
514
+ function worst2(issues) {
515
+ if (issues.some((i) => i.severity === "error")) return "error";
516
+ if (issues.some((i) => i.severity === "warning")) return "warning";
517
+ return null;
518
+ }
519
+ function PlayheadScrubber({ controller, className = "" }) {
520
+ const state = useAnimationEditorState(controller);
521
+ const { duration, shots, time, playing, selection, issues, scene, readOnly } = state;
522
+ const span = Math.max(1, duration);
523
+ const track = (0, import_react6.useRef)(null);
524
+ const [width, setWidth] = (0, import_react6.useState)(600);
525
+ (0, import_react6.useEffect)(() => {
526
+ const el = track.current;
527
+ if (!el || typeof ResizeObserver === "undefined") return;
528
+ const observer = new ResizeObserver(() => setWidth(el.clientWidth || 600));
529
+ observer.observe(el);
530
+ return () => observer.disconnect();
531
+ }, []);
532
+ const ticks = (0, import_animation_editor6.motionRulerTicks)(duration, width);
533
+ const pct = (ms) => `${Math.min(span, Math.max(0, ms)) / span * 100}%`;
534
+ const seekAt = (clientX) => {
535
+ const box = track.current.getBoundingClientRect();
536
+ controller.seek(Math.min(1, Math.max(0, (clientX - box.left) / Math.max(1, box.width))) * span);
537
+ };
538
+ const beats = [];
539
+ if (scene.beat && scene.beat.bpm > 0) {
540
+ for (let b = 0, t = (0, import_animation_editor6.motionBeatTime)(scene.beat, 0); t <= span && beats.length < 400; t = (0, import_animation_editor6.motionBeatTime)(scene.beat, ++b)) if (t >= 0) beats.push(t);
541
+ }
542
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-1 ${className}`, "data-playhead-scrubber": "", "data-shot-timeline": "", children: [
543
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
544
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "rounded border border-border px-2 py-0.5", onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", disabled: !shots.length, children: playing ? "Pause" : "Play" }),
545
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "rounded border border-border px-2 py-0.5", onClick: () => {
546
+ controller.pause();
547
+ controller.seek(0);
548
+ }, "aria-label": "Back to start", children: "\u23EE" }),
549
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "flex items-center gap-1 text-muted-foreground", children: [
550
+ "Loop",
551
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
552
+ "select",
553
+ {
554
+ className: "rounded border border-border bg-transparent px-1 py-0.5",
555
+ value: scene.loop ?? "none",
556
+ disabled: readOnly,
557
+ "aria-label": "Loop",
558
+ onChange: (e) => controller.apply({ op: "set_settings", loop: e.target.value }, "Change loop"),
559
+ children: [
560
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "none", children: "Off" }),
561
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "loop", children: "Loop" }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "ping-pong", children: "Ping-pong" })
563
+ ]
564
+ }
565
+ )
566
+ ] }),
567
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ml-auto tabular-nums text-muted-foreground", "data-time": "", children: [
568
+ seconds2(time),
569
+ " / ",
570
+ seconds2(duration)
571
+ ] })
572
+ ] }),
573
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
574
+ "div",
575
+ {
576
+ ref: track,
577
+ className: "relative cursor-ew-resize select-none touch-none",
578
+ role: "slider",
579
+ "aria-label": "Playhead",
580
+ "aria-valuemin": 0,
581
+ "aria-valuemax": duration,
582
+ "aria-valuenow": Math.round(time),
583
+ tabIndex: 0,
584
+ "data-scrub-track": "",
585
+ onPointerDown: (e) => {
586
+ e.currentTarget.setPointerCapture?.(e.pointerId);
587
+ seekAt(e.clientX);
588
+ },
589
+ onPointerMove: (e) => {
590
+ if (e.buttons & 1) seekAt(e.clientX);
591
+ },
592
+ onKeyDown: (e) => {
593
+ const step = e.shiftKey ? 1e3 : 100;
594
+ if (e.key === "ArrowRight") controller.seek(time + step);
595
+ else if (e.key === "ArrowLeft") controller.seek(time - step);
596
+ else if (e.key === "Home") controller.seek(0);
597
+ else if (e.key === "End") controller.seek(duration);
598
+ else if (e.key === " ") controller.toggle();
599
+ else return;
600
+ e.preventDefault();
601
+ },
602
+ children: [
603
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "relative h-5 border-b border-border text-[10px] text-muted-foreground", "data-ruler": "", children: [
604
+ beats.map((t, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "absolute bottom-0 h-1.5 w-px bg-sky-500/60", style: { left: pct(t) }, "data-beat": i }, `b${i}`)),
605
+ scene.narration?.words.map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "absolute top-0 h-1.5 w-px bg-emerald-500/70", style: { left: pct(w.start) }, title: `\u201C${w.text}\u201D at ${seconds2(w.start)}`, "data-word": i }, `w${i}`)),
606
+ ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: pct(t.time) }, children: t.label && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "absolute -top-0.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time))
607
+ ] }),
608
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-1 flex w-full gap-px", role: "group", "aria-label": "Shots", children: shots.map((shot, i) => {
609
+ const mark = worst2(issues.filter((x) => x.shot === shot.id));
610
+ const selected = selection?.shot === shot.id;
611
+ const cue = scene.shots[i]?.at;
612
+ const cueTime = cue ? (0, import_animation_editor6.motionCueTime)(scene, cue) : void 0;
613
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
614
+ "button",
615
+ {
616
+ type: "button",
617
+ className: `relative min-w-0 truncate rounded border px-1 py-1 text-left text-[11px] ${selected ? "border-rose-500 bg-rose-500/10" : "border-border bg-muted/40"} ${state.currentShot === shot.id ? "font-semibold" : ""}`,
618
+ style: { flexGrow: Math.max(1, shot.end - shot.start), flexBasis: 0 },
619
+ title: `${shot.label ?? shot.id} \xB7 ${seconds2(shot.start)}\u2013${seconds2(shot.end)}${cueTime !== void 0 ? ` \xB7 cued at ${seconds2(cueTime)}` : ""}`,
620
+ "data-shot": shot.id,
621
+ "data-issue": mark ?? void 0,
622
+ onPointerDown: (e) => e.stopPropagation(),
623
+ onClick: () => {
624
+ controller.select({ shot: shot.id });
625
+ controller.seekShot(shot.id);
626
+ },
627
+ children: [
628
+ mark && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: mark === "error" ? "text-red-500" : "text-amber-500", children: "\u25CF " }),
629
+ shot.label ?? shot.id
630
+ ]
631
+ },
632
+ shot.id
633
+ );
634
+ }) }),
635
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: pct(time) }, "data-playhead": "" })
636
+ ]
637
+ }
638
+ )
639
+ ] });
640
+ }
641
+
642
+ // src/animation-editor/SceneSettings.tsx
643
+ var import_react7 = require("react");
644
+ var import_animation_editor7 = require("@pixerate/editor/animation-editor");
645
+ var import_jsx_runtime8 = require("react/jsx-runtime");
646
+ var input2 = "w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs";
647
+ var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
648
+ var unit = (u) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "shrink-0 text-[10px] text-muted-foreground", children: u });
649
+ function SceneSettings({ controller, className = "" }) {
650
+ const state = useAnimationEditorState(controller);
651
+ const { scene, readOnly, error, duration } = state;
652
+ const [local, setLocalState] = (0, import_react7.useState)(null);
653
+ const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
654
+ const [fitTo, setFitTo] = (0, import_react7.useState)("");
655
+ const apply = (edit, label) => {
656
+ setLocal(null);
657
+ return controller.apply(edit, label);
658
+ };
659
+ const settings = (set, label) => apply({ op: "set_settings", ...set }, label);
660
+ const num = (raw, what, ok, then) => {
661
+ if (raw.trim() === "") return then(null);
662
+ const n = Number(raw);
663
+ if (!Number.isFinite(n) || !ok(n)) return setLocal(what);
664
+ then(n);
665
+ };
666
+ const target = scene.target;
667
+ const fitSeconds = fitTo || String(target ? (target[0] + target[1]) / 2 : Math.round(duration / 100) / 10);
668
+ const fit = () => {
669
+ const s = Number(fitSeconds);
670
+ if (!(Number.isFinite(s) && s > 0)) return setLocal("Give the length to fit in seconds, above 0.");
671
+ try {
672
+ const edits = (0, import_animation_editor7.fitMotionSceneEdits)(scene, s * 1e3);
673
+ if (edits.length) apply(edits, `Fit to ${s} s`);
674
+ else setLocal(null);
675
+ } catch (e) {
676
+ setLocal(e.message);
677
+ }
678
+ };
679
+ const beat = scene.beat;
680
+ const message = (local && local.revision === state.revision ? local.text : null) ?? error;
681
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-1.5 text-xs ${className}`, "data-scene-settings": "", children: [
682
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1", children: [
683
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "text-muted-foreground", children: "Loop" }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("select", { className: input2, "aria-label": "Scene loop", disabled: readOnly, value: scene.loop ?? "none", onChange: (e) => settings({ loop: e.target.value }, "Change loop"), children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "none", children: "Off" }),
686
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "loop", children: "Loop" }),
687
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "ping-pong", children: "Ping-pong" })
688
+ ] }),
689
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "text-muted-foreground", children: "Frame rate" }),
690
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
691
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
692
+ CommitInput,
693
+ {
694
+ className: input2,
695
+ type: "number",
696
+ min: 1,
697
+ max: 120,
698
+ "aria-label": "Frame rate",
699
+ placeholder: "30",
700
+ disabled: readOnly,
701
+ value: scene.fps !== void 0 ? String(scene.fps) : "",
702
+ onCommit: (raw) => num(raw, "Frame rate must be 1 to 120 fps.", (n) => n >= 1 && n <= 120, (n) => n !== null && settings({ fps: n }, "Change frame rate"))
703
+ }
704
+ ),
705
+ unit("fps")
706
+ ] }),
707
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "text-muted-foreground", title: "Hold changes on a grid of this many frames a second (12 = on twos); empty is smooth", children: "Steps" }),
708
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
709
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
710
+ CommitInput,
711
+ {
712
+ className: input2,
713
+ type: "number",
714
+ min: 1,
715
+ max: 120,
716
+ "aria-label": "Steps",
717
+ placeholder: "smooth",
718
+ disabled: readOnly,
719
+ value: scene.steps !== void 0 ? String(scene.steps) : "",
720
+ onCommit: (raw) => num(raw, "Steps must be above 0 and at most 120 fps, or empty for smooth.", (n) => n > 0 && n <= 120, (n) => settings({ steps: n }, "Change steps"))
721
+ }
722
+ ),
723
+ unit("fps")
724
+ ] }),
725
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "text-muted-foreground", children: "Target length" }),
726
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
727
+ [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
728
+ CommitInput,
729
+ {
730
+ className: input2,
731
+ type: "number",
732
+ min: 0,
733
+ step: 0.5,
734
+ "aria-label": i ? "Target max" : "Target min",
735
+ placeholder: i ? "max" : "min",
736
+ disabled: readOnly,
737
+ value: target ? String(target[i]) : "",
738
+ onCommit: (raw) => num(raw, "Target lengths are seconds, 0 or more.", (n) => n >= 0, (n) => {
739
+ if (n === null) return settings({ target: null }, "Clear target length");
740
+ const next = target ? [...target] : [n, n];
741
+ next[i] = n;
742
+ if (next[0] > next[1]) return setLocal("The shortest target length must not be longer than the longest.");
743
+ settings({ target: next }, "Change target length");
744
+ })
745
+ },
746
+ i
747
+ )),
748
+ unit("s")
749
+ ] }),
750
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "text-muted-foreground", children: "Fit to" }),
751
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
752
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { className: input2, type: "number", min: 0.1, step: 0.5, "aria-label": "Fit to", value: fitSeconds, onChange: (e) => setFitTo(e.target.value) }),
753
+ unit("s"),
754
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: btn2, disabled: readOnly || !scene.shots.length, onClick: fit, "data-fit": "", title: "Change the shots' speeds so the scene lasts this long", children: "Fit" })
755
+ ] }),
756
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { className: "text-muted-foreground", children: "Beat grid" }),
757
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", "data-beat-grid": "", children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
759
+ CommitInput,
760
+ {
761
+ className: input2,
762
+ type: "number",
763
+ min: 1,
764
+ "aria-label": "BPM",
765
+ placeholder: "bpm",
766
+ disabled: readOnly,
767
+ value: beat ? String(beat.bpm) : "",
768
+ onCommit: (raw) => num(raw, "BPM must be above 0.", (n) => n > 0, (n) => settings({ beat: n === null ? null : { ...beat ?? {}, bpm: n } }, n === null ? "Clear beat grid" : "Change tempo"))
769
+ }
770
+ ),
771
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
772
+ CommitInput,
773
+ {
774
+ className: input2,
775
+ type: "number",
776
+ "aria-label": "Beat offset",
777
+ placeholder: "offset ms",
778
+ disabled: readOnly || !beat,
779
+ value: beat?.offset !== void 0 ? String(beat.offset) : "",
780
+ onCommit: (raw) => beat && num(raw, "The offset is a number of ms.", () => true, (n) => settings({ beat: { ...beat, offset: n ?? 0 } }, "Change beat offset"))
781
+ }
782
+ ),
783
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
784
+ CommitInput,
785
+ {
786
+ className: input2,
787
+ type: "number",
788
+ min: 1,
789
+ step: 1,
790
+ "aria-label": "Meter",
791
+ placeholder: "4/bar",
792
+ disabled: readOnly || !beat,
793
+ value: beat?.meter !== void 0 ? String(beat.meter) : "",
794
+ onCommit: (raw) => beat && num(raw, "The meter is a whole number of beats a bar.", (n) => Number.isInteger(n) && n >= 1, (n) => settings({ beat: { ...beat, meter: n ?? 4 } }, "Change meter"))
795
+ }
796
+ )
797
+ ] })
798
+ ] }),
799
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "flex flex-col gap-0.5", children: [
800
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-muted-foreground", children: "Narration (a word a line: start ms, then the word)" }),
801
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
802
+ "textarea",
803
+ {
804
+ className: `${input2} h-20 font-mono`,
805
+ "aria-label": "Narration",
806
+ placeholder: "0 Meet\n320 the\n480 badge",
807
+ disabled: readOnly,
808
+ defaultValue: (0, import_animation_editor7.formatMotionNarration)(scene.narration),
809
+ onBlur: (e) => {
810
+ if (e.currentTarget.value === (0, import_animation_editor7.formatMotionNarration)(scene.narration)) return;
811
+ const parsed = (0, import_animation_editor7.parseMotionNarration)(e.currentTarget.value);
812
+ if ("error" in parsed) return setLocal(parsed.error);
813
+ settings({ narration: parsed.narration }, parsed.narration ? "Change narration" : "Clear narration");
814
+ }
815
+ },
816
+ (0, import_animation_editor7.formatMotionNarration)(scene.narration)
817
+ )
818
+ ] }),
819
+ message && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
820
+ ] });
821
+ }
822
+
823
+ // src/animation-editor/ShotInspector.tsx
824
+ var import_react8 = require("react");
825
+ var import_animation_editor8 = require("@pixerate/editor/animation-editor");
826
+ var import_jsx_runtime9 = require("react/jsx-runtime");
827
+ var input3 = "w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs";
828
+ var btn3 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
829
+ function ShotInspector({ controller, shot, className = "" }) {
830
+ const state = useAnimationEditorState(controller);
831
+ const { scene, selection, readOnly, error } = state;
832
+ const [local, setLocalState] = (0, import_react8.useState)(null);
833
+ const setLocal = (text) => setLocalState(text ? { text, revision: state.revision } : null);
834
+ const elements = (0, import_react8.useMemo)(() => (0, import_animation_editor8.describeMotionElements)(scene), [scene.artwork]);
835
+ const cues = (0, import_react8.useMemo)(() => (0, import_animation_editor8.motionCueChoices)(scene), [scene]);
836
+ const id = shot ?? selection?.shot ?? scene.shots[0]?.id;
837
+ const index = scene.shots.findIndex((s) => s.id === id);
838
+ const current = scene.shots[index];
839
+ const apply = (edit, label) => {
840
+ setLocal(null);
841
+ return controller.apply(edit, label);
842
+ };
843
+ const addShot = () => {
844
+ const newId = (0, import_animation_editor8.freshShotId)(scene, "shot");
845
+ const at = index >= 0 ? index + 1 : scene.shots.length;
846
+ if (apply({ op: "insert_shots", shots: [{ id: newId, label: "New shot", run: [] }], at }, "Add shot").ok) {
847
+ controller.select({ shot: newId });
848
+ controller.seekShot(newId);
849
+ }
850
+ };
851
+ const number = (key, raw, label) => {
852
+ const n = Number(raw);
853
+ if (raw.trim() === "") return apply({ op: "update_shot", shot: current.id, remove: [key] }, label);
854
+ if (!Number.isFinite(n) || n < 0 || key === "speed" && n <= 0) return setLocal(key === "speed" ? "Speed must be a number above 0 (1 = as written)." : "Hold must be a number of ms, 0 or more.");
855
+ apply({ op: "update_shot", shot: current.id, set: { [key]: n } }, label);
856
+ };
857
+ const message = (local && local.revision === state.revision ? local.text : null) ?? error;
858
+ const cueKey = current ? (0, import_animation_editor8.motionCueKey)(current.at) : "";
859
+ const frame = current?.camera && "frame" in current.camera ? current.camera.frame : "";
860
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: `flex flex-col gap-1.5 text-xs ${className}`, "data-shot-inspector": "", children: [
861
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center gap-1", children: [
862
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "font-medium", children: "Shot" }),
863
+ current && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { className: "text-[10px] text-muted-foreground", children: current.id }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ml-auto flex gap-0.5", children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: btn3, disabled: readOnly, onClick: addShot, "data-add-shot": "", children: "+ Shot" }),
866
+ current && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
867
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: btn3, "aria-label": "Move shot earlier", disabled: readOnly || index === 0, onClick: () => apply({ op: "move_shot", shot: current.id, to: index - 1 }, "Move shot"), children: "\u2190" }),
868
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: btn3, "aria-label": "Move shot later", disabled: readOnly || index === scene.shots.length - 1, onClick: () => apply({ op: "move_shot", shot: current.id, to: index + 1 }, "Move shot"), children: "\u2192" }),
869
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: btn3, "aria-label": "Delete shot", disabled: readOnly, onClick: () => apply({ op: "delete_shots", shots: [current.id] }, "Delete shot"), children: "\u2715" })
870
+ ] })
871
+ ] })
872
+ ] }),
873
+ current && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "grid grid-cols-[6.5rem_1fr] items-center gap-x-2 gap-y-1", children: [
874
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { className: "text-muted-foreground", children: "Label" }),
875
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CommitInput, { className: input3, "aria-label": "Shot label", disabled: readOnly, value: current.label ?? "", onCommit: (raw) => apply(raw.trim() ? { op: "update_shot", shot: current.id, set: { label: raw.trim() } } : { op: "update_shot", shot: current.id, remove: ["label"] }, "Rename shot") }),
876
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { className: "text-muted-foreground", children: "Hold" }),
877
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center gap-1", children: [
878
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CommitInput, { className: input3, type: "number", min: 0, step: 50, "aria-label": "Hold", placeholder: "600", disabled: readOnly, value: current.hold !== void 0 ? String(current.hold) : "", onCommit: (raw) => number("hold", raw, "Change hold") }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "ms" })
880
+ ] }),
881
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { className: "text-muted-foreground", children: "Speed" }),
882
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center gap-1", children: [
883
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CommitInput, { className: input3, type: "number", min: 0.05, step: 0.05, "aria-label": "Speed", placeholder: "1", disabled: readOnly, value: current.speed !== void 0 ? String(current.speed) : "", onCommit: (raw) => number("speed", raw, "Change speed") }),
884
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "\xD7" })
885
+ ] }),
886
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { className: "text-muted-foreground", children: "Camera" }),
887
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
888
+ "select",
889
+ {
890
+ className: input3,
891
+ "aria-label": "Camera frames",
892
+ disabled: readOnly,
893
+ value: current.camera ? frame ? `frame:${frame}` : "rect" : "",
894
+ onChange: (e) => {
895
+ const v = e.target.value;
896
+ if (!v) apply({ op: "update_shot", shot: current.id, remove: ["camera"] }, "Clear camera");
897
+ else if (v.startsWith("frame:")) apply({ op: "update_shot", shot: current.id, set: { camera: { frame: v.slice(6), padding: 24 } } }, "Frame camera");
898
+ },
899
+ children: [
900
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: "keep framing" }),
901
+ current.camera && !frame && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "rect", children: "a rectangle" }),
902
+ elements.map((e) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("option", { value: `frame:${e.id}`, children: [
903
+ "frame ",
904
+ e.name ?? e.id
905
+ ] }, e.id))
906
+ ]
907
+ }
908
+ ),
909
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { className: "text-muted-foreground", children: "Starts on" }),
910
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
911
+ "select",
912
+ {
913
+ className: input3,
914
+ "aria-label": "Cue",
915
+ disabled: readOnly || index === 0,
916
+ value: cueKey,
917
+ onChange: (e) => {
918
+ const cue = (0, import_animation_editor8.motionCueFromKey)(e.target.value);
919
+ apply(cue ? { op: "update_shot", shot: current.id, set: { at: cue } } : { op: "update_shot", shot: current.id, remove: ["at"] }, "Cue shot");
920
+ },
921
+ children: [
922
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: "after the shot before" }),
923
+ cueKey && !cues.some((c) => c.value === cueKey) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: cueKey, children: cueKey }),
924
+ cues.map((c) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: c.value, children: c.label }, c.value))
925
+ ]
926
+ }
927
+ ),
928
+ !cues.length && index > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "col-span-2 text-[10px] text-muted-foreground", children: "Add a beat grid or narration in the scene settings to cue shots to them." })
929
+ ] }),
930
+ message && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-red-500", role: "alert", "data-edit-error": "", children: message })
180
931
  ] });
181
932
  }
933
+
934
+ // src/animation-editor/AnimationEditor.tsx
935
+ var import_jsx_runtime10 = require("react/jsx-runtime");
936
+ 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 = "" }) {
939
+ const state = useAnimationEditorState(controller);
940
+ const { issues, scene, selection, readOnly } = state;
941
+ const [picked, setPicked] = (0, import_react9.useState)(null);
942
+ const [onion, setOnion] = (0, import_react9.useState)(false);
943
+ const [paths, setPaths] = (0, import_react9.useState)(false);
944
+ const command = selection?.command !== void 0 ? scene.shots.find((s) => s.id === selection.shot)?.run[selection.command] : void 0;
945
+ const targets = command && "targets" in command ? command.targets : [];
946
+ const nodes = (0, import_react9.useMemo)(() => (0, import_animation_editor9.indexMotionNodes)(scene.artwork.nodes), [scene.artwork]);
947
+ const pickedValid = picked && nodes.has(picked) ? picked : null;
948
+ const onPick = (id, event) => {
949
+ if (event.shiftKey && id && command && "targets" in command && selection?.command !== void 0 && !readOnly) {
950
+ const next = targets.includes(id) ? targets.filter((t) => t !== id) : [...targets, id];
951
+ if (next.length) controller.apply({ op: "update_command", shot: selection.shot, command: selection.command, set: { targets: next } }, next.length > targets.length ? "Add target" : "Remove target");
952
+ return;
953
+ }
954
+ setPicked(id);
955
+ };
956
+ const stage = /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
957
+ AnimationStage,
958
+ {
959
+ controller,
960
+ values,
961
+ onionSkin: onionSkin ?? (onion ? ONION : void 0),
962
+ motionPaths: motionPaths ?? paths,
963
+ onPick: editable ? onPick : void 0,
964
+ highlight: editable ? [...pickedValid ? [pickedValid] : [], ...targets] : void 0,
965
+ className: "rounded-md border border-border bg-muted/40"
966
+ }
967
+ );
968
+ 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)) });
969
+ if (!editable)
970
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: `flex flex-col gap-2 ${className}`, "data-animation-editor": "", children: [
971
+ stage,
972
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PlayheadScrubber, { controller }),
973
+ issueList
974
+ ] });
975
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
976
+ "div",
977
+ {
978
+ className: `grid gap-3 lg:grid-cols-[minmax(0,1fr)_20rem] ${className}`,
979
+ "data-animation-editor": "",
980
+ onKeyDown: (e) => {
981
+ const el = e.target;
982
+ if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== "z" || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) return;
983
+ e.preventDefault();
984
+ if (e.shiftKey) controller.redo();
985
+ else controller.undo();
986
+ },
987
+ children: [
988
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
989
+ stage,
990
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PlayheadScrubber, { controller }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
992
+ /* @__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" }),
993
+ /* @__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" }),
994
+ onionSkin === void 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex items-center gap-1", children: [
995
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "checkbox", checked: onion, onChange: (e) => setOnion(e.target.checked) }),
996
+ " Onion skin"
997
+ ] }),
998
+ motionPaths === void 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex items-center gap-1", children: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "checkbox", checked: paths, onChange: (e) => setPaths(e.target.checked) }),
1000
+ " Motion paths"
1001
+ ] }),
1002
+ /* @__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: [
1003
+ "Picked ",
1004
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { className: "text-rose-500", children: pickedValid }),
1005
+ " ",
1006
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "underline", onClick: () => setPicked(null), children: "clear" })
1007
+ ] }) : "Click the stage to pick an element; shift-click to toggle it as a target." })
1008
+ ] }),
1009
+ issueList
1010
+ ] }),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-col gap-3", children: [
1012
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(CommandList, { controller, targets: pickedValid ? [pickedValid] : void 0 }),
1013
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(CommandInspector, { controller, picked: pickedValid, className: "border-t border-border pt-2" }),
1014
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { open: true, className: "border-t border-border pt-2", children: [
1015
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("summary", { className: "cursor-pointer text-xs font-medium", children: "Shot" }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShotInspector, { controller, className: "mt-1" })
1017
+ ] }),
1018
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { className: "border-t border-border pt-2", children: [
1019
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("summary", { className: "cursor-pointer text-xs font-medium", children: "Scene settings" }),
1020
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SceneSettings, { controller, className: "mt-1" })
1021
+ ] })
1022
+ ] })
1023
+ ]
1024
+ }
1025
+ );
1026
+ }
182
1027
  // Annotate the CommonJS export names for ESM import in node:
183
1028
  0 && (module.exports = {
184
1029
  AnimationEditor,
185
1030
  AnimationStage,
1031
+ CommandInspector,
1032
+ CommandList,
1033
+ EasePicker,
1034
+ PlayheadScrubber,
1035
+ SceneSettings,
1036
+ ShotInspector,
186
1037
  ShotTimeline,
187
1038
  useAnimationEditor,
188
1039
  useAnimationEditorController,