feat(studio): use @hyperframes/player web component for preview (#238)

## Summary

- **Replaces the studio's hand-rolled iframe + scaling in** **`Player.tsx`** with the `<hyperframes-player>` web component, eliminating duplicated ResizeObserver, dimension detection, and stage-size message handling
- **Adds a public** **`iframeElement`** **getter** to the player web component so the studio's `useTimelinePlayer` can still access the inner iframe for clip manifest parsing, timeline probing, and DOM inspection
- **Updates player package exports** to resolve from source for workspace consumers (matching `@hyperframes/core` pattern), while npm-published consumers still get built `dist/` files

### Why a separate player package?

1. **Zero dependencies, any framework** — 12KB vanilla web component vs 940KB React+Zustand+CodeMirror studio
2. **CDN-ready** — single `<script>` tag, no build pipeline needed
3. **Embeddable by third parties** — users embed compositions in their own sites without the studio
4. **Single source of truth** — studio now uses the player instead of duplicating its scaling/detection logic

## Test plan

- [x] `pnpm --filter @hyperframes/player typecheck` passes
- [x] `pnpm --filter @hyperframes/studio typecheck` passes
- [x] `pnpm --filter @hyperframes/studio build` passes
- [x] `pnpm --filter @hyperframes/studio test` passes (2 pre-existing failures, unrelated)
- [x] E2E: Standalone player loads composition, detects 4s GSAP timeline, controls work, play/pause works
- [x] E2E: Studio preview renders via `<hyperframes-player>`, `iframeElement` bridge works, playback controls sync correctly
This commit is contained in:
Miguel Ángel
2026-04-10 03:00:54 +02:00
committed by GitHub
parent 7e7d41f833
commit 3482441c9f
5 changed files with 72 additions and 104 deletions
@@ -112,13 +112,13 @@ describe("usePlayerStore", () => {
});
});
describe("updateElementStart", () => {
describe("updateElement", () => {
it("updates the start time of a specific element", () => {
usePlayerStore.getState().setElements([
{ id: "el-1", tag: "div", start: 0, duration: 5, track: 0 },
{ id: "el-2", tag: "div", start: 5, duration: 5, track: 1 },
]);
usePlayerStore.getState().updateElementStart("el-1", 3);
usePlayerStore.getState().updateElement("el-1", { start: 3 });
const elements = usePlayerStore.getState().elements;
expect(elements[0].start).toBe(3);
expect(elements[1].start).toBe(5); // unchanged
@@ -129,7 +129,7 @@ describe("usePlayerStore", () => {
{ id: "el-1", tag: "div", start: 0, duration: 5, track: 0 },
];
usePlayerStore.getState().setElements(original);
usePlayerStore.getState().updateElementStart("nonexistent", 10);
usePlayerStore.getState().updateElement("nonexistent", { start: 10 });
expect(usePlayerStore.getState().elements[0].start).toBe(0);
});
});