@hyperframes/studio 0.8.105 → 0.8.106

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-tnfCiVdR.js → hyperframes-player-dl6K6ABp.js} +1 -1
  2. package/dist/assets/{index-DEDhPmPD.js → index-CMiYVGv5.js} +202 -202
  3. package/dist/assets/{index-zpe332hb.js → index-DKT5NF1Y.js} +1 -1
  4. package/dist/assets/index-lrT-gfHa.css +1 -0
  5. package/dist/assets/{index-Ca6LZlVb.js → index-ooaZZhKf.js} +1 -1
  6. package/dist/{chunk-QADHJFDF.js → chunk-ICTZJPQQ.js} +2 -1
  7. package/dist/chunk-ICTZJPQQ.js.map +1 -0
  8. package/dist/index.d.ts +15 -4
  9. package/dist/index.html +11 -2
  10. package/dist/index.js +6601 -6022
  11. package/dist/index.js.map +1 -1
  12. package/dist/player.js +1 -1
  13. package/dist/styles.css +1 -1
  14. package/package.json +9 -9
  15. package/src/captions/keyboard.test.ts +6 -0
  16. package/src/captions/keyboard.ts +2 -1
  17. package/src/components/StudioHeader.dom.test.tsx +31 -9
  18. package/src/components/StudioHeader.tsx +4 -1
  19. package/src/components/ThemeToggle.tsx +99 -0
  20. package/src/components/editor/DomEditOverlay.tsx +37 -5
  21. package/src/components/editor/SourceEditor.tsx +2 -6
  22. package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
  23. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
  24. package/src/components/editor/manualEditsDom.ts +33 -0
  25. package/src/components/editor/marqueeCommit.test.tsx +69 -1
  26. package/src/components/editor/marqueeCommit.ts +6 -1
  27. package/src/components/editor/overlayFrameLoop.test.ts +27 -0
  28. package/src/components/editor/overlayFrameLoop.ts +21 -0
  29. package/src/components/editor/propertyPanelFont.test.tsx +80 -3
  30. package/src/components/editor/propertyPanelFont.tsx +69 -46
  31. package/src/components/editor/propertyPanelHelpers.ts +1 -2
  32. package/src/components/editor/studioCodeTheme.test.ts +20 -0
  33. package/src/components/editor/studioCodeTheme.ts +99 -0
  34. package/src/components/editor/useDomEditNudge.test.tsx +31 -0
  35. package/src/components/editor/useDomEditNudge.ts +2 -2
  36. package/src/components/editor/useInlineTextEditing.tsx +1 -1
  37. package/src/components/editor/useMotionPathData.test.tsx +64 -0
  38. package/src/components/editor/useMotionPathData.ts +5 -1
  39. package/src/hooks/appHotkeysDispatch.ts +2 -1
  40. package/src/hooks/previewPromotionSelection.test.tsx +180 -0
  41. package/src/hooks/useAppHotkeys.test.ts +32 -0
  42. package/src/hooks/useAppHotkeys.ts +1 -2
  43. package/src/hooks/useClipboard.source.test.ts +99 -19
  44. package/src/hooks/useClipboard.ts +145 -124
  45. package/src/hooks/useDomEditSession.test.tsx +76 -72
  46. package/src/hooks/useDomEditSession.ts +7 -43
  47. package/src/hooks/useGsapTweenCache.ts +1 -2
  48. package/src/hooks/useInlineTextEdit.ts +16 -2
  49. package/src/hooks/useLivePreviewIframe.ts +18 -0
  50. package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
  51. package/src/hooks/usePreviewPersistence.ts +2 -5
  52. package/src/hooks/useRenderClipContent.ts +3 -3
  53. package/src/index.ts +2 -0
  54. package/src/livePreviewIframeExport.test.tsx +33 -0
  55. package/src/main.tsx +4 -1
  56. package/src/player/components/AudioWaveform.test.tsx +36 -15
  57. package/src/player/components/AudioWaveform.tsx +24 -7
  58. package/src/player/components/TimelineClip.test.tsx +38 -0
  59. package/src/player/components/TimelineClip.tsx +14 -6
  60. package/src/player/components/TimelineClipFades.test.tsx +376 -74
  61. package/src/player/components/TimelineClipFades.tsx +368 -137
  62. package/src/player/components/useTimelineKeyboardActor.ts +1 -1
  63. package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
  64. package/src/player/hooks/useShadowPreviewReload.ts +72 -5
  65. package/src/player/lib/automationStoreSync.test.ts +18 -1
  66. package/src/player/lib/automationStoreSync.ts +29 -17
  67. package/src/player/sceneSwap.ts +18 -0
  68. package/src/styles/components.css +26 -0
  69. package/src/styles/studio.css +6 -0
  70. package/src/styles/theme.css +10 -0
  71. package/src/utils/clipboardPayload.ts +10 -1
  72. package/src/utils/studioPendingEdits.ts +21 -0
  73. package/src/utils/studioTheme.test.tsx +141 -0
  74. package/src/utils/studioTheme.ts +34 -0
  75. package/src/utils/studioUiPreferences.ts +3 -0
  76. package/src/utils/typingTarget.test.ts +8 -1
  77. package/src/utils/typingTarget.ts +5 -0
  78. package/dist/assets/index-Cre9X4RS.css +0 -1
  79. package/dist/chunk-QADHJFDF.js.map +0 -1
