@hyperframes/studio 0.7.107 → 0.7.109

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 (126) hide show
  1. package/dist/assets/{hyperframes-player-BLrfwq5o.js → hyperframes-player-BYgqAPib.js} +1 -1
  2. package/dist/assets/{index-BSBk5srs.js → index-Blxia-Lt.js} +1 -1
  3. package/dist/assets/{index-BA9yhzfR.js → index-CIRxodzK.js} +1 -1
  4. package/dist/assets/index-CfBGQdrX.js +428 -0
  5. package/dist/assets/index-DNkh9mbV.css +1 -0
  6. package/dist/index.d.ts +85 -2
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +17696 -12512
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/components/StudioRightPanel.tsx +18 -1
  12. package/src/components/TimelineToolbar.test.tsx +46 -0
  13. package/src/components/TimelineToolbar.tsx +17 -0
  14. package/src/components/editor/PropertyPanel.test.tsx +5 -1
  15. package/src/components/editor/PropertyPanel.tsx +7 -23
  16. package/src/components/editor/PropertyPanelFlat.tsx +25 -0
  17. package/src/components/editor/audioFxSummary.test.ts +75 -0
  18. package/src/components/editor/audioFxSummary.ts +36 -0
  19. package/src/components/editor/audioFxTelemetry.test.ts +129 -0
  20. package/src/components/editor/audioFxTelemetry.ts +230 -0
  21. package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
  22. package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
  23. package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
  24. package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
  25. package/src/components/editor/propertyPanelAutomation.ts +97 -0
  26. package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
  27. package/src/components/editor/propertyPanelFlatProps.ts +1 -0
  28. package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
  29. package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
  30. package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
  31. package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
  32. package/src/components/editor/propertyPanelFxControls.tsx +356 -0
  33. package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
  34. package/src/components/editor/propertyPanelFxFamily.ts +83 -0
  35. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
  36. package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
  37. package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
  38. package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
  39. package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
  40. package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
  41. package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
  42. package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
  43. package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
  44. package/src/components/editor/propertyPanelFxSection.tsx +550 -0
  45. package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
  46. package/src/components/editor/propertyPanelTypes.ts +5 -0
  47. package/src/components/editor/useFxAudition.ts +95 -0
  48. package/src/components/editor/useFxCarve.ts +0 -0
  49. package/src/components/editor/useFxChainObserved.ts +83 -0
  50. package/src/components/editor/useFxLevelling.ts +235 -0
  51. package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
  52. package/src/components/editor/useVolumeAutomation.ts +54 -0
  53. package/src/contexts/DomEditContext.tsx +11 -0
  54. package/src/hooks/useAppHotkeys.test.ts +198 -0
  55. package/src/hooks/useAppHotkeys.ts +30 -3
  56. package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
  57. package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
  58. package/src/hooks/useDomEditAttributeCommits.ts +90 -10
  59. package/src/hooks/useDomEditCommits.test.tsx +38 -0
  60. package/src/hooks/useDomEditCommits.ts +2 -0
  61. package/src/hooks/useDomEditSession.ts +2 -0
  62. package/src/hooks/useDomEditTextCommits.ts +2 -0
  63. package/src/hooks/useElementLifecycleOps.ts +3 -0
  64. package/src/hooks/useLivePlayheadTime.ts +49 -0
  65. package/src/hooks/usePreviewPersistence.ts +7 -0
  66. package/src/hooks/useTimelineEditing.ts +3 -0
  67. package/src/player/components/AutomationSelectionMenu.tsx +76 -0
  68. package/src/player/components/AutomationValueInput.tsx +51 -0
  69. package/src/player/components/LayerDisclosureRow.tsx +5 -4
  70. package/src/player/components/Timeline.test.ts +84 -0
  71. package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
  72. package/src/player/components/TimelineAutomationLane.tsx +674 -0
  73. package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
  74. package/src/player/components/TimelineLanes.test.tsx +24 -5
  75. package/src/player/components/TimelineLanes.tsx +142 -154
  76. package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
  77. package/src/player/components/TimelineTrackHeader.tsx +166 -6
  78. package/src/player/components/TimelineTrackRow.tsx +8 -1
  79. package/src/player/components/automationClipboard.test.ts +94 -0
  80. package/src/player/components/automationClipboard.ts +136 -0
  81. package/src/player/components/automationGestureKeys.test.ts +49 -0
  82. package/src/player/components/automationGestureKeys.ts +49 -0
  83. package/src/player/components/automationLaneData.test.ts +270 -0
  84. package/src/player/components/automationLaneData.ts +258 -0
  85. package/src/player/components/automationLaneDragMath.ts +194 -0
  86. package/src/player/components/automationLaneGeometry.test.ts +239 -0
  87. package/src/player/components/automationLaneGeometry.ts +287 -0
  88. package/src/player/components/automationLaneHeight.ts +15 -0
  89. package/src/player/components/automationLanePointer.ts +20 -0
  90. package/src/player/components/automationLaneSelection.test.ts +183 -0
  91. package/src/player/components/automationLaneSelection.ts +135 -0
  92. package/src/player/components/automationShapes.test.ts +82 -0
  93. package/src/player/components/automationShapes.ts +70 -0
  94. package/src/player/components/automationSimplify.test.ts +48 -0
  95. package/src/player/components/automationSimplify.ts +51 -0
  96. package/src/player/components/timelineClipGestureHandlers.ts +223 -0
  97. package/src/player/components/timelineLaneProps.ts +19 -0
  98. package/src/player/components/timelineLayout.ts +11 -5
  99. package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
  100. package/src/player/components/useAutomationEdgeStretch.ts +269 -0
  101. package/src/player/components/useAutomationLaneGestures.ts +537 -0
  102. package/src/player/components/useAutomationLanes.test.tsx +112 -0
  103. package/src/player/components/useAutomationLanes.ts +170 -0
  104. package/src/player/components/useAutomationRangeDrag.ts +114 -0
  105. package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
  106. package/src/player/components/useTimelineTrackLayout.ts +83 -7
  107. package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
  108. package/src/player/hooks/useTimelinePlayer.ts +13 -1
  109. package/src/player/lib/automationStoreSync.test.ts +83 -0
  110. package/src/player/lib/automationStoreSync.ts +56 -0
  111. package/src/player/lib/timelineDOM.test.ts +33 -0
  112. package/src/player/lib/timelineDOM.ts +12 -0
  113. package/src/player/store/automationSelectionSlice.test.ts +30 -0
  114. package/src/player/store/automationSelectionSlice.ts +54 -0
  115. package/src/player/store/playerStore.test.ts +15 -0
  116. package/src/player/store/playerStore.ts +54 -65
  117. package/src/player/store/timelineElement.ts +78 -0
  118. package/src/styles/studio.css +89 -0
  119. package/src/telemetry/agentRuntime.test.ts +45 -0
  120. package/src/telemetry/agentRuntime.ts +62 -0
  121. package/src/telemetry/system.ts +8 -0
  122. package/src/utils/studioFileHistory.test.ts +49 -0
  123. package/src/utils/studioFileHistory.ts +17 -1
  124. package/src/utils/studioTelemetry.ts +16 -10
  125. package/dist/assets/index-dF-CmLZu.js +0 -428
  126. package/dist/assets/index-zQ4JFwwB.css +0 -1
