@hyperframes/studio 0.7.71 → 0.7.73

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 (79) hide show
  1. package/dist/assets/{hyperframes-player-C1X90psg.js → hyperframes-player-DGlzhf_s.js} +1 -1
  2. package/dist/assets/index-B37rWXo4.css +1 -0
  3. package/dist/assets/{index-DzxDHPR1.js → index-CbVTOJ-E.js} +1 -1
  4. package/dist/assets/{index-nY4lLBqM.js → index-CcA5WVJv.js} +1 -1
  5. package/dist/assets/index-DXcLI2wu.js +428 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7762 -4261
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +4 -4
  12. package/src/components/StudioLeftSidebar.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +10 -4
  14. package/src/components/editor/AnimationCard.test.tsx +100 -0
  15. package/src/components/editor/AnimationCard.tsx +5 -2
  16. package/src/components/editor/EaseCurveSection.test.tsx +348 -0
  17. package/src/components/editor/EaseCurveSection.tsx +444 -237
  18. package/src/components/editor/EaseParamFields.test.tsx +188 -0
  19. package/src/components/editor/EaseParamFields.tsx +246 -0
  20. package/src/components/editor/PropertyPanel.test.tsx +9 -7
  21. package/src/components/editor/PropertyPanelFlat.tsx +75 -88
  22. package/src/components/editor/colorGradingFrameAnalysis.test.ts +101 -0
  23. package/src/components/editor/colorGradingFrameAnalysis.ts +203 -0
  24. package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
  25. package/src/components/editor/easeCurveSvg.tsx +65 -0
  26. package/src/components/editor/easePresetLibrary.test.ts +114 -0
  27. package/src/components/editor/easePresetLibrary.ts +49 -0
  28. package/src/components/editor/gsapAnimationConstants.ts +15 -13
  29. package/src/components/editor/propertyPanelColorCurveGraph.tsx +549 -0
  30. package/src/components/editor/propertyPanelColorCurves.test.tsx +158 -0
  31. package/src/components/editor/propertyPanelColorCurves.tsx +185 -0
  32. package/src/components/editor/propertyPanelColorGradingControls.tsx +115 -77
  33. package/src/components/editor/propertyPanelColorScopes.tsx +258 -0
  34. package/src/components/editor/propertyPanelColorSecondary.test.tsx +181 -0
  35. package/src/components/editor/propertyPanelColorSecondary.tsx +448 -0
  36. package/src/components/editor/propertyPanelColorWheels.test.tsx +119 -0
  37. package/src/components/editor/propertyPanelColorWheels.tsx +334 -0
  38. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +109 -0
  39. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +239 -41
  40. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +260 -300
  41. package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
  42. package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
  43. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
  44. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
  45. package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
  46. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
  47. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
  48. package/src/components/editor/propertyPanelGradingNumberField.test.tsx +79 -0
  49. package/src/components/editor/propertyPanelGradingNumberField.tsx +116 -0
  50. package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
  51. package/src/components/editor/propertyPanelTypes.ts +13 -0
  52. package/src/components/editor/useColorGradingController.test.ts +331 -10
  53. package/src/components/editor/useColorGradingController.ts +57 -90
  54. package/src/components/editor/useColorGradingPreviews.ts +350 -0
  55. package/src/components/editor/useColorGradingScopes.test.tsx +143 -0
  56. package/src/components/editor/useColorGradingScopes.ts +62 -0
  57. package/src/components/editor/useInspectorGestureTransaction.ts +30 -1
  58. package/src/components/nle/NLEContext.tsx +1 -1
  59. package/src/contexts/PanelLayoutContext.tsx +3 -6
  60. package/src/hooks/useBlockCatalog.ts +31 -13
  61. package/src/hooks/useBlockHandlers.ts +22 -0
  62. package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
  63. package/src/hooks/useGsapScriptCommits.ts +14 -1
  64. package/src/hooks/usePanelLayout.test.ts +68 -1
  65. package/src/hooks/usePanelLayout.ts +72 -34
  66. package/src/icons/SystemIcons.tsx +4 -0
  67. package/src/player/components/PlayerControls.tsx +68 -229
  68. package/src/player/components/ShortcutsPanel.tsx +2 -4
  69. package/src/player/components/SpeedMenu.tsx +1 -2
  70. package/src/telemetry/events.test.ts +9 -0
  71. package/src/telemetry/events.ts +5 -0
  72. package/src/utils/blockInstaller.test.ts +67 -0
  73. package/src/utils/blockInstaller.ts +162 -109
  74. package/src/utils/studioUiPreferences.test.ts +5 -0
  75. package/src/utils/studioUiPreferences.ts +8 -0
  76. package/dist/assets/index-CSCh2Vrp.js +0 -428
  77. package/dist/assets/index-Ceyz8Qt2.css +0 -1
  78. package/src/player/components/PlayerControls.test.ts +0 -20
  79. package/src/player/components/useSeekBarDrag.ts +0 -169
