Commit Graph

901 Commits

Author SHA1 Message Date
Miguel Angel Simon Sierra d0eb2b2104 perf(studio): virtualize timeline rows 2026-07-19 00:16:00 +02:00
Miguel Angel Simon Sierra 38ea7ffa93 perf(studio): centralize timeline viewport geometry 2026-07-19 00:16:00 +02:00
Miguel Angel Simon Sierra 5f0c6de79a refactor(studio): decompose timeline orchestration 2026-07-19 00:06:22 +02:00
Miguel Angel Simon Sierra 128c1a9ad2 refactor(studio): extract timeline render contracts 2026-07-19 00:05:13 +02:00
Miguel Angel Simon Sierra 860b7fddba refactor(studio): isolate clip drag lifecycle 2026-07-19 00:03:59 +02:00
Miguel Angel Simon Sierra 46b8692c5d test(studio): add timeline browser performance gate 2026-07-19 00:02:51 +02:00
Miguel Angel Simon Sierra 046aa7339a test(studio): add timeline performance fixtures 2026-07-19 00:01:42 +02:00
Miguel Angel Simon Sierra a03f772700 perf(studio): define timeline viewport budgets 2026-07-19 00:00:48 +02:00
Miguel Angel Simon Sierra 09a5e14f2e fix(studio): preserve exact keyframe edit intent 2026-07-16 21:19:32 -04:00
Miguel Angel Simon Sierra 49ed3450e0 fix(studio): harden keyframe edit identity 2026-07-16 18:12:14 -04:00
Miguel Angel Simon Sierra b807d95a6c fix(studio): update ease mode optimistically 2026-07-16 18:12:13 -04:00
Miguel Angel Simon Sierra 9e5966ba0a fix(studio): publish keyframe cache refresh atomically 2026-07-16 18:12:13 -04:00
Miguel Angel Simon Sierra c1fd7ba5b9 fix(studio): resolve safe keyframe review findings 2026-07-16 18:12:13 -04:00
Miguel Angel Simon Sierra 63342fc3c3 test(studio): restore hold ease seek coverage 2026-07-16 18:12:13 -04:00
Miguel Angel Simon Sierra 556e0975c5 chore(studio): keep edited modules within size gate 2026-07-16 18:12:12 -04:00
Miguel Angel Simon Sierra 2158ba08a5 feat(studio): bulk-edit easing for merged keyframes 2026-07-16 18:12:12 -04:00
Miguel Angel Simon Sierra 3c17c9a8d8 test(studio): cover expanded keyframe timeline 2026-07-16 18:12:12 -04:00
Miguel Angel Simon Sierra f385e111f0 feat(studio): wire expanded keyframe timeline lanes 2026-07-16 18:12:12 -04:00
Miguel Angel Simon Sierra 4e80941969 refactor(studio): consolidate animation inspector callbacks 2026-07-16 18:12:11 -04:00
Miguel Angel Simon Sierra 179f4c10af feat(studio): add timeline keyframe callbacks 2026-07-16 18:12:11 -04:00
Miguel Angel Simon Sierra f284fb41d5 feat(studio): add keyframe track headers 2026-07-16 18:12:10 -04:00
Miguel Angel Simon Sierra dba0a517d3 feat(studio): add timeline property lanes 2026-07-16 18:12:10 -04:00
Miguel Angel Simon Sierra 84799219e7 feat(studio): add timeline keyframe interactions 2026-07-16 18:12:10 -04:00
Miguel Angel Simon Sierra ff47c390e8 feat(studio): add variable timeline row geometry 2026-07-16 18:12:09 -04:00
Miguel Angel Simon Sierra 050bd2042a feat(studio): add keyframe timeline state 2026-07-16 18:12:09 -04:00
Miguel Angel Simon Sierra ee9141d04d feat(studio): add keyframe ease inspector 2026-07-16 18:11:37 -04:00
Miguel Angel Simon Sierra 0e07d599d7 feat(studio): add keyframe ease controls 2026-07-16 18:09:30 -04:00
Miguel Angel Simon Sierra 5304a03c4e chore(studio): keep thumbnail store under size limit 2026-07-16 16:37:12 -04:00
Miguel Angel Simon Sierra 1d59e84b23 test(studio): enable thumbnails in the empty-label test (fixes pre-existing failure)
The thumbnails-off-by-default toggle (6b5ea8257) left this test asserting
thumbnail content without enabling thumbnails, so it produced no thumbnail
content and failed. Enable thumbnailsEnabled for the case and reset it in
afterEach; the label ownership assertion is unchanged (TimelineClip owns names).
2026-07-16 16:37:11 -04:00
Miguel Angel Simon Sierra 5274e4d19f fix(studio): resolve Chrome for dev thumbnails via env + puppeteer cache
Dev-server thumbnail generation only probed three hardcoded system Chrome paths,
so on a machine with no system Chrome install (but a Puppeteer-cached
Chrome-for-Testing) it found no browser and every thumbnail request 500'd.
Resolve the executable from PUPPETEER_EXECUTABLE_PATH / CHROME_PATH / CHROME_BIN,
then a broader set of system browsers, then the Puppeteer cache (highest
version).
2026-07-16 16:37:11 -04:00
Miguel Angel Simon Sierra e8a9c7a184 feat(studio): timeline clip thumbnails off by default with a toolbar toggle
Background thumbnail generation (per-clip composition/video frame capture)
lags timeline scrubbing on long videos (#2428). Adds a persisted
thumbnailsEnabled setting, default OFF, so the timeline is snappy out of
the box; a toggle next to Fit in the zoom toolbar turns thumbnails on.
When off, clips render as plain bars (audio still shows its waveform).
2026-07-16 16:37:11 -04:00
James 68beec0e76 fix(studio): guard storyboard history navigation 2026-07-16 15:49:40 -04:00
James ded443647d fix(studio): address storyboard review feedback 2026-07-16 14:56:11 -04:00
James 73dd38f93e feat(studio): clarify storyboard review handoff 2026-07-16 14:56:11 -04:00
Vance Ingalls 8a793ba2b2 fix(studio): gate Layout and Style panel sections on the element having a rendered box
Both panels showed Layout (X/Y/W/H/Rotation/Z-index) unconditionally —
no gate existed for it at all — and Style was gated only on
canEditStyles (a permission check), never on the element's tag. Neither
gate accounted for `<audio>`, which never paints a visual frame, so a
music track's inspector showed a full set of position/size/fill/shadow
controls with zero visual effect.

Add `layout`/`style` applicability to resolveEditingSections (core),
keyed on tag !== "audio", and gate both panels' Layout section and the
existing Style gate on it. Media/Motion/Grade/Text were already
correctly gated (verified via a research pass across both panels) and
are untouched.
2026-07-16 01:47:46 -07:00
Vance Ingalls 14dbfca5a7 fix(studio): give flat Text section rows more vertical breathing room
6px between rows left little clearance above a row's value for the
promote-to-variable badge (now positioned above the row). Widen the
row gap to 10px.
2026-07-16 01:17:04 -07:00
Vance Ingalls 5f045b6bdf fix(studio): move the promote-to-variable badge above the row instead of a padding gutter
Shrinking the wrapped control's width to make room for the badge (previous
commit) fixed the overlap but pushed the value left unnecessarily. Move the
badge to sit above the row instead, clearing the value without touching its
layout.
2026-07-16 01:11:30 -07:00
Vance Ingalls 9fa1b26570 fix(studio): reserve a gutter for the promote-to-variable badge
PromotableControl absolutely-positions its "◇ var" / "◆ {id}" badge over
the wrapped control without reserving any space, so on rows where the
value renders flush to the right edge (flat Font/Color rows) the badge
sat directly on top of the value text instead of beside it. Add a
right-padding gutter on the wrapper sized to each badge state, and cap
the bound chip to a fixed max-width so it always fits inside its gutter.
2026-07-16 01:06:11 -07:00
Vance Ingalls d5a259a3d2 fix(studio): make flat inspector's Stroke width free of style-name typing
Stroke width committed border-width and border-style together from one
free-text field, so setting a style meant typing an exact CSS keyword
(e.g. "dashed") with no indication of which ones were valid — the row
also duplicated the discoverable Stroke style select directly below it.
Stroke width now only commits border-width; style changes go exclusively
through the existing select.
2026-07-16 00:48:53 -07:00
Vance Ingalls b32f9a3e86 fix(studio): right-align FlatRow's value input in the flat inspector
FlatRow lays out label…gap…value across a `justify-between` row, but the
shared CommitField input it wraps had no text-align, so its text hugged
the LEFT edge of the value's own (often much wider) right-hand box —
looking left-aligned relative to the row, out of step with FlatSelectRow
and FlatSlider, which already right-align.