@@ -1,8 +1,10 @@
1
- import { memo, useCallback, useEffect, useMemo, useRef } from "react";
1
+ import { memo, useCallback, useContext, useEffect, useMemo, useRef } from "react";
2
+ import { fadeGain } from "@hyperframes/core/audio-fade";
2
3
  import { useMountEffect } from "../../hooks/useMountEffect";
3
4
  import { useThumbnailLease } from "../../hooks/useThumbnailLease";
4
5
  import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
5
6
  import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
7
+ import { ClipFadesContext, type ClipFadeShape } from "./TimelineClipFades";
6
8
 
7
9
  export interface AudioWaveformProps {
8
10
  audioUrl: string;
@@ -22,7 +24,10 @@ export interface AudioWaveformProps {
22
24
 
23
25
  const BAR_STEP = 3;
24
26
 
25
- type BarGeometry = { x: number; width: number; height: number };
27
+ export const rendersWaveform = (el: { tag: string }) => el.tag === "audio";
28
+ const FADE_GHOST_OPACITY = 0.27;
29
+
30
+ type BarGeometry = { x: number; width: number; height: number; gain: number };
26
31
 
27
32
  function paintWaveformBars(
28
33
  context: CanvasRenderingContext2D,
@@ -33,11 +38,18 @@ function paintWaveformBars(
33
38
  amplitudes: readonly number[],
34
39
  ) {
35
40
  bars.forEach((bar, index) => {
36
- const amplitude = amplitudes[index] ?? 0;
41
+ const opacity = loudnessToOpacity(amplitudes[index] ?? 0);
37
42
  context.fillStyle = `rgb(${waveformBaselineRgb})`;
38
43
  context.fillRect(bar.x, height - 2, bar.width, 2);
39
- context.fillStyle = `rgba(${waveformBarRgb},${loudnessToOpacity(amplitude).toFixed(2)})`;
40
- context.fillRect(bar.x, height - bar.height, bar.width, bar.height);
44
+ const paint = (alpha: number, top: number, barHeight: number) => {
45
+ context.fillStyle = `rgba(${waveformBarRgb},${alpha.toFixed(2)})`;
46
+ context.fillRect(bar.x, top, bar.width, barHeight);
47
+ };
48
+ const faded = bar.height * bar.gain;
49
+ if (faded > 0) paint(opacity, height - faded, faded);
50
+ if (faded < bar.height) {
51
+ paint(opacity * FADE_GHOST_OPACITY, height - bar.height, bar.height - faded);
52
+ }
41
53
  });
42
54
  }
43
55
 
@@ -47,6 +59,7 @@ export function drawWaveformCanvas(
47
59
  muted: boolean,
48
60
  trimStartFraction: number,
49
61
  trimEndFraction: number,
62
+ fades: ClipFadeShape | null = null,
50
63
  ) {
51
64
  const width = Math.max(1, canvas.clientWidth);
52
65
  const height = Math.max(1, canvas.clientHeight);
@@ -67,6 +80,9 @@ export function drawWaveformCanvas(
67
80
  x: (index * width) / amplitudes.length,
68
81
  width: Math.max(1, width / amplitudes.length),
69
82
  height: Math.max(3, amplitude * height),
83
+ gain: fades
84
+ ? fadeGain(((index + 0.5) / amplitudes.length) * fades.duration, fades.duration, fades)
85
+ : 1,
70
86
  }));
71
87
  const channelToken = muted ? "--timeline-waveform-muted-rgb" : "--timeline-waveform-bar-rgb";
72
88
  const waveformBarRgb = getComputedStyle(canvas).getPropertyValue(channelToken);
@@ -179,11 +195,12 @@ export const AudioWaveform = memo(function AudioWaveform({
179
195
  const peaks =
180
196
  snapshot.status === "ready" && snapshot.value.kind === "waveform" ? snapshot.value.peaks : null;
181
197
 
198
+ const fades = useContext(ClipFadesContext);
182
199
  const draw = useCallback(() => {
183
200
  const canvas = canvasRef.current;
184
201
  if (!canvas || !peaks) return;
185
- drawWaveformCanvas(canvas, peaks, muted, trimStartFraction ?? 0, trimEndFraction ?? 1);
186
- }, [muted, peaks, trimEndFraction, trimStartFraction]);
202
+ drawWaveformCanvas(canvas, peaks, muted, trimStartFraction ?? 0, trimEndFraction ?? 1, fades);
203
+ }, [fades, muted, peaks, trimEndFraction, trimStartFraction]);
187
204
 
188
205
  const setCanvasRef = useCallback(
189
206
  (canvas: HTMLCanvasElement | null) => {
@@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import type { TimelineElement } from "../store/playerStore";
7
7
  import { TimelineClip } from "./TimelineClip";
8
+ import { ClipFadesContext } from "./TimelineClipFades";
8
9
  import type { TimelineEditCapabilities } from "./timelineEditing";
9
10
  import { defaultTimelineTheme, type TimelineTheme } from "./timelineTheme";
10
11
 
@@ -67,6 +68,43 @@ function renderClip({
67
68
  }
68
69
 
69
70
  describe("TimelineClip", () => {
71
+ it("hands its content the clip's fades, and none to a clip the mixer does not hear", () => {
72
+ const seen: unknown[] = [];
73
+ function Probe() {
74
+ seen.push(React.useContext(ClipFadesContext));
75
+ return null;
76
+ }
77
+ const render = (element: TimelineElement) => {
78
+ const host = document.createElement("div");
79
+ document.body.append(host);
80
+ const root = createRoot(host);
81
+ act(() => {
82
+ root.render(
83
+ <TimelineClip
84
+ el={element}
85
+ pps={100}
86
+ clipY={0}
87
+ isSelected={false}
88
+ isHovered={false}
89
+ hasCustomContent
90
+ capabilities={capabilities}
91
+ isComposition={false}
92
+ onHoverStart={vi.fn()}
93
+ onHoverEnd={vi.fn()}
94
+ onClick={vi.fn()}
95
+ onDoubleClick={vi.fn()}
96
+ >
97
+ <Probe />
98
+ </TimelineClip>,
99
+ );
100
+ });
101
+ act(() => root.unmount());
102
+ };
103
+ render({ id: "music", tag: "audio", start: 0, duration: 4, track: 0, fadeIn: 1, fadeOut: 0.5 });
104
+ render({ id: "title", tag: "div", start: 0, duration: 4, track: 0, fadeIn: 1 });
105
+ expect(seen).toEqual([{ fadeIn: 1, fadeOut: 0.5, duration: 4 }, null]);
106
+ });
107
+
70
108
  it("renders the clip label above custom content without showing default timecode", () => {
71
109
  const { host, root } = renderClip({
72
110
  element: { id: "hero", label: "Hero", tag: "div", start: 1, duration: 1, track: 0 },
@@ -9,7 +9,8 @@ import {
9
9
  import type { TimelineEditCapabilities } from "./timelineEditing";
10
10
  import { isAudioTimelineElement } from "../../utils/timelineInspector";
11
11
  import { timelineClipFocusId } from "./timelineNavigationIdentity";
12
- import { TimelineClipFades } from "./TimelineClipFades";
12
+ import { ClipFadesContext, TimelineClipFades, useClipFadeDraft } from "./TimelineClipFades";
13
+ import { rendersWaveform } from "./AudioWaveform";
13
14
 
14
15
  interface TimelineClipProps {
15
16
  el: TimelineElement;
@@ -82,6 +83,8 @@ export const TimelineClip = memo(function TimelineClip({
82
83
  "--clip-handle": theme.handleColor,
83
84
  } as CSSProperties;
84
85
  const isAudioClip = isAudioTimelineElement(el);
86
+ const hasFades = (isAudioClip || Boolean(el.hasAudio)) && !isGestureActor;
87
+ const fade = useClipFadeDraft(el);
85
88
  const clipClassName = [
86
89
  "timeline-clip",
87
90
  "absolute",
@@ -206,16 +209,21 @@ export const TimelineClip = memo(function TimelineClip({
206
209
  {startLabel}-{endLabel}s
207
210
  </span>
208
211
  )}
209
- {children}
210
- {/* Fade handles + ramps for anything the mixer hears — audio clips and
211
- videos marked data-has-audio. They write data-fade-in/out on the clip
212
- and are the timeline half of the inspector's Fade rows. */}
213
- {(isAudioClip || el.hasAudio) && !isGestureActor && (
212
+ <ClipFadesContext.Provider value={hasFades ? fade.shape : null}>
213
+ {children}
214
+ </ClipFadesContext.Provider>
215
+ {/* Fade handles for anything the mixer hears: audio clips and videos marked
216
+ data-has-audio. They write data-fade-in/out, the timeline half of the
217
+ inspector's Fade rows. */}
218
+ {hasFades && (
214
219
  <TimelineClipFades
215
220
  el={el}
216
221
  pps={pps}
217
222
  widthPx={widthPx}
218
223
  showHandles={(isHovered || isSelected) && !isDragging}
224
+ focusable={isSelected}
225
+ hasWaveform={rendersWaveform(el)}
226
+ fade={fade}
219
227
  />
220
228
  )}
221
229
  </button>