@@ -0,0 +1,108 @@
1
+ import { useState } from "react";
2
+ import type { RegistryItem } from "@hyperframes/core/registry";
3
+ import { useBlockCatalog } from "../../hooks/useBlockCatalog";
4
+ import { Film, Plus } from "../../icons/SystemIcons";
5
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
6
+ import type { DomEditSelection } from "./domEditing";
7
+ import { FLAT_PREVIEW_GRID } from "./propertyPanelFlatPrimitives";
8
+ import type { ElementTiming } from "./propertyPanelFlatTimingDerivation";
9
+
10
+ export const MEDIA_TREATMENT_OVERLAY_TAG = "media-treatment-overlay";
11
+
12
+ export function filterMediaTreatmentOverlays(items: readonly RegistryItem[]): RegistryItem[] {
13
+ return items.filter(
14
+ (item) =>
15
+ item.type === "hyperframes:block" &&
16
+ item.tags?.includes(MEDIA_TREATMENT_OVERLAY_TAG) === true,
17
+ );
18
+ }
19
+
20
+ export function deriveMediaOverlayPlacement(
21
+ element: Pick<DomEditSelection, "dataAttributes" | "sourceFile">,
22
+ timing: Pick<ElementTiming, "start" | "duration">,
23
+ ) {
24
+ const authoredTrack = Number.parseInt(element.dataAttributes["track-index"] ?? "", 10);
25
+ return {
26
+ start: timing.start,
27
+ ...(timing.duration > 0 ? { duration: timing.duration } : {}),
28
+ ...(Number.isFinite(authoredTrack) ? { track: authoredTrack + 1 } : {}),
29
+ compositionPath: element.sourceFile,
30
+ };
31
+ }
32
+
33
+ export function FlatOverlaysSection({
34
+ onAddOverlay,
35
+ }: {
36
+ onAddOverlay: (name: string) => Promise<void>;
37
+ }) {
38
+ const track = useTrackDesignInput();
39
+ const { blocks, loading, error } = useBlockCatalog();
40
+ const overlays = filterMediaTreatmentOverlays(blocks);
41
+ const [adding, setAdding] = useState<string | null>(null);
42
+ const [previewing, setPreviewing] = useState<string | null>(null);
43
+
44
+ if (loading) {
45
+ return <div className="py-4 text-center text-[10px] text-panel-text-4">Loading overlays…</div>;
46
+ }
47
+ if (error) {
48
+ return <div className="py-4 text-center text-[10px] text-red-300">{error}</div>;
49
+ }
50
+
51
+ const busy = adding !== null;
52
+ return (
53
+ <div data-flat-overlays="true" className={FLAT_PREVIEW_GRID}>
54
+ {overlays.map((overlay) => (
55
+ <button
56
+ key={overlay.name}
57
+ type="button"
58
+ data-flat-overlay={overlay.name}
59
+ aria-label={`Add ${overlay.title}`}
60
+ disabled={busy}
61
+ title={overlay.description}
62
+ onPointerEnter={() => setPreviewing(overlay.name)}
63
+ onPointerLeave={() => setPreviewing(null)}
64
+ onFocus={() => setPreviewing(overlay.name)}
65
+ onBlur={() => setPreviewing(null)}
66
+ onClick={() => {
67
+ track("button", `Add ${overlay.title}`);
68
+ setAdding(overlay.name);
69
+ void onAddOverlay(overlay.name).finally(() => setAdding(null));
70
+ }}
71
+ className="group min-w-0 overflow-hidden border border-panel-hairline bg-panel-bg-soft text-left transition-colors hover:border-panel-border-input hover:bg-panel-bg disabled:cursor-wait disabled:opacity-50"
72
+ >
73
+ <span className="relative flex aspect-video w-full items-center justify-center overflow-hidden bg-black/20">
74
+ {previewing === overlay.name && overlay.preview?.video ? (
75
+ <video
76
+ src={overlay.preview.video}
77
+ poster={overlay.preview.poster}
78
+ autoPlay
79
+ muted
80
+ loop
81
+ playsInline
82
+ className="block h-full w-full object-cover"
83
+ />
84
+ ) : overlay.preview?.poster ? (
85
+ <img
86
+ data-flat-overlay-preview={overlay.name}
87
+ src={overlay.preview.poster}
88
+ alt=""
89
+ draggable={false}
90
+ loading="lazy"
91
+ className="block h-full w-full object-cover"
92
+ />
93
+ ) : (
94
+ <Film size={15} className="text-panel-text-4" />
95
+ )}
96
+ <Plus
97
+ size={12}
98
+ className="absolute right-1.5 top-1.5 text-white opacity-70 drop-shadow group-hover:text-panel-accent group-hover:opacity-100"
99
+ />
100
+ </span>
101
+ <span className="block truncate px-2 py-1.5 text-[10px] text-panel-text-2">
102
+ {adding === overlay.name ? "Adding…" : overlay.title}
103
+ </span>
104
+ </button>
105
+ ))}
106
+ </div>
107
+ );
108
+ }
@@ -8,6 +8,8 @@ import {
8
8
  type PropertyValueTier,
9
9
  } from "./propertyPanelValueTier";
