## Summary
Coordinated minor bump across all published packages. No source changes in this PR itself; it is the version stamp for everything that landed on main since v0.2.5.
## Version bumps
| Package | from | to |
|---|---|---|
| `@hyperframes/cli` | 0.2.5 | **0.3.0** |
| `@hyperframes/core` | 0.2.5 | **0.3.0** |
| `@hyperframes/engine` | 0.2.5 | **0.3.0** |
| `@hyperframes/player` | 0.2.7 | **0.3.0** |
| `@hyperframes/producer` | 0.2.5 | **0.3.0** |
| `@hyperframes/studio` | 0.2.9 | **0.3.0** |
Between 0.2.5 and this release, `player` and `studio` received several patch versions on npm as we iterated on the bundler, entry point, and SSR issues. 0.3.0 collapses that into a single coordinated minor so the ecosystem is aligned again.
## What is in v0.3.0
### `@hyperframes/player`
**Restored package entry points to the compiled `dist/` output.** 0.2.5 shipped with `"main": "./src/hyperframes-player.ts"` but the published tarball only included `dist/` via the `"files"` field. Every consumer trying to import the package failed with `Module not found: Can't resolve '@hyperframes/player'`. Entry points now point at the built JS/`.d.ts` files inside `dist/`.
**DOM-based root timeline resolution in the ready probe.** In a bundled preview, `window.__timelines` contains the master composition alongside its sub-compositions, for example:
```js
{
main: GSAPTimeline(14s),
intro: GSAPTimeline(1.5s),
'scene2-4-canvas': GSAPTimeline(12.6s),
'scene5-logo-outro': GSAPTimeline(3.2s),
}
```
The probe used to select the adapter with `keys[keys.length - 1]`. Object key ordering meant the last-registered sub-composition would win, so the `ready` event reported a sub-composition's duration (e.g. 3.2s) instead of the master's 14s. The probe now looks up the root composition id from the outermost `[data-composition-id]` element in the iframe DOM and uses its key. Falls back to the last key when no element is present, so standalone sub-composition previews keep working.
### `@hyperframes/studio`
**`useTimelinePlayer.getAdapter()` uses the same DOM-based root id lookup** as the player. Previously play, pause, seek, and duration readout were all driven by whichever sub-composition happened to register its timeline last.
**`Player.tsx` loads `@hyperframes/player` lazily.** The component used to call `import "@hyperframes/player"` at module scope, which runs the package's `customElements.define(...)` side effect during module evaluation. `HTMLElement` does not exist in the Node runtime, so any consumer page that transitively imported the studio during server rendering threw:
```
ReferenceError: HTMLElement is not defined
at module evaluation (@hyperframes/studio/src/player/components/Player.tsx)
```
The import now runs inside the mount effect via `import(...)` so it only evaluates in the browser. Added a cancellation flag and deferred cleanup so a fast unmount before the dynamic import resolves does not leak listeners or DOM nodes.
**Captions module imports stripped of `.js` extensions.** Files under `src/captions/` imported siblings as `./types.js` and `./parser.js`. That is legal ESM TypeScript but Turbopack and several other bundlers refuse to resolve those specifiers against `.ts` files inside `node_modules`, breaking any consumer build that transitively pulled in the captions module. Captions now uses extensionless imports, matching the rest of the studio codebase.
### `@hyperframes/core`, `@hyperframes/cli`, `@hyperframes/engine`, `@hyperframes/producer`
Version bump only, no source changes since 0.2.5. Kept on the same version so the ecosystem is easier to reason about.
## Impact for consumers
If you use `@hyperframes/studio` in a Next.js app:
- The play button in a bundled preview reports the correct composition duration and drives the master timeline.
- The session page no longer 500s in dev mode when the studio barrel is imported (the SSR fix).
- Turbopack builds that transitively load the captions module no longer fail on `Cannot resolve './types.js'`.
If you use `@hyperframes/player` directly:
- Consumer bundlers can resolve the package again (dist entry points restored).
- The `ready` event duration reports the master, not a sub-composition.
## After merge
Publish each package to npm with `pnpm publish` (workspace deps auto-resolve).
@hyperframes/player
Embeddable web component for playing HyperFrames compositions. Zero dependencies, works with any framework.
Install
npm install @hyperframes/player
Or load directly via CDN:
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
Usage
<hyperframes-player src="./my-composition/index.html" controls></hyperframes-player>
The player loads the composition in a sandboxed iframe, auto-detects its dimensions and duration, and scales it responsively to fit the container.
With a framework
import "@hyperframes/player";
// The custom element is now registered — use it in your markup
// React: <hyperframes-player src="..." controls />
// Vue: <hyperframes-player :src="url" controls />
Poster image
Show a static image before playback starts:
<hyperframes-player
src="./composition/index.html"
poster="./thumbnail.jpg"
controls
></hyperframes-player>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
src |
string | — | URL to the composition HTML file |
width |
number | 1920 | Composition width in pixels (aspect ratio) |
height |
number | 1080 | Composition height in pixels (aspect ratio) |
controls |
boolean | false | Show play/pause, scrubber, and time display |
muted |
boolean | false | Mute audio playback |
poster |
string | — | Image URL shown before playback starts |
playback-rate |
number | 1 | Speed multiplier (0.5 = half, 2 = double) |
autoplay |
boolean | false | Start playing when ready |
loop |
boolean | false | Restart when the composition ends |
JavaScript API
const player = document.querySelector("hyperframes-player");
// Playback
player.play();
player.pause();
player.seek(2.5); // jump to 2.5 seconds
// Properties
player.currentTime; // number (read/write)
player.duration; // number (read-only)
player.paused; // boolean (read-only)
player.ready; // boolean (read-only)
player.playbackRate; // number (read/write)
player.muted; // boolean (read/write)
player.loop; // boolean (read/write)
// Inner iframe access (for advanced consumers — see "Advanced: iframe access" below)
player.iframeElement; // HTMLIFrameElement (read-only)
Advanced: iframe access
The composition runs inside a sandboxed <iframe> in the player's Shadow DOM. For most use cases you don't need direct access — the JavaScript API above is enough. But if you're building an editor, recorder, or custom timeline that needs to inspect the composition's DOM or read its __player / __timelines runtime objects, use the iframeElement getter:
const player = document.querySelector("hyperframes-player");
const iframe = player.iframeElement;
// Now you can reach into the composition's DOM and runtime
iframe.contentDocument.querySelectorAll("[data-composition-id]");
iframe.contentWindow.__timelines;
This is the canonical way to bridge the player into tools like @hyperframes/studio. The studio exports a resolveIframe helper that works with both iframe refs and web-component refs:
import { useTimelinePlayer, resolveIframe } from "@hyperframes/studio";
const { iframeRef } = useTimelinePlayer();
const player = document.createElement("hyperframes-player");
player.setAttribute("src", src);
container.appendChild(player);
// Forward the inner iframe so useTimelinePlayer can drive play/pause/seek.
iframeRef.current = resolveIframe(player);
React: declarative ref pattern
If you prefer JSX over imperative element creation, attach a ref directly to the web component and resolve the iframe inside an effect:
import "@hyperframes/player";
import type { HyperframesPlayer } from "@hyperframes/player";
import { useTimelinePlayer, resolveIframe } from "@hyperframes/studio";
function StudioPreview({ src }: { src: string }) {
const { iframeRef, onIframeLoad } = useTimelinePlayer();
const playerRef = useRef<HyperframesPlayer>(null);
useEffect(() => {
iframeRef.current = resolveIframe(playerRef.current);
});
return <hyperframes-player ref={playerRef} src={src} onLoad={onIframeLoad} />;
}
Heads up — common gotcha
If you pass the
<hyperframes-player>element itself (notiframeElement) into a hook that expects an<iframe>, every.contentWindow/.contentDocumentaccess returnsnullbecause the iframe lives inside the player's Shadow DOM. Always extractiframeElementfirst, or useresolveIframefrom@hyperframes/studiowhich handles both iframe and web-component hosts transparently.
Events
| Event | Detail | Fired when |
|---|---|---|
ready |
{ duration } |
Composition loaded and duration determined |
play |
— | Playback started |
pause |
— | Playback paused |
timeupdate |
{ currentTime } |
Playback position changed (~10 fps) |
ended |
— | Reached the end (when not looping) |
error |
{ message } |
Composition failed to load |
player.addEventListener("ready", (e) => {
console.log(`Duration: ${e.detail.duration}s`);
});
player.addEventListener("ended", () => {
console.log("Done!");
});
Sizing
The player fills its container and scales the composition to fit while preserving aspect ratio. Set a size on the element or its parent:
hyperframes-player {
width: 100%;
max-width: 800px;
aspect-ratio: 16 / 9;
}
The width and height attributes define the composition's native resolution for aspect ratio calculation — they don't set the player's display size.
How it works
The player renders compositions in a sandboxed <iframe> inside a Shadow DOM. It communicates with the HyperFrames runtime via postMessage. If the composition has GSAP timelines (window.__timelines) but no runtime, the player auto-injects it from CDN.
Distribution
| Format | File | Use case |
|---|---|---|
| ESM | hyperframes-player.js |
Bundlers (Vite, webpack, etc.) |
| CJS | hyperframes-player.cjs |
Node.js / require() |
| IIFE | hyperframes-player.global.js |
<script> tag, CDN |
All formats are minified with source maps. TypeScript definitions included.
License
MIT