feat(studio): export AudioWaveform, ImageThumbnail, useRenderClipContent (#4301)

@hyperframes/studio's public index doesn't export the clip-content
thumbnail primitives, forcing a deep import. Desktop's Canva timeline
lane needs to draw audio waveforms and image thumbnails from a host
app without reaching into the package's internals.

Adds a test that mounts each export through the package name the way
a host app would (not a relative import), so a future removal or
accidental un-export fails a test instead of a downstream deep-import
workaround.
This commit is contained in:
Miguel Ángel
2026-09-23 02:05:42 -04:00
committed by GitHub
parent b73df3549e
commit b9fe13ad8c
5 changed files with 67 additions and 3 deletions
@@ -0,0 +1,54 @@
// @vitest-environment happy-dom
// Imports the clip-content thumbnail primitives the way a host app does: by package name.
// Desktop's timeline lane draws audio waveforms and image thumbnails without a deep import.
import { act } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { installReactActEnvironment, mountReactHarness } from "./hooks/domSelectionTestHarness";
const { leaseSpy } = vi.hoisted(() => ({
leaseSpy: vi.fn((_request: unknown) => ({ status: "loading" as const })),
}));
vi.mock("./hooks/useThumbnailLease", () => ({
useThumbnailLease: leaseSpy,
}));
import { AudioWaveform, ImageThumbnail, useRenderClipContent } from "@hyperframes/studio";
installReactActEnvironment();
afterEach(() => {
leaseSpy.mockClear();
document.body.innerHTML = "";
});
describe("clip content package exports", () => {
it("mounts AudioWaveform and leases waveform decoding", async () => {
const root = mountReactHarness(
<AudioWaveform
audioUrl="/media/voice.wav"
label=""
labelColor="#fff"
projectId="project-a"
sessionEpoch={9}
priority="interaction"
/>,
);
expect(leaseSpy).toHaveBeenCalledWith(expect.objectContaining({ kind: "waveform" }));
await act(async () => root.unmount());
});
it("mounts ImageThumbnail and leases the still frame", async () => {
const root = mountReactHarness(
<ImageThumbnail imageSrc="/media/frame.png" label="" labelColor="#fff" />,
);
expect(leaseSpy).toHaveBeenCalledWith(expect.objectContaining({ kind: "image" }));
await act(async () => root.unmount());
});
it("exposes useRenderClipContent as a hook a host can call", () => {
expect(typeof useRenderClipContent).toBe("function");
});
});
@@ -97,7 +97,7 @@ function renderAudioClip(
});
}
interface UseRenderClipContentOptions {
export interface UseRenderClipContentOptions {
projectIdRef: { current: string | null };
compIdToSrc: Map<string, string>;
activePreviewUrl: string | null;
+10
View File
@@ -77,6 +77,16 @@ export {
export { TimelineProvider, useTimelineContext } from "./player/components/TimelineProvider";
export type { TimelineTheme } from "./player/components/timelineTheme";
// Clip content thumbnails: used by a host rendering its own timeline lane.
export { AudioWaveform } from "./player/components/AudioWaveform";
export type { AudioWaveformProps } from "./player/components/AudioWaveform";
export { ImageThumbnail } from "./player/components/ImageThumbnail";
export type { ImageThumbnailProps } from "./player/components/ImageThumbnail";
export { useRenderClipContent } from "./hooks/useRenderClipContent";
export type { UseRenderClipContentOptions } from "./hooks/useRenderClipContent";
export type { ThumbnailPriority } from "./player/lib/thumbnailScheduler";
export type { TimelineClipRenderContext } from "./player/components/TimelineTypes";
// Host overlays: draw over the preview in composition coordinates (see EditorShellProps.gestureOverlay)
export { usePreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
export type { PreviewCompositionRect } from "./components/editor/usePreviewCompositionRect";
@@ -4,7 +4,7 @@ import { useThumbnailLease } from "../../hooks/useThumbnailLease";
import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
interface AudioWaveformProps {
export interface AudioWaveformProps {
audioUrl: string;
waveformUrl?: string;
label: string;
@@ -5,7 +5,7 @@ import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailSche
import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
import { computeThumbnailStrip, probeImageAspect } from "./thumbnailUtils";
interface ImageThumbnailProps {
export interface ImageThumbnailProps {
imageSrc: string;
label: string;
labelColor: string;