* fix(studio): keep a pick made just before a reload swaps the preview frame
* fix(studio): trust only frames that were the primary preview, one pick helper in the tests
* test(studio): a zoomed frame is not trusted once the zoom is gone
* refactor(studio): name the trusted frames instead of commenting them
* feat(studio): draw a waveform strip under video clips that carry sound
An audible video keeps its thumbnails and gains the audio waveform along the bottom
38% of the clip (also when thumbnails are off); muted and silent video are unchanged.
The waveform route already decodes video audio; a test now proves it on an mp4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): latch a CLIP light on the master meter at the export ceiling
The light turns red when the preview master sample peak reaches -1 dBFS (the export
true-peak ceiling) and stays lit until clicked. Mutation-tested threshold and latch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): paint the CLIP light from theme colour tokens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): mark where each clip redlines on its waveform
studio-server computes an absolute per-50ms sample-peak map once per media file
(native channels, cached by size+mtime next to the waveform cache); the timeline
paints red marks where the source at the clip's own volume reaches -1 dBFS and
names the loudest point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): let a video with sound be the beat source
Studio's isMusicSourceElement accepts an audible, unmuted video for the music track
and the beat fallback (lane zoning unchanged); core findMusicAudioSrc accepts
<video data-has-audio="true"> without muted; the beats CLI message mentions video.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): decode each peak map once and never offer Duck on a carve source
Concurrent peak requests for one file share a single ffmpeg decode; Duck under voice
is withheld on a clip another bed already carves against.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): decode the same audio stream the peak map probes
The channel probe read stream a:0 while the decode left stream choice to
ffmpeg, which picks a later stereo stream over a mono first one; every frame
was then read at the wrong width, doubling the bins and taking the other
stream's level. Both commands now select a:0, and the cache key moves to
peaks-v2 so maps built from the wrong stream are rebuilt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): terminate the peak map's ffprobe options before the input
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): reject a peak map whose decode exits nonzero
A decode that emits some samples and then fails no longer resolves as a
complete map, so /peaks cannot cache a truncated clipping map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio-server): share loudness measurement with an absolute target and video support
normalize-audio moves its measure/plan code into @hyperframes/studio-server/loudness,
gains an absolute mode (--lufs, default -16, clamped at +12 dB and -1.5 dBTP) and
accepts <video> clips with sound. Adds the @hyperframes/core/audible-video subpath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio-server): add a measure-only loudness normalize endpoint
POST /projects/:id/loudness/normalize measures a clip's played window (audio or
video) and answers the -16 LUFS data-volume plan; Studio writes the attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: register the loudness and audible-video subpaths in their descriptors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style: format the loudness and audible-video subpath descriptors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): add Normalize loudness and Duck under voice to the clip menu
Normalize measures the clip on studio-server and writes data-volume as one undoable
edit; Duck toggles a carve against every overlapping voice (audio or video with
sound) at the default strength. Both live in clipMenuAudioItems with one hook-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(studio): tighten carveOutput docs to the package comment share
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(producer): report how far the true-peak limiter lowered the export
The AAC true-peak pass now returns its attenuation; the render logs "Audio lowered
by X.X dB to stay under -1 dBTP" and sets RenderJob.audioLoweredDb, which Studio
servers carry in the complete SSE event and the /renders record. Also exports
useAudioMetersVisible from @hyperframes/studio for hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): scan HTML start tags without a backtracking regex
The start-tag regex backtracked quadratically on '<' followed by many spaces
(CodeQL js/polynomial-redos). A linear character scan reads the same name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): refuse to normalize a volume-automated clip and honour save outcomes
A volume lane owns the clip's gain in preview and render, so writing
data-volume changed nothing while the toast claimed success; Normalize now
refuses with a clear message. Normalize and Duck also check each quiet-save
outcome, stop at the first failed or refused write and report it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(core): drop the duplicate audible-video subpath descriptor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): undo paints a move or nudge back at once while its save is still running
* test(studio): the edit accuracy gate judges the two part C sequences again
* fix(studio): a save lasts until undo can paint it from the history
* test(studio): the bench reads the box after a key's frame, as a user sees it
The nudge step already waits a frame per key (sequences.mjs:162); undo and the text step's Enter now do too.
* refactor(studio): say which registrations a pending edit adopts
* feat(core): add gentle ramp-in, ramp-out and slow-mo-middle speed presets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): add voice, look and crop clip-menu items and clip badges
Voice and Look are single-choice submenus writing data-fx-chain and
data-color-grading; None clears. Crop opens the canvas crop preset bar.
ClipBadges reads the live preview node and follows attribute edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): drop the unstyled badge classes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): add crop aspect presets to the canvas crop handles
Free, 16:9, 9:16, 1:1, 4:5, Reset and Done float above the clip opened from
the clip menu. Presets commit a centred inset clip-path; Reset removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(studio): read the committed crop from the inline clip-path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio-server): add freeze-frame file mutation
Extracts the frame under the playhead with ffmpeg (media time honours
data-media-start, playback rate and a rate lane) into assets/freeze/, then in
one write splits the video, inserts a 2 s <img> still with the video's box
styles, shifts the rest of the track and splits a data-link partner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): import replaceFileAtomically from core atomic-file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): add freeze frame to the clip menu
Freeze frame on a video under the playhead posts the studio-server
freeze-frame mutation and records its before/after as one undo step.
onFreezeFrame is threaded through the timeline edit callbacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): keep the freeze still inside assets/freeze
The clip id from the project HTML went straight into ffmpeg's -y output
path, so an id with ../ segments wrote outside the project. The id is now
reduced to one safe filename component, the output is pinned inside
assets/freeze before extraction, and the still's src is built from that
name rather than from a filesystem path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio-server): give every freeze still its own immutable name
Two clip ids that sanitise alike (a.b / a_b), ids sharing an 80-character
prefix, or a second freeze of one clip at the same playhead all produced the
same still path, and ffmpeg's -y replaced the first still's pixels. The name
now carries a hash of the raw id and a per-extraction token, the route
refuses a name that already exists, and ffmpeg runs with -n so it can never
overwrite. The freeze media time reads data-playback-start before
data-media-start, as playback does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(core): add isAudibleVideoElement predicate and audible media selector
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(runtime): route processed audible videos through web audio in preview
An audible <video> joins the Web Audio schedule, hidden-toggle and
group-capture passes when it needs a gain el.volume cannot express: a
boost above unity, an fx chain, automation, or an audio group. A plain
audible video at unity stays on native output. The clock master stays
audio-only; a video whose capture fails stays on native output (never
decoded, never failed closed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(audio): let audible videos join audio groups in preview and render
resolveAudioGroups, audioGroupOf, group render ids, the engine mixer and
the audio_group_no_members lint rule accept an audible <video>. A hidden
group drops its video members from the mix; muted videos stay inert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(editing): offer the audio fx section on audible videos
EditableElementFacts gains hasAudio; studio and sdk derive it from muted and data-has-audio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): show audio meters when a project has an audible video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): offer audible videos as carve voices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): let audible video clips join audio groups
Drops the video refusal; grouping and group audition count audible videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): honor hidden audio groups for auto-detected video audio
Muted videos that declare data-has-audio are no longer mixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep authored src, loop and honest status when keeping cutout sound
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): treat only data-has-audio=true as audible in runtime and lint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(core): trim the web audio route and group-key comments
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(runtime): capture ramped clips so a group's bus carries them in preview
A rate lane skipped Web Audio entirely, so a ramped video or audio clip in a
group played native output without the group's fader, mute or FX while the
export applied them. Only the decoded-buffer fallback cannot follow a rate
curve; the media-element capture can, so the skip now guards that fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): show meters only for videos Web Audio carries
A plain audible video at unity stays on native output, which the meters
cannot read, so it no longer advertises them. The runtime's join rule and the
meter gate now share audibleVideoNeedsWebAudio (core/audible-video).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The server patch is already durable, so keep it and its undo entry and keep
the preview matching the file, but throw after history so the caller cannot
report success (the cutout would say Sound kept with no audio saved).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#4869 dropped prepareContent from the persist options and the server path
while #4813's attribute batch commit (mediaAudioEdits) still passes it, so
main fails typecheck and the audio edit would lose its prepared content.
Restore the option, take the server patch when it is set, and write the
prepared content over the patch under the same file queue.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(studio): resizing an element GSAP only positions saves its CSS width and height
* fix(studio): a resize never saves GSAP's scale mask as the element's CSS
* fix(studio): keep the resize size writer inside the file and comment budgets
* fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders
* fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it
* test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview
* fix(studio): a move keeps an authored xPercent centring set in the saved file
* fix(studio): a drag on a centred element no longer flashes off-centre on release
* fix(studio): a font pick saves the edit and its @font-face in one write
* fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit
* test(scripts): pin that a slash after i-- before a quoted slash is division
* fix(studio): an inspector edit saved while a preview reload loads stays on screen
* fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it
* fix(studio-server): a font name or path with braces in quotes still saves its @font-face
* fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule
* test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass
* feat(cli): one owner for init media placeholders, audio stays on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): init keeps a video's sound on the clip; silent files scaffold muted
Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com>
Co-Authored-By: kapelame <kapelame@users.noreply.github.com>
* feat(registry): example templates keep the a-roll's sound on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): video audio rules recommend keeping sound on the video
Update video_missing_muted and video_audio_double_source message and
fixHint text to recommend the new default: keep audio on the video with
data-has-audio="true" (not muted). Tests verify the new wording is in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): a video is either audible (data-has-audio) or muted, never both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): follow the compiler's audio rule in the audio-track toggle
Confirm before background removal drops a video's sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): hyperframes-core teaches sound on the video by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): workflows and scaffold guidance keep a video's sound on the clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: footage with sound keeps it on the video clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): init from-file a-roll is timed, muted strip is attribute-exact
The from-file a-roll video gets data-start="0" so a scaffold lints clean, and
the orphaned audio-only slot comment is dropped. setVideoAudio now walks the
tag attribute by attribute, so a class value containing the word muted
survives. The ffprobe-missing warning says the video is assumed to have sound
and how to switch to muted, and DEFAULT_META notes the failure surfaces at
render until lint support lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep has-audio in step with the muted toggle, test cutout confirm
Muting a video now clears has-audio and unmuting sets it to true, in both the
Design and flat media panels, matching the Has audio track toggle. Audio
elements are unchanged. Adds coverage for both panels plus the cutout
window.confirm guard: declined leaves src alone, accepted swaps src and mutes,
a silent video never prompts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false
An audio element or an undefined hasAudio no longer picks up a muted attribute;
only an explicit false on a video writes muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: video_missing_muted rule text, silent-footage notes, consistent audible examples
lint.mdx and cli.mdx describe the current video_missing_muted rule instead of
the retired unmuted-video wording. The editing recipes say how to adapt for
silent footage and require the outgoing clip's sound to end at a J-cut. The
Remotion media example shows the audible mapping consistently, data-attributes
states that has-audio is required unless muted, and the hairline frame note no
longer claims the video is muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): encode init media filenames and probe --audio duration
Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded.
--audio now passes the probed track length as the composition duration in both init paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): audio-state-aware media rules and time-aware double-source check
video_missing_muted honors any explicit data-has-audio.
video_audio_double_source only considers audible videos and only fires
when the same-src clips overlap in time.
duplicate_audio_track now covers audible videos.
audio_file_without_element no longer claims silence when an audible video
carries the sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): audio-stream check for remotion ports, followable J-cut recipe
Remotion Video maps to data-has-audio only when the file has an audio stream.
The J/L-cut recipe now shows both shots with muted pictures and separate audio.
embedded-captions removes the audio.mp3 scratch file it extracts.
skills-manifest.json regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(lint): trim comments to satisfy the comment ratchet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): atomic muted edits, cutout keeps sound on a linked audio
Muted and Has audio track commit one batched write (one persist, one
undo entry) and remove data-has-audio instead of writing an empty
marker. Unmuting a video probes its src: audible -> true, silent ->
false, probe failed -> absent. Remove background on an audible video
inserts a linked sibling <audio> with the original sound in the same
write, replacing the drop-the-sound confirm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(producer): skip hidden videos when auto-adding video audio
A video hidden in Studio is dropped by the mixer, so its src never reached
existingAudioSrcs and the render re-added it at full volume. The video parser
now records the shared hidden predicate and the auto-detect step skips hidden
videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep useDomEditSession under the 600-line cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com>
Co-authored-by: kapelame <kapelame@users.noreply.github.com>
* feat(studio): hosts can run Studio's agent tools on their own edit session
Exports buildStudioTools, collectStudioLookScene and their types from the package index. EditorShell keeps using the same builder; no behaviour change.
* style(studio): format the agent tools export and keep its comment share
* ci(studio): measure drag smoothness on a quiet runner, with a blank-page control
* ci(studio): gates read the base the run measured, not the base branch tip
* style(ci): format the base-file test
* fix(studio): undo right after an edit undoes that edit, even before its save lands
* fix(studio): undo right after a nudge undoes the nudge
* fix(studio): undo during a nudge's wait for more keys never paints the edit before it
* fix(studio): a failed save never blocks undo, and a nudge burst always ends its pending edit
* fix(studio): undo waits out every failed or conflicting edit, and Delete counts as pending
* fix(studio): every GSAP script commit counts as a pending edit, so undo waits for it
* fix(studio): keep App under the file size limit
* test(studio): bank the nudge-then-undo sequences the gate now passes
* fix(studio): the timeline scroll gate catches dropped frames in the virtualized view
* fix(studio): the timeline scroll gate measures a failed arm once more before failing
* fix(studio): the virtualized timeline gate holds interactions to 58.3 ms
* fix(scripts): count comments after regex literals in the comment ratchet
* fix(scripts): keep comments after JSX tags, ++/-- and wrapped division in the ratchet
* fix(studio): an undo that lands mid-drag keeps the box under the pointer
* fix(studio): an undo that lands on a pressed layer keeps what its gesture draws
* fix(studio): every gesture mark names what it draws, and a text edit draws nothing
* test(studio): gate the drag, undo, drag cases again now that their race is fixed
* fix(studio): a text edit's gesture mark draws nothing, so an undo under it reverts
* test(studio): keep the one drag, undo, drag case whose press can still jump in quarantine
* test(studio): bank the drag, undo, drag cases that now pass
* fix(studio): a reload requested while a canvas save is in flight loads again once it lands
* test(studio): type the late-reload drag helper for every edit kind
* fix(core): a composition made only of a set shows it at a seek in the preview
* chore(core): keep the comment share of the runtime init file level
* test(studio): bank the four resize-hold rows the preview now gets right
* fix: a save's temp file no longer reaches Studio as an outside change
* refactor: the project file walk and its tests carry no new duplication or complexity
* test(studio): the edit accuracy gate judges the two nudge sequences again
* fix(studio): a hand edit after a preview reload draws on the element now on screen
* fix(studio): a preview reload waits for the gesture on screen to end
* fix(studio): a marquee after a reload selects from the preview on screen
* fix(studio): a reload requested before a canvas save loads again once the save lands
* feat(studio-server): a write receipt names the version the write replaced
* test(studio-server): route receipts carry the version each write replaced
* refactor(studio-server): the receipt's from field needs no comment
* fix(studio): the preview serves a same-size edit written right after the last one
* chore(studio): keep the comment share and complexity gates green
* refactor(studio): keep the project signature doc comment on the function it describes
* fix(studio): the preview re-reads files written in the last two seconds, whoever wrote them
* feat(studio): paste and duplicate can keep a clip's own time, on the next free track
* fix(studio): an in-place paste uses the copied time only in the composition it was copied in
* test(studio): an in-place duplicate inside a sub-composition keeps its local time
* refactor(studio): the paste anchor is its own function, and the in-place tests share their setup
* refactor(studio): paste names its undo step and note in one place, and two paste tests share their steps
* 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): catch teleports and multi-step edit bugs in the edit accuracy bench
* test(studio): share the edit accuracy case harness between single gestures and sequences
* test(studio): keep edit accuracy helpers private unless shared
* test(studio): score plain-CSS edits on pages that load GSAP for other elements
* test(studio): add an idle GSAP axis value so plain-CSS edits are scored on GSAP pages
* test(studio): report a slow undo save as late with its latency, apart from a lost one
* test(studio): sample the gap between back-to-back drags and allow one frame of lag
* test(studio): read the edge from the element's fractional size, so a crop edge is exact
* test(studio): keep waiting for a late save until an undone file stays put for 3 s
* ci(studio): run the edit accuracy grid in 12 shards to stay near 25 minutes each
* test(studio): read edit bench frames just before paint, so unpainted states never count
* test(studio): scope the edit bench frame sampler so page globals cannot clash
* test(studio): let a lagging box catch up the pointer travel it owes, so a held frame is no jump
* test(studio): record every save a sequence makes, even two that land 10 ms apart
* test(studio): end a drag's teleport scoring at its pointer-up, not at the next element's press
* test(studio): bank for each edit accuracy case a run that agrees with the gate's verdict
* test(studio): fail the edit accuracy gate on a teleport or an unmeasured drag
* test(studio): fail the edit accuracy gate when a text edit is not saved or shown
* test(studio): measure but do not gate the edit accuracy cases a known Studio race flips
* test(studio): bank the edit accuracy cases that newly pass with the teleport metric
* test(studio): quarantine the two nudge sequences whose redo write sits at the 2 s limit
* style(studio): cut three bench comments to one line each
* feat(studio): a light and dark theme toggle in the header that remembers the choice
The sun and moon button flips Studio between Graphite light and dark with a
circular reveal, saves the choice, and follows the system until one is made.
* fix(studio): studio opens light the first time, then keeps the saved theme
* fix(studio): drop the theme field's comment, the theme owner states the rule
* fix(studio): the theme toggle shows only in studio's own app and follows the theme on screen
A host that embeds StudioApp gets no toggle; the button reads the document's theme, the reveal drops the default crossfade, and reduced motion skips it.
* fix(studio): the theme reveal leaves other view transitions alone; one default for the toggle
The crossfade rule applies only while the toggle's own transition runs, StudioApp passes the header's single default through, and the reveal's reduced-motion and keyboard paths are tested.
* fix(studio): a second theme click mid-reveal keeps the crossfade off
Only the latest reveal clears the document marker; tests pin the marker while a reveal runs and the circle's radius.
* refactor(studio): the theme toggle shares its icon props and reads as plain functions
Simplify pass: one icon props object for the sun and moon, no useCallback around a handler nothing memoises, cannotReveal named for what it answers, the test reuses the theme reader.
* fix(studio): studio's own app turns the theme toggle on through context; tests parse index.html
App.tsx stays at main's size: main.tsx provides ShowThemeToggle and the header reads it, default off for hosts. The boot-script tests read index.html through DOMParser instead of an HTML regex.
* fix(studio-server): a folder rename past an unreadable folder or file answers ok
* fix(studio-server): a rename skips only folders and files it may not read
* fix(studio-server): the rename's skip helpers say it by name
* test(studio-server): the rename tests share one request
* perf(studio): load the font lists when the font picker first opens
Selecting an element no longer fetches and dedupes the 1.7k-name Google font list, so the response cannot land inside a drag frame. The lists are fetched once per session on the first open.
* test(studio): expect the font lists to load on the first picker open
* perf(studio): load the font lists at session start and process them when input is idle
* style(studio): drop a comment the font list names already carry
* perf(studio): process the font lists in idle slices of at most 3 ms
* perf(studio): join the font lists without iterating them when they arrive
* feat(studio): fade handles ride the fade's end and the waveform shows the fade
* refactor(studio): read fade handle keys from one lookup
* fix(studio): fade handle keys stay with the handle and snap past the clip's own edges
* fix(studio): Cmd shortcuts keep working on a focused slider and fade boxes stay on their tabs
* fix(studio): keep a fade handle while it is in use and restore the viewport gate's Chrome path
* fix(studio): a fade's draft clears with its own save and grow drags never shrink it
* fix(studio): saved fades reach the timeline store so the handle stays where it was dropped
* fix(studio): a fade longer than its clip starts a drag or key from the clip's end
* fix(studio): fade comments state the rule, not its history
* feat(studio): graphite colour system with a light theme that passes contrast
Studio's tokens take the Graphite values: a lifted dark, a light set behind
data-theme="paper", the player dark in both. Every text role clears 4.5:1 and
every icon and ring 3:1 on the fills it sits on, held by the contrast ratchet.
* fix(studio): every studio surface follows the light theme and passes contrast
The dock no longer inherits dockview's forced dark scheme, popovers sit on the
raised surface, stock white, red and amber text map to roles, the keyframe
diamonds and track headers use roles, and the contrast ratchet measures every
pair in both themes (150 pairs).
* fix(studio): stock white, red and amber stay colours; studio text and fills use roles
Round-two review: white, red and amber are colour content as well as text, so
they keep Tailwind's values and hosts keep their palettes. Text, toasts, the
feedback card, the FX popover, destructive buttons, category labels and the dock
take Graphite roles instead; the contrast gate rejects an unparseable colour.
* fix(studio): every light-theme ink, icon, dot and focus ring holds its contrast
Stock white, red, amber and file-type hex left in Studio chrome now use roles or a light-dark pair; the file tree icons carry a contrast test.
* chore(studio): drop two comments the names already carry
* fix(studio): red marks, teal outlines and colour curves hold contrast in both themes
Danger graphics use the danger ink, file tree outlines use the accent ink, the colour-curve well stays dark like the scopes, and six timeline token pins come back.
* fix(studio): invalid readouts, the curve focus ring and editor dots stay distinct in both themes
The invalid readout border is dashed as well as red, the curve graph's ring sits inside its dark well, the radius dots and ease handle centre follow the theme.
* refactor(studio): ease and radius editors use colour classes; the dashed invalid border has a test
Simplify pass: the ease editor's inline accent styles become stroke and fill classes, the radius dot class is computed once, the ratio check is one expression, and the alias table is named for what it holds.
* fix(studio): the timeline viewport gate judges p95 over every measured scroll step
* fix(studio): format the pooled p95 lines in the timeline viewport gate
* test(studio): pin the timeline viewport gate's pooled verdict with tests
* refactor(studio): share one Chrome launcher across the timeline and fade-handle gates
* fix(studio): undo and redo are no longer held behind thumbnail renders
* test(studio): composition card tests share one mount, and the bench's 2 s undo rule is tested
* fix(studio): a composition card and its timeline clip share one thumbnail render
* test(studio): bank opening a film at 15 thumbnail requests, one per composition
* fix(studio): composition cards load near the viewport and keep their last frame until the next
* fix(studio): a composition card keeps its last frame under cache pressure, and loads 200 px early
* fix(studio): a rotate GSAP does not own saves where you let go, as the element's own CSS rotate
* fix(studio): a rotate keeps a transform-centred element in place
* fix(studio): a plain rotate shows its angle in the panel and turns inline elements too
* fix(studio): a rotate turns a mirrored or stretched centred element with the cursor
* fix(studio): keep the rotate's panel readout within the comment budget after the move landed
* fix(studio): an inline element's turn saves the inline-block it was drawn with
The overlay passes the rotate target it read at press into the commit, so the
save draws and patches exactly what the draft did instead of re-reading an
element the draft already made inline-block. The GSAP-aware commit also routes
on that press-time decision.
* test(studio): bank the 27 rotate cases a GSAP-free rotate now passes
baseline.json is the edit-accuracy-gate artifact of the CI run at cb75cc37,
copied byte for byte: 530 to 557 passing, all rotate-none cases, none
regressed.
* fix(studio): a word an older Studio turned keeps its inline-block and whole angle on a new rotate
Clearing the legacy rotation marks puts the element back to inline and drops
their turn, so the save reads the target again instead of using the one the
press read before the clear. A test also pins that a turn the press drew as CSS
stays on the CSS writer.
* test(studio): bank the edit accuracy baseline from the rebased rotate run