| 1 | # Spike report — hyperframes/studio integration (2026-05-29) |
| 2 | |
| 3 | Verdict: **plan A (rebuild html-video studio on top of hf NLELayout) costs |
| 4 | several weeks, not one. Pivot to plan B — keep the current vanilla studio, |
| 5 | borrow the small leaf components.** |
| 6 | |
| 7 | ## What we tried |
| 8 | |
| 9 | `packages/studio-next/` is a Vite + React 19 app that imports |
| 10 | `@hyperframes/studio` 0.6.52 from npm, then attempts: |
| 11 | |
| 12 | 1. Mount `<Player directUrl={...}>` pointing at an existing frame served |
| 13 | by the production studio (`http://127.0.0.1:3071/preview/<id>/frame/<nid>`). |
| 14 | 2. Bind `useElementPicker(iframeRef)` to the player's iframe. |
| 15 | 3. Toggle pick mode + click an element in the loaded frame. |
| 16 | |
| 17 | ## What works |
| 18 | |
| 19 | - **Vite imports `@hyperframes/studio` cleanly.** No transpiler tweaks needed. |
| 20 | - **`SourceEditor` (CodeMirror 6 wrapper for HTML/CSS/JS)** renders and edits |
| 21 | out of the box — confirmed in the earlier two-pane spike. |
| 22 | - **`hyperframes` runtime auto-injection** is real: hf Player polls the iframe |
| 23 | every 200ms, detects `__timelines` (GSAP) or absence and injects an IIFE |
| 24 | shim. Our agent-generated frames are valid hosts for this. |
| 25 | - **Pure utility exports** (`applyPatch`, `parseStyleString`, |
| 26 | `mergeStyleIntoTag`, `findElementBlock`, `EaseCurveEditor`) are pure logic |
| 27 | and drop in. |
| 28 | |
| 29 | ## What blocks plan A |
| 30 | |
| 31 | - **`Player.directUrl` is a fallback, not the canonical entry.** Source |
| 32 | reads `const src = directUrl || /api/projects/${projectId}/preview`. |
| 33 | In practice the player still expects the surrounding hf studio backend: |
| 34 | - serves `/api/projects/:id/composition` returning a composition.json |
| 35 | - hosts the runtime IIFE on a same-origin path |
| 36 | - implements asset / file APIs (`/api/projects/:id/files/...`) that the |
| 37 | runtime calls back to fetch nested compositions |
| 38 | Pointing `directUrl` at our `/preview/:id/frame/:nodeId` request loads |
| 39 | the frame's HTML, but the runtime then tries to call back into a hf |
| 40 | backend that doesn't exist, and the picker's postMessage handshake |
| 41 | (`{ source: "hf-parent", type: "control", action: "enable-pick-mode" }`) |
| 42 | is answered by nothing. |
| 43 | |
| 44 | - **`useElementPicker` only works behind hf Player.** The iframe must be |
| 45 | the same one the player has injected its runtime into. Our own iframe |
| 46 | can't be substituted. |
| 47 | |
| 48 | - **`NLELayout` reads from hf's Composition + FileManager + TimelineElement |
| 49 | model**, which are concepts we don't map onto. Our ContentGraph nodes are |
| 50 | N independent full-bleed HTML pages; hf's Composition is one timeline |
| 51 | with multiple layers / clips on shared tracks. |
| 52 | |
| 53 | - **The NLE shows a single composition's timeline, not "N storyboard |
| 54 | cards"**. We'd have to either: |
| 55 | - shoehorn each frame into a separate hf composition (and the NLE |
| 56 | becomes a per-frame editor with no inter-frame view), or |
| 57 | - flatten our N frames into one hf composition with N sequential layers |
| 58 | (and lose the per-frame `data-hv-text` editing model) |
| 59 | |
| 60 | Both choices wreck the v0.8 phase-driven flow. |
| 61 | |
| 62 | ## What we can still take |
| 63 | |
| 64 | | component | status | what we'd use it for | |
| 65 | |-----------|--------|---------------------| |
| 66 | | `SourceEditor` (CodeMirror 6) | ✅ drop-in | Right-pane "Source" tab next to Frame text | |
| 67 | | `EaseCurveEditor` | ✅ drop-in | Future motion-curve picker on per-element edits | |
| 68 | | `applyPatch` + `parseStyleString` + `mergeStyleIntoTag` + `findElementBlock` | ✅ drop-in | Programmatic edits to frame HTML (used by an inline element picker we control) | |
| 69 | | Their styling tokens (`tailwind-preset.ts`) | ⚠️ would need React anyway | Visual consistency if we go React | |
| 70 | | `Player` / `Timeline` / `NLELayout` | ❌ requires hf backend | n/a | |
| 71 | | `useElementPicker` | ❌ requires hf runtime contract | n/a | |
| 72 | |
| 73 | ## Recommended path (plan B) |
| 74 | |
| 75 | 1. **Production studio stays vanilla JS at port 3071.** No React migration. |
| 76 | 2. **Right-pane gets a "Source" tab** that wraps `SourceEditor` from |
| 77 | `@hyperframes/studio`. Joey can edit the active frame's HTML directly |
| 78 | in CodeMirror with HTML/CSS/JS syntax + autocomplete. Save via |
| 79 | `PUT /api/projects/:id/frames/:nodeId/raw-html` (already exists). |
| 80 | 3. **Click-to-edit text overlay stays our hand-rolled one** (commit |
| 81 | `e17af19` / `5046fa5`). It already works without postMessage and |
| 82 | without hf runtime. |
| 83 | 4. **`packages/studio-next` becomes a permanent home** for any React-only |
| 84 | borrowable hf widget (EaseCurveEditor etc), exposed back into the |
| 85 | vanilla studio via an `<iframe>` portal or a tiny standalone bundle. |
| 86 | No need to React-migrate the main app. |
| 87 | 5. **If we ever want NLE-style editing**, the path is to ship our own |
| 88 | timeline UI on top of our existing frames[] model — not to reshape our |
| 89 | model to fit hf's. |
| 90 | |
| 91 | ## Out of scope for this report |
| 92 | |
| 93 | - Alternatives like Remotion / Motion Canvas / Revideo as backend (we're a |
| 94 | meta-aggregator anyway — they're future). |
| 95 | - Building a hf-compatible backend so `Player` works as designed. Doable |
| 96 | but ~1 week, and the resulting integration still won't surface our |
| 97 | 5-phase chat or ContentGraph — it's a parallel UI for editing one |
| 98 | composition at a time. |
| 99 |