Skip to content

Commit 604f02b

Browse files
docs(studio): write down what Studio does not tell you about itself (#3165)
Working in packages/studio for the first time costs a day rediscovering things the source does not show: that the chrome is a measurement drawn in Studio's document over an iframe, that some gestures cannot be synthesised at all so a driver needs window.__studioTest, that the diagnostic channels exist and are off by default, that bare `bun test` reports failures that are not real, and which gates reject a PR. Scoped to the package, following docs/AGENTS.md, and pointed at from the project-structure list in both root files so it is found before the first edit rather than after.
1 parent c86d401 commit 604f02b

3 files changed

Lines changed: 101 additions & 2 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ packages/
6666
player/ → Embeddable <hyperframes-player> web component
6767
producer/ → Full rendering pipeline (capture + encode + audio mix)
6868
shader-transitions/ → WebGL shader transitions for compositions
69-
studio/ → Browser-based composition editor UI
69+
studio/ → Browser-based composition editor UI (read packages/studio/AGENTS.md first)
7070
registry/
7171
blocks/ → Installable sub-composition scenes (50+)
7272
components/ → Installable effects and snippets

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ packages/
9696
player/ → Embeddable <hyperframes-player> web component
9797
producer/ → Full rendering pipeline (capture + encode + audio mix)
9898
shader-transitions/ → WebGL shader transitions for compositions
99-
studio/ → Browser-based composition editor UI
99+
studio/ → Browser-based composition editor UI (read packages/studio/AGENTS.md first)
100100
registry/
101101
blocks/ → Installable sub-composition scenes (50+)
102102
components/ → Installable effects and snippets

‎packages/studio/AGENTS.md‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Working on Studio
2+
3+
Read this before your first change in `packages/studio`. It is the handful of
4+
things that are not visible from the source, and that cost real time to
5+
rediscover.
6+
7+
## The shape of the thing
8+
9+
Studio renders the user's composition in an **iframe**, and draws its own
10+
chrome — selection box, handles, dashed outlines, toolbars — in **Studio's own
11+
document**, positioned over the iframe. Nothing Studio draws lives inside the
12+
composition, because a render would capture it and the composition's styling
13+
would inherit into it.
14+
15+
Two consequences you will meet immediately:
16+
17+
- Reaching a preview element from a driver or a test means going through the
18+
iframe: `iframe.contentDocument.getElementById(...)`. Studio's own panels may
19+
be inside shadow roots, so a plain `document.querySelector` finds neither.
20+
- Every overlay box is a *measurement* of an element, not the element. When
21+
chrome disagrees with the pixels underneath it, the bug is almost always in
22+
the measurement, in `components/editor/domEditOverlayGeometry.ts`.
23+
24+
## Driving Studio for verification
25+
26+
A pixel-precise click inside the preview is not something an automated driver
27+
can reliably land, and some gestures cannot be synthesised at all: the canvas
28+
overlay takes pointer capture and recognises a double press itself, so
29+
`page.mouse` click pairs do not open a text edit no matter how they are timed.
30+
31+
Use the dev-only hook instead. In a dev build `window.__studioTest` exposes:
32+
33+
```js
34+
await window.__studioTest.selectByDomId("headline"); // selects, reveals the inspector
35+
```
36+
37+
That is the same selection a click produces. The general lesson: from a settled
38+
selection, keyboard paths are dependable where pointer paths are not. Prefer a
39+
key over a synthesised gesture whenever the feature offers one.
40+
41+
`useStudioTestHooks` also carries the timeline performance fixtures. The hook is
42+
gated on `STUDIO_TEST_HOOKS_ENABLED` (dev or development mode only), so
43+
`window.__studioTest` is absent in production builds — feature-detect it.
44+
45+
## Tracing decisions
46+
47+
The interesting failures here are decisions, not crashes: a preview that
48+
reloads when it should not, a shift-click that selects the wrong element.
49+
Nothing throws, so a trace of the decision is the only way to avoid guessing.
50+
51+
Channels are off by default. Turn one on and reload:
52+
53+
```js
54+
localStorage.setItem("hf-drag-debug", "1"); // then grep the console for [hf-drag]
55+
```
56+
57+
Live channels: `reload`, `select`, `drag`, `resize`, `commit`. Add one with
58+
`makeStudioDebugLogger("<name>")` in `utils/studioDebug.ts`.
59+
60+
## Running the tests
61+
62+
Studio's tests are **vitest**, not `bun test`. Running bare `bun test` in this
63+
package collects the files with the wrong runner and reports failures that are
64+
not real:
65+
66+
```bash
67+
bun run --cwd packages/studio test # all of them
68+
bun run --cwd packages/studio test src/components/editor # one directory
69+
```
70+
71+
happy-dom is not a browser. It does not reflect the individual transform
72+
properties (`rotate`, `scale`, `translate`) into computed style, and it has no
73+
`DOMMatrix` — the geometry tests carry their own stand-in. When a behaviour
74+
depends on real layout or real computed style, prove it in a browser and keep
75+
the unit test on the pure function underneath.
76+
77+
## Gates that will fail your PR
78+
79+
- **600 lines per file.** CI checks only non-test files your PR changed. A file
80+
that grows past it has to be split in the same PR that grew it.
81+
- **`bunx fallow audit --base origin/main --fail-on-issues`** — complexity per
82+
function, duplication, unused exports. Adding branches to an already-complex
83+
function trips it; extract rather than nest.
84+
- **oxlint and oxfmt**, not eslint or prettier.
85+
86+
## Traps worth knowing
87+
88+
- **`rotate` is not `transform`.** Studio's rotate handle writes the CSS
89+
`rotate` property, which is an individual transform property and does not
90+
appear in `getComputedStyle(el).transform`. Anything measuring an angle has to
91+
read both and compose them the way CSS does, individual properties first.
92+
- **A seek re-renders the whole timeline**, not the tween you patched. Patching
93+
several elements one at a time and seeking after each repaints the ones still
94+
queued from their un-patched tweens. Batch, then render once.
95+
- **Studio's own writes must not reload the preview.** Writes carry a token so
96+
the file-watcher event can be recognised as ours; a new write path that
97+
forgets it makes the preview flash on every edit.
98+
- **Preserving a selection set that does not contain the id empties it.** Check
99+
`preserveSet` semantics before reusing it.

0 commit comments

Comments
 (0)