Added an optional `align` prop to CommitField (default "left", preserving
the legacy panel's MetricField/DetailField layouts where label-then-value
sits inline and left reads naturally) and pass `align="right"` from
FlatRow. Left the Motion Timing row's Start/End/Duration cells alone —
those stack label-above-value in a grid, a different pattern from the
inline label…value row this fix targets.

New tests: FlatRow's input has `text-right` (not `text-left`); the legacy
MetricField's input keeps `text-left` (not `text-right`), pinning
CommitField's default so the shared component doesn't drift for the
panel that didn't ask for this.

Full studio suite (2645 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-16 00:48:53 -07:00
Vance Ingalls 578d6202b4 fix(studio): resync the shared SDK session after a Design-panel variable promote
Reported as "template variables are broken": binding an element's field to a
variable via the flat inspector's "◇ var" promote chip (or editing an
already-bound field's value) wrote the correct bytes to disk, but the
Variables tab kept showing the pre-edit value until the whole Studio page
was hard-reloaded.

Root cause: DesignPanelPromoteProvider deliberately opens its OWN SDK
session (`useSdkSession(projectId, selection.sourceFile ?? activeCompPath)`)
so that promoting inside a sub-composition binds the variable in the
sub-comp's own file, not the host's. For the common case — a top-level
element, same file as `activeCompPath` — this session is a SEPARATE
in-memory `Composition` instance from the shared one `VariablesPanel`
(Variables tab, Slideshow, etc.) reads. A persist through the promote
provider's session never fires the shared session's own "change" event.

Worse, the shared session's file-change listener runs
`isSelfWriteEcho(path, content)` to decide whether to reload — but
`sdkSelfWriteRegistry` is keyed by file path only, not by session instance
(its own doc comment assumes "the studio process has a single SDK session
lifecycle at a time"). It sees the promote provider's write registered
under the same path and concludes it's its own echo, permanently
suppressing the reload it actually needs.

Threaded `forceReloadSdkSession` (the same mechanism every other
server-side-write path in Studio already uses for exactly this "resync
after a write I didn't make myself" case) from App.tsx through
StudioRightPanel into DesignPanelPromoteProvider, and call it after every
successful promote/setDefault persist — unconditionally, not gated on the
promote target matching activeCompPath, since re-opening a file that
didn't change is a harmless no-op re-parse and a path-equality guard here
already produced one subtly wrong comparison (activeCompPath can be null
while the shared session still defaults to "index.html") before landing on
this simpler version. Verified live: editing a variable-bound field's
value now updates the Variables tab immediately, no reload required.

App.tsx crossed the 600-line file-size gate after threading the new prop;
extracted the tiny handleAddAssetAtPlayhead wrapper into its own
useAddAssetAtPlayhead hook (with a regression test) to bring it back under.

Full studio suite (2639 tests) green against a fresh main; typecheck/
oxlint/oxfmt clean.
2026-07-16 00:18:36 -07:00
James Russo 21cb722ebd refactor(core): unify composition contract (#2157)
* refactor(core): unify composition contract

* fix(parsers): parse start expressions linearly
2026-07-16 02:44:22 -04:00
Miguel Ángel 0f287ee0b6 chore: release v0.7.60 2026-07-16 05:26:57 +00:00
Vance Ingalls 2b51c52634 fix(studio): give flat-inspector value fields a resting-state input affordance
Every editable value in the flat inspector (FlatRow's CommitField, the
Motion Timing row's Start/End/Duration cells, and every raw <select> —
Style/Text dropdowns, Grade's Custom LUT and Copy-grade-to scope) rendered
its underline/border only on hover (`border-transparent
group-hover:border-...` or no border at all). At rest a value looked like
plain static text, with nothing distinguishing it from a label — testers
reported not being able to tell which fields were editable.

Give each a dim-but-visible resting border (`border-panel-border-input/50`,
or `border-panel-accent/30` for the explicitCustom tier) that brightens on
hover/focus, instead of a fully transparent one. Purely visual — no
behavior change.

Full studio suite (2641 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-15 20:54:07 -07:00
Vance Ingalls fb24ecac22 fix(studio): make setRightPanelTab itself flat-aware, not just the direct tab click
Review feedback on #2497 (Rames D Jusso) found a real gap: the exclusivity
this PR introduced only applied to the direct in-panel tab click, which
calls setExclusiveRightInspectorPane. Every OTHER caller that reaches
setRightPanelTab("design"|"layers") — element select (useDomSelection.ts),
closing block-params (App.tsx), the header Inspector button
(StudioHeader.tsx), and even this PR's own "!inspectorTabActive" entry
branch in handleInspectorPaneButtonClick — went through
trackedSetRightPanelTab's old unconditional additive
`{...panes, [tab]: true}`, reproducing the exact "both tabs highlight, only
one renders" bug this PR claims to fix. Confirmed via the reviewer's traced
repro: fresh boot, click Layers tab while no inspector tab is yet active →
rightInspectorPanes ends up {design:true, layers:true}.

Fixed at the reviewer's preferred choke point: trackedSetRightPanelTab
itself is now flat-aware, applying the same exclusive-radio update
setExclusiveRightInspectorPane does whenever STUDIO_FLAT_INSPECTOR_ENABLED
is on, falling back to the legacy additive update otherwise. This closes
the gap for every current and future caller of setRightPanelTab, not just
the one call site this PR touched.

New usePanelLayout.test.ts cases pin both directions: setRightPanelTab
stays additive under flat=off (legacy split-view behavior unchanged), and
enforces exclusivity under flat=on even when called directly (not through
the tab-click handler) — using the vi.doMock(manualEditingAvailability)
pattern already established in PropertyPanel.test.tsx for flag-dependent
module state.

Full studio suite (2643 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-15 20:53:47 -07:00
Vance Ingalls 79b688f204 fix(studio): show Layers full-height in the flat inspector, not split with Design
The flat inspector split Layers and Design into a vertically-resizable
stacked pair whenever both panes were toggled on, mirroring the legacy
panel's layout. For the flat redesign this reads as two competing panels
crammed into one column; Layers should always render full-height by
itself there instead.

Gate the split-view branch behind !STUDIO_FLAT_INSPECTOR_ENABLED so it
still applies to the legacy panel, and fall through to Layers rendering
alone (the existing `layersPaneOpen` branch already does this — it just
never got reached previously because the split check ran first).

Also added setExclusiveRightInspectorPane (radio-style: selecting one pane
turns the other off) and use it for the Design/Layers tab clicks under the
flat flag, since leaving both panes independently toggleable would highlight
both tabs as "active" while only one actually renders.

New usePanelLayout.test.ts covers both the existing toggle behavior and the
new exclusive variant. Full studio suite (2634 tests) green; typecheck/
oxlint/oxfmt clean.
2026-07-15 19:05:38 -07:00
James Russo 2417293dab fix(studio): enforce optimistic file concurrency (#2156)
* fix(studio): enforce optimistic file concurrency

* fix(studio): harden conditional file writes

* fix(studio): honor explicit file preconditions

* test(producer): allow zero-ms encode timing
2026-07-15 18:07:04 -04:00
Vance Ingalls ff3b1541e5 chore: release v0.7.59 2026-07-15 11:57:49 -07:00
Miguel Ángel e038cc93c6 fix(studio): mute composition hover previews (#2478)
* fix(studio): mute composition hover previews

* fix(studio): route hover muting through bridge
2026-07-15 12:02:36 -04:00
James Russo 42055296ee fix(studio): make sdk cutover transactional (#2155) 2026-07-15 10:29:22 -04:00