@@ -14,6 +14,7 @@ import {
14
14
  import { trackStudioEvent } from "../utils/studioTelemetry";
15
15
  import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapUndoRestore";
16
16
  import { usePlayerStore } from "../player";
17
+ import { syncStoredAutomationFromPreview } from "../player/lib/automationStoreSync";
17
18
 
18
19
  /** The restore payload the undo/redo preview-sync consumes (from the history store). */
19
20
  interface HistoryPreviewRestore {
@@ -207,7 +208,13 @@ export function usePreviewPersistence({
207
208
  player.setElements([]);
208
209
  player.setSelectedElementId(null);
209
210
  player.setTimelineReady(false);
211
+ return;
210
212
  }
213
+ // A soft restore patched the reverted attributes onto the live preview, but the
214
+ // player store keeps its own copy and that copy is what the automation lanes
215
+ // draw — so without this an undone envelope edit stayed invisible until a
216
+ // reload. The full path above clears the store and waits for discovery instead.
217
+ syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
211
218
  },
212
219
  [previewIframeRef, activeCompPathRef, reloadPreview],
213
220
  );
@@ -449,6 +449,9 @@ export function useTimelineEditing({
449
449
  kind: "timeline",
450
450
  files: { [targetPath]: patchedContent },
451
451
  readFile: async () => originalContent,
452
+ // remove-element already wrote the removal, so disk holds THAT — not the
453
+ // content read at the top. Undo still goes back to the original.
454
+ diskContent: { [targetPath]: removedContent },
452
455
  writeFile: writeProjectFile,
453
456
  recordEdit,
454
457
  });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Context menu for a right-click inside an automation time selection: the four
3
+ * utility shapes, then Simplify. Portal + dismiss handling mirror
4
+ * TrackGapContextMenu; rows never vanish — an inapplicable Simplify dims with
5
+ * a reason instead of leaving a shorter menu.
6
+ */
7
+ import { memo } from "react";
8
+ import { createPortal } from "react-dom";
9
+ import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
10
+ import { AUTOMATION_SHAPES, type AutomationShapeId } from "./automationShapes";
11
+
12
+ interface AutomationSelectionMenuProps {
13
+ x: number;
14
+ y: number;
15
+ onClose(): void;
16
+ onInsertShape(shape: AutomationShapeId): void;
17
+ onSimplify(): void;
18
+ /** At least three points in the range — fewer has nothing to thin. */
19
+ canSimplify: boolean;
20
+ }
21
+
22
+ export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
23
+ x,
24
+ y,
25
+ onClose,
26
+ onInsertShape,
27
+ onSimplify,
28
+ canSimplify,
29
+ }: AutomationSelectionMenuProps) {
30
+ const menuRef = useContextMenuDismiss(onClose);
31
+ const row =
32
+ "block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-bg-3 disabled:opacity-40";
33
+ // Same edge-clamping precedent as TrackGapContextMenu: without it a
34
+ // right-click near the bottom/right of the timeline renders this menu
35
+ // partially off-screen.
36
+ const menuWidth = 140;
37
+ const menuHeight = AUTOMATION_SHAPES.length * 24 + 32;
38
+ const overflowY = y + menuHeight - window.innerHeight;
39
+ const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x;
40
+ const adjustedY = overflowY > 0 ? y - overflowY - 8 : y;
41
+ return createPortal(
42
+ <div
43
+ ref={menuRef}
44
+ className="hf-automation-menu fixed z-50 min-w-[140px] rounded border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
45
+ style={{ left: adjustedX, top: adjustedY }}
46
+ >
47
+ {AUTOMATION_SHAPES.map((shape) => (
48
+ <button
49
+ key={shape.id}
50
+ type="button"
51
+ className={row}
52
+ onClick={() => {
53
+ onInsertShape(shape.id);
54
+ onClose();
55
+ }}
56
+ >
57
+ {shape.label}
58
+ </button>
59
+ ))}
60
+ <div className="my-1 border-t border-panel-border-input" />
61
+ <button
62
+ type="button"
63
+ className={row}
64
+ disabled={!canSimplify}
65
+ title={canSimplify ? undefined : "Fewer than three points in the selection"}
66
+ onClick={() => {
67
+ onSimplify();
68
+ onClose();
69
+ }}
70
+ >
71
+ Simplify
72
+ </button>
73
+ </div>,
74
+ document.body,
75
+ );
76
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Typing an exact value into a breakpoint.
3
+ *
4
+ * Dragging is how an envelope is shaped, but it cannot land on a number: -6.0 dB
5
+ * is not a pixel you can find. This is the lane's keyboard route to one value,
6
+ * kept in its own file so the lane component stays about the pointer.
7
+ */
8
+
9
+ export interface AutomationValueInputProps {
10
+ text: string;
11
+ /** Left edge in the lane's own coordinates. */
12
+ leftPx: number;
13
+ label: string;
14
+ onChange(text: string): void;
15
+ /** Enter or blur: apply what was typed. */
16
+ onCommit(): void;
17
+ /** Escape: leave the point where it was. */
18
+ onCancel(): void;
19
+ }
20
+
21
+ export function AutomationValueInput({
22
+ text,
23
+ leftPx,
24
+ label,
25
+ onChange,
26
+ onCommit,
27
+ onCancel,
28
+ }: AutomationValueInputProps) {
29
+ return (
30
+ <input
31
+ // pointer-events-auto: the lane band around it takes none, so that clips
32
+ // sharing the row do not cover each other's envelopes (see the lane).
33
+ className="hf-automation-value pointer-events-auto absolute rounded-[3px] border border-panel-border-input bg-panel-bg-2 px-1 font-mono text-[9px] text-panel-text-1"
34
+ style={{ left: leftPx, top: 1, width: 44, zIndex: 4 }}
35
+ // The lane is a pointer surface, and the timeline above it owns single-key
36
+ // shortcuts. Without stopping both, a press lands on the lane instead of
37
+ // the field and a typed "5" scrubs the transport.
38
+ onPointerDown={(e) => e.stopPropagation()}
39
+ onKeyDown={(e) => {
40
+ e.stopPropagation();
41
+ if (e.key === "Enter") onCommit();
42
+ if (e.key === "Escape") onCancel();
43
+ }}
44
+ onChange={(e) => onChange(e.target.value)}
45
+ onBlur={onCommit}
46
+ value={text}
47
+ aria-label={`${label} value`}
48
+ autoFocus
49
+ />
50
+ );
51
+ }
@@ -1,12 +1,11 @@
1
1
  import { CaretRight } from "@phosphor-icons/react";
2
- import type { TimelineElement } from "../store/playerStore";
3
2
  import { TRACK_H } from "./timelineLayout";
4
3
  import { TrackClipCount } from "./TrackClipCount";
5
4
 
6
5
  // Layer row (Figma order: disclosure ▸/▾, diamond, name) — the disclosure lives
7
6
  // here, not on the clip bar, and re-expands a collapsed layer.
8
7
  export function LayerDisclosureRow({
9
- keyframeClip,
8
+ name,
10
9
  clipCount,
11
10
  isExpanded,
12
11
  gutterBackground,
@@ -15,7 +14,10 @@ export function LayerDisclosureRow({
15
14
  onToggleClipExpanded,
16
15
  children,
17
16
  }: {
18
- keyframeClip: TimelineElement;
17
+ /** What this row is called. The active clip's own name when it is alone on the
18
+ * track; the track itself once it holds several, since naming a shared row
19
+ * after one of its clips reads as if the rows under it were that clip's. */
20
+ name: string;
19
21
  clipCount: number;
20
22
  isExpanded: boolean;
21
23
  gutterBackground: string;
@@ -31,7 +33,6 @@ export function LayerDisclosureRow({
31
33
  /** Trailing controls that act on the LAYER (the visibility eye), not on a lane. */
32
34
  children?: React.ReactNode;
33
35
  }) {
34
- const name = keyframeClip.label ?? keyframeClip.domId ?? keyframeClip.id;
35
36
  return (
36
37
  <div
37
38
  className="absolute left-0 top-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
@@ -37,6 +37,7 @@ import {
37
37
  getTimelineLaneTop,
38
38
  createTimelineRowGeometry,
39
39
  } from "./timelineLayout";
40
+ import { AUTOMATION_LANE_H } from "./automationLaneHeight";
40
41
  import { formatTime } from "../lib/time";
41
42
  import { usePlayerStore } from "../store/playerStore";
42
43
  import { TimelineEditProvider } from "../../contexts/TimelineEditContext";
@@ -578,6 +579,89 @@ describe("Timeline provider boundary", () => {
578
579
  act(() => root.unmount());
579
580
  });
580
581
 
582
+ // The caret belongs to the row, not to whichever clip on it is selected: the
583
+ // automation lanes below it are the track's, shared per property. Toggling one
584
+ // clip left the row's state depending on the selection, and a collapse that
585
+ // only dropped the active clip left the row stuck open.
586
+ it("expands and collapses every clip on a shared track together", () => {
587
+ const host = createSizedTimelineHost(720);
588
+ const automation = JSON.stringify({
589
+ version: 1,
590
+ lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }],
591
+ });
592
+ usePlayerStore.setState({
593
+ duration: 8,
594
+ timelineReady: true,
595
+ elements: [
596
+ { id: "narration-1", tag: "audio", start: 0, duration: 4, track: 0, automation },
597
+ { id: "narration-2", tag: "audio", start: 4, duration: 4, track: 0, automation },
598
+ ],
599
+ });
600
+ const root = createRoot(host);
601
+ act(() => root.render(React.createElement(Timeline)));
602
+
603
+ const row = host.querySelector<HTMLElement>('[data-el-id="narration-1"]')?.parentElement
604
+ ?.parentElement;
605
+ // A row of several clips is named for the track, so the caret is too.
606
+ const caret = () => host.querySelector<HTMLButtonElement>('button[aria-label$=" keyframes"]');
607
+ expect(caret()?.getAttribute("aria-label")).toBe("Expand Track 1 keyframes");
608
+
609
+ act(() => caret()?.click());
610
+ // One shared volume row, and BOTH clips hold it open.
611
+ expectTrackExpansion(row, ["narration-1", "narration-2"], TRACK_H + AUTOMATION_LANE_H);
612
+
613
+ // Every clip bar on the row is capped to one track height. Only the clip
614
+ // owning the property lanes used to be, so its siblings stretched the whole
615
+ // expanded row and painted their waveforms over the envelopes below.
616
+ expect(
617
+ ["narration-1", "narration-2"].map(
618
+ (id) => host.querySelector<HTMLElement>(`[data-el-id="${id}"]`)?.style.height,
619
+ ),
620
+ ).toEqual([`${TRACK_H - 2 * CLIP_Y}px`, `${TRACK_H - 2 * CLIP_Y}px`]);
621
+
622
+ act(() => caret()?.click());
623
+ expectTrackExpansion(row, [], TRACK_H);
624
+ act(() => root.unmount());
625
+ });
626
+
627
+ // The lanes are the row's, and selecting a clip must not rebuild them. They
628
+ // used to hang off the active clip's property lanes, so clicking a sibling
629
+ // moved the whole subtree into a different element and remounted every lane —
630
+ // which threw away each one's hover state and any gesture in flight. Pressing
631
+ // a lane to select its clip therefore made the handles vanish under the
632
+ // pointer, which is the one gesture the read-only lane exists to support.
633
+ it("keeps the automation lanes mounted when the selection moves along the row", () => {
634
+ const host = createSizedTimelineHost(720);
635
+ const automation = JSON.stringify({
636
+ version: 1,
637
+ lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }],
638
+ });
639
+ usePlayerStore.setState({
640
+ duration: 8,
641
+ timelineReady: true,
642
+ selectedElementId: "narration-2",
643
+ elements: [
644
+ { id: "narration-1", tag: "audio", start: 0, duration: 4, track: 0, automation },
645
+ { id: "narration-2", tag: "audio", start: 4, duration: 4, track: 0, automation },
646
+ ],
647
+ });
648
+ const root = createRoot(host);
649
+ act(() => root.render(React.createElement(Timeline)));
650
+ act(() => host.querySelector<HTMLButtonElement>('button[aria-label$=" keyframes"]')?.click());
651
+
652
+ const before = [...host.querySelectorAll(".hf-automation-lane")];
653
+ expect(before).toHaveLength(2);
654
+
655
+ act(() => usePlayerStore.setState({ selectedElementId: "narration-1" }));
656
+
657
+ // Identity, not deep equality: a remount produces structurally identical
658
+ // nodes, so only `toBe` can tell the two apart.
659
+ const after = [...host.querySelectorAll(".hf-automation-lane")];
660
+ expect(after).toHaveLength(before.length);
661
+ after.forEach((node, index) => expect(node).toBe(before[index]));
662
+ act(() => root.unmount());
663
+ });
664
+
581
665
  it("marks every clip in selectedElementIds as selected", () => {
582
666
  const host = createSizedTimelineHost(720);
583
667