10
10
 
11
+ export const FLAT_PREVIEW_GRID = "grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-1";
12
+
11
13
  /* ------------------------------------------------------------------ */
12
14
  /* FlatRow — single-column label/value property row */
13
15
  /* ------------------------------------------------------------------ */
@@ -0,0 +1,79 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { GradingNumberField } from "./propertyPanelGradingNumberField";
7
+
8
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
+
10
+ afterEach(() => {
11
+ document.body.innerHTML = "";
12
+ });
13
+
14
+ function changeInput(input: HTMLInputElement, value: string) {
15
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
16
+ if (!setter) throw new Error("Expected the native input setter");
17
+ setter.call(input, value);
18
+ input.dispatchEvent(new Event("input", { bubbles: true }));
19
+ }
20
+
21
+ function renderField() {
22
+ const callbacks = {
23
+ onBegin: vi.fn(),
24
+ onPreview: vi.fn(),
25
+ onSettle: vi.fn(),
26
+ onCancel: vi.fn(),
27
+ };
28
+ const host = document.body.appendChild(document.createElement("div"));
29
+ const root = createRoot(host);
30
+ act(() =>
31
+ root.render(
32
+ <GradingNumberField label="Level" value={10} min={-100} max={100} {...callbacks} />,
33
+ ),
34
+ );
35
+ const input = host.querySelector("input");
36
+ if (!input) throw new Error("Expected a grading field");
37
+ return { root, input, callbacks };
38
+ }
39
+
40
+ describe("GradingNumberField", () => {
41
+ it("does not begin or settle an untouched focus and blur", () => {
42
+ const { root, input, callbacks } = renderField();
43
+ act(() => input.focus());
44
+ act(() => input.blur());
45
+
46
+ expect(callbacks.onBegin).not.toHaveBeenCalled();
47
+ expect(callbacks.onPreview).not.toHaveBeenCalled();
48
+ expect(callbacks.onSettle).not.toHaveBeenCalled();
49
+ expect(callbacks.onCancel).not.toHaveBeenCalled();
50
+ act(() => root.unmount());
51
+ });
52
+
53
+ it("cancels a real edit that returns to its baseline", () => {
54
+ const { root, input, callbacks } = renderField();
55
+ act(() => input.focus());
56
+ act(() => changeInput(input, "25"));
57
+ act(() => changeInput(input, "10"));
58
+ act(() => input.blur());
59
+
60
+ expect(callbacks.onBegin).toHaveBeenCalledOnce();
61
+ expect(callbacks.onSettle).not.toHaveBeenCalled();
62
+ expect(callbacks.onCancel).toHaveBeenCalledOnce();
63
+ act(() => root.unmount());
64
+ });
65
+
66
+ it("previews valid text and settles once on Enter", () => {
67
+ const { root, input, callbacks } = renderField();
68
+ act(() => input.focus());
69
+ act(() => changeInput(input, "-25"));
70
+ act(() => {
71
+ input.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
72
+ });
73
+
74
+ expect(callbacks.onBegin).toHaveBeenCalledOnce();
75
+ expect(callbacks.onPreview).toHaveBeenLastCalledWith(-25);
76
+ expect(callbacks.onSettle).toHaveBeenCalledOnce();
77
+ act(() => root.unmount());
78
+ });
79
+ });
@@ -0,0 +1,116 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { clampNumber } from "../../utils/studioHelpers";
3
+
4
+ export function GradingNumberField({
5
+ label,
6
+ ariaLabel = label,
7
+ value,
8
+ min,
9
+ max,
10
+ disabled,
11
+ formatValue = String,
12
+ labelClassName = "min-w-0",
13
+ labelTextClassName = "block",
14
+ inputClassName = "w-full",
15
+ onBegin,
16
+ onPreview,
17
+ onSettle,
18
+ onCancel,
19
+ }: {
20
+ label: string;
21
+ ariaLabel?: string;
22
+ value: number;
23
+ min: number;
24
+ max: number;
25
+ disabled?: boolean;
26
+ formatValue?: (value: number) => string;
27
+ labelClassName?: string;
28
+ labelTextClassName?: string;
29
+ inputClassName?: string;
30
+ onBegin: () => void;
31
+ onPreview: (value: number) => void;
32
+ onSettle: () => void;
33
+ onCancel: () => void;
34
+ }) {
35
+ const [draft, setDraft] = useState(() => formatValue(value));
36
+ const focusedRef = useRef(false);
37
+ const cancelBlurRef = useRef(false);
38
+ const baselineRef = useRef(value);
39
+ const dirtyRef = useRef(false);
40
+
41
+ useEffect(() => {
42
+ if (!focusedRef.current) setDraft(formatValue(value));
43
+ }, [formatValue, value]);
44
+
45
+ const settle = () => {
46
+ focusedRef.current = false;
47
+ if (cancelBlurRef.current) {
48
+ cancelBlurRef.current = false;
49
+ return;
50
+ }
51
+ const parsed = Number(draft);
52
+ if (draft.trim() === "" || !Number.isFinite(parsed)) {
53
+ setDraft(formatValue(value));
54
+ if (dirtyRef.current) onCancel();
55
+ dirtyRef.current = false;
56
+ return;
57
+ }
58
+ const next = clampNumber(parsed, min, max);
59
+ setDraft(formatValue(next));
60
+ if (!dirtyRef.current || Object.is(next, baselineRef.current)) {
61
+ if (dirtyRef.current) onCancel();
62
+ dirtyRef.current = false;
63
+ return;
64
+ }
65
+ onPreview(next);
66
+ onSettle();
67
+ dirtyRef.current = false;
68
+ };
69
+
70
+ return (
71
+ <label className={labelClassName}>
72
+ <span className={labelTextClassName}>{label}</span>
73
+ <input
74
+ type="text"
75
+ inputMode="decimal"
76
+ aria-label={ariaLabel}
77
+ value={draft}
78
+ disabled={disabled}
79
+ onFocus={() => {
80
+ focusedRef.current = true;
81
+ baselineRef.current = value;
82
+ dirtyRef.current = false;
83
+ setDraft(formatValue(value));
84
+ }}
85
+ onChange={(event) => {
86
+ const next = event.target.value;
87
+ setDraft(next);
88
+ if (next.trim() === "") return;
89
+ const parsed = Number(next);
90
+ if (!Number.isFinite(parsed)) return;
91
+ const clamped = clampNumber(parsed, min, max);
92
+ if (!Object.is(clamped, baselineRef.current) && !dirtyRef.current) {
93
+ dirtyRef.current = true;
94
+ onBegin();
95
+ }
96
+ if (dirtyRef.current) onPreview(clamped);
97
+ }}
98
+ onBlur={settle}
99
+ onKeyDown={(event) => {
100
+ if (event.key === "Escape") {
101
+ event.preventDefault();
102
+ cancelBlurRef.current = true;
103
+ focusedRef.current = false;
104
+ setDraft(formatValue(value));
105
+ if (dirtyRef.current) onCancel();
106
+ dirtyRef.current = false;
107
+ event.currentTarget.blur();
108
+ } else if (event.key === "Enter") {
109
+ event.currentTarget.blur();
110
+ }
111
+ }}
112
+ className={`border-b border-panel-border-input/50 bg-transparent py-0.5 text-right font-mono text-[9px] text-panel-text-2 outline-none focus:border-panel-accent disabled:opacity-40 ${inputClassName}`}
113
+ />
114
+ </label>
115
+ );
116
+ }
@@ -0,0 +1,36 @@
1
+ import type { NormalizedHfColorGrading } from "@hyperframes/core/color-grading";
2
+ import type { ColorGradingPreviewOptions } from "./useColorGradingController";
3
+
4
+ export function presetPreviewHandlers({
5
+ id,
6
+ label,
7
+ resolve,
8
+ onPreview,
9
+ onCommit,
10
+ onTrack,
11
+ }: {
12
+ id: string;
13
+ label: string;
14
+ resolve: () => NormalizedHfColorGrading;
15
+ onPreview: (
16
+ grading: NormalizedHfColorGrading | null,
17
+ options?: ColorGradingPreviewOptions,
18
+ ) => void;
19
+ onCommit: (grading: NormalizedHfColorGrading) => void;
20
+ onTrack: (label: string) => void;
21
+ }) {
22
+ return {
23
+ title: `Preview ${label}`,
24
+ onPointerEnter: () =>
25
+ onPreview(resolve(), {
26
+ animatedPreview: { kind: "presets" as const, id },
27
+ }),
28
+ onPointerLeave: () => onPreview(null),
29
+ onFocus: () => onPreview(resolve()),
30
+ onBlur: () => onPreview(null),
31
+ onClick: () => {
32
+ onTrack(label);
33
+ onCommit(resolve());
34
+ },
35
+ };
36
+ }
@@ -19,6 +19,18 @@ export interface BackgroundRemovalResult {
19
19
  provider?: string;
20
20
  }
21
21
 
22
+ export interface MediaOverlayPlacement {
23
+ start: number;
24
+ duration?: number;
25
+ track?: number;
26
+ compositionPath?: string;
27
+ }
28
+
29
+ export type AddMediaOverlayHandler = (
30
+ blockName: string,
31
+ placement: MediaOverlayPlacement,
32
+ ) => Promise<void>;
33
+
22
34
  export interface PropertyPanelProps {
23
35
  projectId: string;
24
36
  projectDir: string | null;
@@ -69,6 +81,7 @@ export interface PropertyPanelProps {
69
81
  onAskAgent: () => void;
70
82
  onToggleElementHidden?: (elementKey: string, hidden: boolean) => void | Promise<void>;
71
83
  onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
84
+ onAddMediaOverlay?: AddMediaOverlayHandler;
72
85
  fontAssets?: ImportedFontAsset[];
73
86
  onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
74
87
  previewIframeRef?: RefObject<HTMLIFrameElement | null>;