* chore: release v0.8.159
* chore: v0.8.159 notes list the first-frame fix
* chore: v0.8.159 notes list the sandboxed GPU check fix
* chore: v0.8.159 notes list the hidden-scene fix and the offline font test
* chore: release v0.8.154
* ci: give the studio test job fifteen minutes
The queue run installs and builds inside the same ten minute cap.
It was still inside the studio tests when that cap cancelled the job.
* chore: release v0.8.152
* docs(changelog): correct the v0.8.152 release notes
The summary overstated snapshot retention, history turns, and video state.
It also left out the new artifact paths and nullable keyframe times.
* test(studio): bundle the real-Chrome undo test with esbuild so its setup fits on Windows
* test(studio): stub only the Node built-ins the undo test bundle reaches
* fix(studio): the motion path draws on every load, not only when the layer's node is the frame's own
useMotionPathData found the selected layer with `instanceof` against the preview
frame's HTMLElement. In the Studio preview the body's nodes often carry the editor
window's prototypes (see core's runtime/domRealm.ts), so the check failed, the path
never got its anchor and drew nothing. Core's realm-free isHtmlElement owns that
question; both motion-path call sites now use it.
* fix(studio): every preview node check asks what a node is, not which window built it
Studio checked preview nodes against their document window's own constructors in
seek reapply, studio motion, timeline z-order commits, the manual offset drag and
the z-order menu. On loads where body nodes carry the editor window's prototypes
those checks fail silently, as the motion path did. They now use core's realm-free
predicates, and a source test fails on a new window-bound check.
* fix(studio): the webmcp preview helper asks what a node is, and both realm tests stub the frame
* fix(studio): the last preview node checks ask what a node is, and the guard sees any constructor read
The selector walk read ancestor ids only from editor-realm nodes and the audio group
observer never saw a group the preview added. Both use core's predicates now. The guard
flags any DOM interface read off an object, the spellings it missed included, and the
window gates the old constructor needed are gone.
* test(studio): cover the main seek filter across windows and split member reads
* fix(studio): a press on the motion path's line inside the selected layer moves the layer
* fix(studio): a press inside the selected layer moves it, and path nodes sit where the layer is
* refactor(studio): one helper places a path point, for keyframes and the start
* fix(studio): a handle over a path node takes its press; nodes sit right on centred layers
* fix(studio): reading a layer's path anchor leaves its CSS translate alone
* chore(studio): keep the motion path home file under the comment share
* fix(studio): a new motion path ends where you click on a CSS-centred layer
* fix(studio): a path made on a transform-centred layer ends where you click
* style(studio): keep a motion path test comment within 120 columns
* perf(studio): keep timeline bulk actions under a second, hand-move at most 3 clips
Dragging or keyboard-moving a selection of 4 or more clips is refused with a
toast pointing at the prompt box; 1 to 3 clips move as before.
Root causes of the multi-second waits on large projects:
- every selected clip fired its own source-existence probe; they now leave as
one request per file
- a timeline delete sent one request per clip, each re-parsing the file; it is
now one request that parses once
- each click on a clip from another file re-fetched GSAP data for every
composition; covered files are kept and unchanged files answer 304
- any root index.html write re-captured every composition thumbnail; only a
root head edit does now, since a scene page borrows nothing else from it
Adds timeline-bulk-actions.mjs, a browser benchmark of select-all, move 3 and
delete-all on a real project.
* chore(studio): trim the bulk benchmark header to the comment limit
* refactor(studio): tidy the bulk-action fixes after review
Root head memo is keyed by the root file's content so every subscriber of one write gets one answer, and a thumbnail request can no longer overwrite it. The GSAP read reuses the shared content version as its ETag.
* fix(studio): keep a head edit's thumbnail refresh when a later write replaces it
File changes coalesce to the newest while a drain is in flight, so the affected-composition lists are merged instead of replaced. A failed GSAP read stays unloaded and is retried, and the GSAP read's ETag is salted with the parser version.
* feat(studio): cap hand-resizing a multi-selection at 3 clips like moving
Dragging a resize edge with 4 or more clips selected is refused with the same toast as a move; the rule is one constant and one helper shared by pointer move, pointer resize and keyboard pickup.
* fix(studio): keep a head edit's thumbnail refresh through a replaced conflict
A newer blocked change replaces the held one, so it now inherits the thumbnails the held one owed.
* fix(studio): keep a held head edit's thumbnail refresh when a later write saves cleanly
The clean-save path and the failed-cleanup path now merge the held blocked change's affected compositions, and the blocked-state merge runs inside the state update so two conflicts in one tick cannot lose the first.
* chore(studio): keep useTimelineClipDrag under the 600-line cap
* feat(studio): cap by the clips an edit would change, and cut the added comments
The 3-clip rule counts the clips a move or trim would write to: a locked clip swept into a move stays put, and a trim some member cannot take edits only the grabbed clip. Comment share is back at or under the base in every changed file.
* fix(studio): clear fallow audit and comment length on the multi-select cap
* fix(studio): keep bench script comments under the block limit
* fix(studio): keep the failed file's payload when another file's snapshot delete fails
* fix(studio): parse modern CSS colors without a new dependency
* refactor(studio): one colour parser for the text toolbar, stricter rgb numbers
The inline text toolbar kept its own canvas fallback for named colours.
parseCssColor now resolves them, so the toolbar uses it alone.
rgb() channels must be well-formed numbers; that replaces the NaN check
after parsing and rejects rgb(1. 2 3) as CSS does.
* refactor(studio): drop comment lines the comment ratchet refuses in colorValue
* fix(studio): linear-time rgb() patterns and no new colour literal in the parser
The rgb() number pattern now has one way to match each digit run, which clears
CodeQL's slow-regex alerts. The canvas reset uses transparent so the Studio
colour-literal ratchet stays at its baseline.
* fix(studio): legacy colours resolve in browsers without relative colour syntax
parseBrowserColor reads the colour itself from the canvas first and accepts a
#rrggbb or rgba() result, then tries color(from ...). Named, hsl() and percent
rgb() colours resolve again in the text toolbar on Chrome before 119 and Safari 17.
* test(studio): a colour the canvas rejects parses to null, not transparent black
* fix(studio): an inserted gradient stop takes the colour the gradient paints there
The stop colour is color-mix() in oklab, or srgb when both stops are legacy,
resolved by the browser, so it matches the painted midpoint including
premultiplied alpha; the channel blend stays as the fallback. The parser tries
color(from ...) before the legacy read, keeping alpha precise, and calls
CSS.supports inside its try. A Chrome-147-shaped fake canvas covers the modern
path in unit tests.
* test(studio): keep the fake browser canvas inside its test file
* fix(studio): mix an inserted gradient stop in the space the whole gradient uses
* fix(studio): let an unresolved var() stop keep a legacy gradient in sRGB
* fix(studio): only a bare var() stop leaves the gradient space to its neighbours
* fix(studio): pick light-dark() text colour from what the element paints
* refactor(studio): drop the neighbour clause the whole-gradient check already covers
* fix(studio): resolve the picker colour from the element that paints the selected text
* fix(studio): read the picker colour from the character the swatch colour came from
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>