* 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>
* feat(studio): preview new tracks at drop boundaries
* fix(studio): preserve group ownership across overlapping moves
* refactor(studio): share timeline drop decisions and fixtures
* fix(studio): open an empty audio zone below visual tracks
* fix(studio): cancel thumbnails when project pages leave
* refactor(studio): share thumbnail regression setup
* test(studio): gate thumbnail socket release in browser CI
* chore(studio): keep optimistic revision comments within ratchet
* fix(studio): pin regression proxy requests to its backend
* test(studio): bound the proxy origin probe
* test(studio): probe proxy origin through the ready document
* docs(studio): map filmstrip clip rendering contract
* feat(studio): give timeline clips a separate filmstrip name band
* test(studio): require the filmstrip picture wrapper
* fix(studio): keep fallback clip warnings clear of menu badges
* fix(studio): reserve clip text space beside status badges
* fix(studio): separate clip FX and fade hit areas
* fix(studio): separate clip tool pointer targets
* fix(studio): reserve FX space without lowering fades
* refactor(studio): trim fade geometry measurement
* test(studio): refresh fade target after closing FX menu
* test(studio): collapse keyframe lanes before pointer witness
* test(studio): register filmstrip control browser gate
* feat(studio): the code editor follows studio's light and dark theme
Replaces One Dark with a Studio editor theme whose colours are theme.css variables: eight syntax role tokens (One Light and One Dark hues held at 4.5:1), editor surfaces from Studio's roles, all measured by the contrast ratchet.
* test(studio): the code editor theme takes every colour from a theme.css token
* fix(studio): code stays at 4.5:1 when a selection or search match sits on the active line
Syntax tokens are tuned for the selection over the active line, which the ratchet now measures; matches are outlined rather than stacking another wash; the search panel's pressed button and special characters follow the theme; unused tooltip rules go.
* fix(studio): code matches carry no background, and types read apart from constants
Search and selection matches are outlined only, so CodeMirror's lime match wash and stacked washes are gone; type and constant tokens split by hue in both themes.
* fix(studio): the current search match reads apart by width, not colour alone
A 2px accent outline on the current match and a muted 1px outline on other copies of the selected word.
* test(studio-server): history prune tests allow a file time a moment past Date.now()
* ci: temporary probe for the history prune test flake
* fix(studio): timeline scroll-end timer no longer fires after unmount
* ci: remove the temporary history prune probe
* test(studio): check the virtualizer teardown with a spy and a 200 ms wait
* test(studio): start an edit accuracy benchmark for manual editing (work in progress)
* test(studio): an edit accuracy benchmark for manual editing
Drives real pointer and keyboard gestures in the built CLI Studio on generated one-element projects and scores tracking, drop jump, reload drift, undo fidelity and smoothness in composition px.
* test(studio): move render drift out of the first benchmark PR
Render drift against a producer frame ships in the stacked follow-up so this PR stays near the size budget.
* test(studio): keep the edit accuracy bench clean under fallow
Builds the grid as a product of its axes, drops unused exports and marks the harness functions that fallow scores as complex.
* test(studio): report edit accuracy with the Studio and bench commits it measured
Moves scoring and the report files into report.mjs, stamps both commits at start, counts undo byte failures apart from the worst box, and survives Studio swapping the preview iframe mid-read.
* test(studio): bank the edit accuracy baseline measured on main
660 cases on main ae914334f: 0 pass all six checks, 304 pass everything but smoothness. Five cases that hit a harness error in the full run were rerun and spliced in.
* test(studio): state the edit accuracy build prerequisite in the runner header
* test(studio): keep the edit accuracy baseline one line per case under the formatter
* test(studio): store edit accuracy baseline values rounded up
Rounding to the nearest 0.01 stored seven 0.502 px tracking results as 0.5, which reads as a pass at the 0.5 px limit.
* test(studio): score edit smoothness on dropped frames and main-thread work per frame
* test(studio): measure edit accuracy on a still preview and score press jump
Snapshots wait for the preview frames and the box to hold still for 1 s, so Studio's asynchronous reload after a save is measured, not raced. A preview that never settles fails the metrics it feeds instead of erroring.
Adds press jump (box at press against the box before it, 0.5 px). Smoothness counts only dropped frames beyond a blank-page control driven the same way in the same Chrome; unknown trace data fails smoothness alone.
The runner takes the suite lock per chunk of 8 cases (--lock), reads the port Studio announces, keeps undo history and tmp projects inside the case dir and removes them on a signal, and stamps the CLI tree's commit and bundle hash.
* test(studio): stamp the edit accuracy build with the Studio bundle as well as the CLI
The CLI bundle hashed the same for two different Studio builds; Studio's index.html names its hashed assets, so the stamp now covers it.
* test(studio): regenerate the edit accuracy baseline on main after the settle fix
Full grid on main 14e87005d with the settle fix, press jump and the smoothness control: 70/660 pass, 314/660 pass everything but smoothness, 0 harness errors, 0 unsettled. Scored without render, which #4764 adds.
* test(studio): wait for each edit accuracy undo write before sending redo
Undo and redo each wait up to 15 s for their own write, like the settle; redo is sent only once undo landed, and a write that never lands fails undo with undoTimeout instead of scoring an untested redo. The reload waits for the files to hold still. The headline is now the accuracy count, with smoothness on its own line against the control. The settle compares each read with the window's first read, and a CLI outside a git tree no longer stops the run.
* perf(studio): light ui and player entries for a host's first screen
* perf(studio): the ui entry's logo no longer loads the studio header
* test(studio): refuse only packages studio itself depends on
* test(studio): the light entries also refuse the hf-ids parser