Commit Graph
50 Commits
Author SHA1 Message Date
Ular Kimsanov 1f9a8f0985 Merge pull request #2756 from heygen-com/feat/studio-preview-layout
refactor(studio): simplify preview workspace layout
2026-07-25 00:33:34 -07:00
Ular Kimsanov 05c30292cf Merge pull request #2755 from heygen-com/feat/media-treatment-studio
feat(studio): add media treatment inspector
2026-07-24 21:20:01 -07:00
Ular Kimsanov 359a13fe6d Merge pull request #2754 from heygen-com/feat/media-treatment-overlays
feat(registry): add media treatment overlays
2026-07-24 19:52:06 -07:00
Ular Kimsanov 3978b8e8c9 Merge pull request #2753 from heygen-com/feat/media-treatment-agent
feat(cli): add agent-first media treatment workflow
2026-07-24 19:33:31 -07:00
Ular Kimsanov 7cf64164ed Merge pull request #2752 from heygen-com/feat/media-treatment-runtime
feat(runtime): render media treatments deterministically
2026-07-24 12:05:14 -07:00
Ular Kimsanov 9e7ddb0188 Merge pull request #2751 from heygen-com/feat/media-treatment-schema
feat(core): define media treatment capabilities
2026-07-24 09:58:05 -07:00
Ular Kimsanov d82cc1cf76 Merge pull request #2670 from heygen-com/codex/fix-flat-keyframe-retime
fix(studio): retime flat tween keyframe diamonds
2026-07-20 21:32:58 -07:00
Ular KimsanovandMiguel Angel Simon Sierra 89db718899 feat(studio): mirror canvas z-order actions into timeline lanes (track order = default paint order) (#2380)
* feat(studio): mirror canvas z-order actions into timeline lanes, badge z overrides

Track order = default paint order; authored z = advanced override.

- timelineZMirror.ts: pure resolver mapping a successful z-menu action to a
  timeline lane move — closest track in the action's direction that is free
  over the clip's whole span, else a new lane adjacent to the crossed
  neighbor; temporal-overlap scope (default pending product sign-off, see
  module doc); visual zone only; same-file reference scoping; persistTrack
  via the shared authored-space rules. null for non-clips (menu stays
  z-only) and at-extreme/no-overlap cases.
- useCanvasZOrderTimelineMirror.ts: after the z commit resolves, the mirror
  persists the lane move through the same machinery as a timeline lane drag
  (optimistic store update, authoredTrack refresh, rollback); inserts reuse
  commitTrackInsert's renumber via a shared buildTrackInsertEdits core. Both
  writes share one coalesce key (zReorderCoalesceKey) and fold into ONE undo
  entry (test proves it over the real history reducer). The mirror never
  triggers the lane->z stacking sync, so it cannot fight the z values the
  action just set.
- timelineZOverride.ts + TimelineClip badge: clips whose paint order
  contradicts lane order among temporally-overlapping same-context visual
  neighbors (laneIsAbove XOR paintsAbove, the stacking-sync predicates) show
  a 'z' badge — authored z overrides are surfaced instead of silently
  disagreeing with the timeline.
- Timeline.tsx track derivations extracted to useTimelineTrackDerivations
  (600-line cap).

* fix(studio): fold mirrored z-order gestures into one undo entry across slow persists

Live verification caught the z write and the mirrored lane write splitting
into two undo entries: the mirror runs after the z persist's server round
trip, which exceeds editHistory's default 300ms coalesce window under real
latency (the unit test's deterministic clock sat inside it).

zReorderCoalesceKey now mints a per-gesture-unique key (monotonic seq, the
laneChangeGestureSeq precedent) and both records carry coalesceMs Infinity —
distinct gestures can never merge, and one gesture always folds regardless
of write latency. coalesceMs threaded through the persist chain alongside
coalesceKey. Also hardens the existing lane-drag move->z fold, which had the
same latent split. Fold test now simulates a 400ms gap (failed before the
fix, passes after); a two-separate-gestures test asserts two entries.

* feat(studio): flashless lane mirror, z-order menu icons, close-gap track menu

- Track-only batch moves (the z-mirror's lane hop and the insert renumber)
  skip the GSAP fallback round-trip and the preview reload entirely — the
  renderer never reads data-track-index, and the live DOM patch + optimistic
  store update cover the UI. Mixed batches keep current behavior. Kills the
  canvas blink on mirrored Bring/Send actions (live-verified: an
  iframe-scoped marker survives the whole gesture).
- The four z-order menu items get 16px stroke icons (single layer diamond +
  directional arrow for Forward/Backward; pierced two-layer stack for
  Front/Back); labels unchanged — they are the industry-standard names.
- New track context menu on empty lane space: 'Close gap' (shifts the next
  clip and every clip after it on that lane left by the clicked gap's width;
  leading gaps count, so a single clip with empty space before it compacts
  to 0) and 'Close all gaps' (whole lane contiguous from 0). Pure gap math
  in timelineGaps.ts; persists through the drag path's atomic batch move
  (one undo per action); refuses when a clip that must shift is locked;
  items disable when there is nothing to close.

* fix(studio): rebind-only preview sync for unmutated timing edits, classical z-menu order

Timing edits that rewrote NO GSAP positions (gap closes and moves of
selector-addressed caption clips, zero-delta batches, comps without a
rewritable script) full-reloaded the preview — and the rerun-current-scripts
attempt was wrong for real compositions: re-executing init-style scripts
(three.js scenes, caption engines) is exactly the unsafe case, verified live
by doubled init warnings and a fallback reload anyway.

The correct observation: when mutated === false the existing __timelines are
still valid — only the runtime's clip visibility windows are stale, and the
live DOM timing attributes were already patched. So the no-mutation path now
runs applySoftReloadFinalization only (seek + __hfForceTimelineRebind +
manual-edits reapply), extracted from the soft-reload machinery — zero
script execution. This also un-blinks comps with no GSAP script at all,
which previously always remounted. Rewritten-script soft reloads,
cannot-soft-reload, otherFileChanged, and mutation failures keep their
existing behavior. gsapSoftReload's undo/redo restore section moved verbatim
to gsapUndoRestore.ts for the 600-line cap.

Also: z-order menu items reordered to the classical arrangement (Bring to
Front, Bring Forward, Send Backward, Send to Back).

Live-verified on a three.js-heavy composition: Close-all-gaps shifted 4
caption clips with correct cumulative amounts, the preview iframe was never
remounted (marker survived), and one undo reverted everything.

* fix(studio): bound forward/backward mirror to a one-element step

User-specified semantic: Bring Forward / Send Backward move the clip past
EXACTLY ONE element. The mirror's lane target is now bounded by the next
temporally-overlapping element beyond the crossed neighbor: a free lane
strictly between the two is taken (closest to the neighbor), and when they
are back-to-back a new track is inserted immediately beyond the crossed
element — never past the second one. Previously the resolver took the
closest free lane anywhere beyond the neighbor, which could carry the track
past a second element while the z action only stepped past one — a
track/paint contradiction our own zOverride badge would flag. Front/back
keep whole-set semantics (past everything; back stays above the audio
zone). End-to-end test pins the 3-stacked case through commitZMirrorLaneMove
to the persisted renumbered tracks.

* feat(studio): permanent gap-menu rows with hover and click-select gap highlights

- TrackGapContextMenu always renders both rows; an inapplicable action dims
  with a tooltip ("No gap here" / lock reason / "No gaps on this track")
  instead of vanishing into a one-item menu. Width badge only when a gap
  exists under the pointer.
- Hovering an ACTIONABLE row highlights the strip(s) it would close in the
  timeline: the single gap for Close gap, every current gap (leading included)
  for Close all gaps. New resolveAllGapIntervals in timelineGaps.ts reports
  present-state intervals (epsilon-tolerant, overlap-safe), distinct from
  resolveAllTrackGaps' post-compaction starts.
- Click-selecting a single clip paints a quieter tint over its lane's gaps
  (suppressed for marquee multi-selection and during drags; the gap-menu hover
  wins on its own lane). Derivation lives in useTimelineGapHighlights with the
  pure buildTimelineGapStrips exported and unit-tested.
- Strips render in TimelineCanvas with the drop-placeholder geometry (row top
  + clip inset), dashed accent for hover, faint tint for selection.
- Timeline.tsx stayed under the 600-line cap by extracting the scroll-viewport
  plumbing (ResizeObserver width + shortcut-hint sync) into
  useTimelineScrollViewport, behavior unchanged.

* feat(studio): stronger capcut-style timeline zoom steps

One button press / pinch gesture now moves the zoom meaningfully: step
factors 1.25x/0.8x -> 1.5x/(2/3) (kept reciprocal so in+out round-trips) and
pinch sensitivity 0.0035 -> 0.007. Addresses "zooming several times to get
anywhere" feedback; cursor anchoring unchanged.

* feat(studio): three-way z sync — layers drags mirror timeline lanes, panel tracks live z edits

Completes the layers/canvas/timeline sync triangle: the Layers panel was the
one surface whose reorders never reached the timeline, and the one that went
stale when the other two wrote z flashlessly.

- Layers drag -> minimal z + equal-jump lane mirror. handleReorder now uses
  the canvas menu's realization core via resolveZOrderReposition (one
  between-z write when a strict gap exists, band-safe scoped renumber
  otherwise) instead of computeReorderZValues' all-sibling stamp — that
  helper is deleted, completing the #2347 unification follow-up. The drop
  then mirrors into a timeline lane move through the same machinery as the
  canvas menu (new resolveRepositionLaneMove: the clip lands on a free lane
  strictly between its NEW paint neighbors' lanes — nearest clip siblings in
  the desired render order, decorations skipped — else a track insert at
  that boundary; audio zone never crossed). Both writes share one
  per-gesture zReorderCoalesceKey with an unbounded fold window, so a drag
  is exactly ONE undo entry; useCanvasZOrderTimelineMirror's plumbing is
  factored into useMirrorLaneMoveCommit and reused by the new
  useLayerReorderTimelineMirror. A same-slot drop is a hard no-op (new
  order-equality guard in resolveZOrderReposition).
- Panel staleness fix: flashless z commits (skipReload) reload nothing and
  bump no refreshKey, so the panel's z-sorted order went stale while paused.
  handleDomZIndexReorderCommit now bumps a store zEditVersion on apply AND
  rollback; the panel re-collects on it. Verified live: the panel re-sorts
  the instant a drag commits and again on undo.
- Layer click reveal (useLayerRevealOverride): clicking a layer that stays
  hidden at the current frame (animation-parked opacity, non-clip
  display/visibility hides, hidden ancestors) temporarily forces the chain
  visible with live inline styles — exact priors restored on deselect, on
  another reveal, on play, and on unmount; never persisted (file diff == 0
  verified live). Clips keep the existing seek-into-window behavior; the
  override applies on a short defer so a seek-revealed clip needs none.
- layerOrdering's unused hasExplicitZIndex probe (zero callers) removed.

Live-verified on a bed copy: a 2-position layers drag wrote exactly one
element (z 6->23 + data-track-index 15->2), the timeline lane moved without
a reload, and a single Cmd+Z restored the file byte-identically.

* feat(studio): full-track selection highlight, borderless gap hover strips

- Click-selecting a clip now lights the WHOLE lane minus its clips — leading
  gap, inter-clip gaps, and the open space after the last clip to the rendered
  end (new resolveLaneEmptyIntervals; displayDuration threaded into the strip
  derivation). Still click-only: any drag/resize suppresses the strips, and a
  marquee multi-select never shows them.
- The gap-menu hover strips drop the dashed border (user feedback) — fill only,
  nudged to 0.18 alpha to keep the same visual weight.

* feat(studio): selected layer paints on top via a reader-transparent z lift

Clicking a layer in the Layers tab now shows the element as if it were at the
very top of the stack while selected — whatever its authored z or panel
position — extending the reveal override (which already forced hidden chains
visible) with a temporary inline z lift:

- liftElementToTop parks the TRUE effective z in data-hf-reveal-prior-z and
  writes a far-top inline z; a static element gets a layout-preserving
  position:relative with its prior parked in data-hf-reveal-prior-pos. Only
  the RENDERER sees the lift: all three studio z readers
  (readTimelineElementZIndex, getElementZIndex, readEffectiveZIndex) return
  the parked prior while the attribute is present, so the canvas z-menu, the
  zOverride badge, the lane mirror, the stacking sync, and the panel sort
  keep reasoning on the element's real z.
- Strictly ephemeral: exact priors restored on deselect / another reveal /
  play / unmount, each property only while it still holds the value the
  override wrote (a later real edit is never clobbered). File diff == 0
  verified live across a full lift/restore cycle.
- A z-reorder commit CONSUMES an active lift (handleDomZIndexReorderCommit
  reads the parked position for its persist-position:relative static check,
  then drops the attributes) — the committed z becomes the truth and the
  later restore is a guarded no-op.

* fix(studio): flashless undo/redo — three full-reload causes in the soft-restore path

Cmd+Z blinked the canvas on essentially every undo. Three independent causes
in applyUndoRestoreToPreview, each sufficient on its own:

1. Master-view path gate: activeCompPath is NULL at the master view, so the
   'paths[0] === activeCompPath' eligibility check could never match the
   index.html restore and every default-view undo full-reloaded at the first
   gate. Normalized to the codebase-wide 'activeCompPath ?? "index.html"'.
2. Nested identity innerHTML check: the diff compared each identified
   element's innerHTML, but the composition root wraps every clip — any child
   change re-detected at the root rejected the restore. Change detection now
   compares only each element's OWN attribute surface; structure/text
   integrity is still guaranteed by the normalize-residual whole-doc pass
   (text nodes, added/removed elements, and un-identified attrs all remain
   after normalization and force the full reload).
3. id-only identity: elements addressed by data-hf-id / selector (no DOM id)
   fell outside the diff entirely. Identity is now id OR data-hf-id, with the
   live sync resolving either.

Also stop re-running an UNCHANGED GSAP script: attribute-only restores (z,
lane, timing, style — the overwhelmingly common undo) now use the rebind-only
finalization (seek + __hfForceTimelineRebind + manual reapply, zero script
execution — the same path as flashless timing edits), instead of tearing down
and rebuilding live timelines or full-reloading when the script can't be
scoped. A restore whose script text genuinely changed still re-runs it via
applySoftReload, and structural restores (split/delete) still full-reload.

Live-verified on the bed (iframe marker): gap-close undo AND redo both keep
the iframe mounted, live DOM lands on the restored values, disk restored
byte-identically.

* feat(studio): left breathing pad before t=0, double zoom sensitivity again

TRACKS_LEFT_PAD (48px) — the horizontal sibling of TRACKS_TOP_PAD: empty lane
surface between the sticky gutter and the ruler's 00:00 / the first clips,
scrolling WITH the content.

- The lanes and the ruler realize it as a plain flow spacer between the
  sticky gutter cell and the time-mapped content div, so every
  content-relative computation (clip left = t*pps, beat lines, lane-menu
  time, clip drag deltas) is untouched by construction.
- Canvas-space overlays shift by the pad: playhead (getTimelinePlayheadLeft),
  gap strips, drop placeholder, snap guide, range highlight, marquee clip
  rects, beat SVG; the insert line spans the pad.
- Every pointer->time inverse subtracts it symmetrically: seekFromX, razor,
  range/marquee anchors, asset drops, and the zoom-anchor gutter basis; fit
  pps and the display width account for the consumed viewport width.
- Live-verified: t=0 clip edge, the 00:00 tick, and the playhead line center
  all sit at GUTTER + TRACKS_LEFT_PAD, and a ruler click lands the playhead
  center exactly under the pointer.

Also doubles the timeline zoom sensitivity again (user feedback after
feel-testing the first bump): button steps 1.5x/(2/3) -> 2x/0.5, pinch
0.007 -> 0.014.

* fix(studio): left pad renders as true empty space, not lane surface

The pad before t=0 inherited each row's background and bottom border from the
row wrapper, so it read as track lanes. Lane visuals now live on the cells:
the sticky gutter keeps its own separator (header column stays delineated),
the time-mapped content div carries the row background + separator, and the
pad spacer stays transparent — bare shell background, no lines. The
new-track insertion line also starts at the pad's end instead of crossing it.

* fix(studio): no vertical line in the ruler band before 00:00

The ruler corner's right border drew the header-boundary line through the
ruler strip, so the band didn't read as starting at 00:00. Dropped it — the
boundary line belongs to the track rows below; the ruler stays completely
clean from the panel edge to the first tick, matching the empty left pad.

* refactor(studio): remove the timeline z-override badge

User decision: the "z" chip on clips never earned its place — dropped
entirely (timelineZOverride.ts + test deleted, TimelineClip badge rendering
and the zOverrideKeys derivation/threading removed). This also eliminates the
review's D2 finding at the root: the badge's cross-document comparison
(stackingContextId ?? null collides across source files in the expanded view)
produced false positives, and there is no longer a detector to mis-fire.
overlapsInTime/paintsAbove lose their export (the badge was their only
external consumer); the paint-order predicate itself is unchanged.

* fix(studio): collision-free expanded child lanes and host-window gap floors

Review findings D1 (blocker) and 4.

- D1: buildChildElements assigned expanded children synthetic display rows as
  `host.track + index` — integers that can EQUAL a real clip's lane in another
  file (host on 0 with two children puts child #2 on 1). Lane grouping merges
  purely by track number, so the collision fused clips from different source
  files into one display lane, and lane-scoped actions (the gap menu) then
  batch-persisted a foreign file's clip. Children now take FRACTIONS strictly
  between the host's lane and the next integer — structurally unable to
  collide with any normalized lane, while still rendering as ordered rows
  under the host. Regression test pins the reviewer's exact two-file scenario.
- Finding 4: gap math compacted toward absolute 0, but an expanded child's
  display time is host-anchored — close/compact could drag it before its host
  window and persist a wrong (even negative) local time. All gap functions
  now take a lane FLOOR (laneGapFloor: 0 for ordinary lanes, the children's
  expandedParentStart for child lanes — single-origin per lane post-D1),
  threaded through the menu model, hover highlights, selected-lane strips,
  and both commits. Close-gap shifts clamp at the gap's own left edge.

* fix(studio): scope mirror references, insert writes, and crossed-neighbor identity

Review findings 1, 2, and 3.

- Finding 1: buildTrackInsertEdits normalized the FULL display set and
  persisted every shifted clip — writing host-lane numbers into OTHER
  composition files when expanded children were showing. The renumber write
  set is now the edited element's own source file (the sanctioned multi-write
  converges one FILE to lane space, never neighbors' files); foreign clips
  keep their authored tracks and re-derive display lanes. The locked-clip
  refusal scopes the same way. Expanded-origin elements refuse the insert
  outright (a new lane is a host-space renumber, meaningless in the child's
  file), and the mirrors restrict an expanded child's lane candidates to its
  own siblings' lanes — a sub-comp child still mirrors WITHIN its sub-comp
  (persisting the sibling's authored track) but can never land on a host lane
  with no same-file occupant. authoredTrackForLane's offset fallback rounds:
  fractional synthetic rows must never leak fractions into data-track-index.
- Finding 2: the mirror comparison sets required only sameSourceFile, but a
  file can contain several CSS stacking contexts and leaf z is only
  comparable within one. Both resolvers now scope by samePaintScope — same
  source file AND same stackingContextId (the file check also stops null root
  contexts of different files from comparing equal in the expanded view).
- Finding 3: the crossed-neighbor key was derived without selectorIndex, so
  duplicate class selectors (.sub) resolved to occurrence 0 — a different
  clip. The key now carries getSelectorIndex, matching how z-reorder entries
  derive theirs.

* fix(studio): z-to-lane gestures are one serialized transaction gated on durable persists

Review findings 5 and 7.

- Finding 5: commitDomEditPatchBatches resolved successfully even when the
  server matched NO patch target — the z write never reached disk (the
  preview reloads to reconverge) yet the lane mirror still ran, desyncing
  track order from what actually paints. The commit now resolves a durability
  report ({allMatched, changed}; the save queue and commit types are generic
  over the result), and the mirror phase is skipped on allMatched === false.
- Finding 7: the z persist rides the DOM-edit save queue while the lane move
  rides the timeline/SDK path — two queues, so a second rapid gesture's z
  write could land BETWEEN the first gesture's z and lane phases. Every
  z-to-lane gesture (canvas z-order menu AND Layers-panel drag) now runs
  through runZLaneGesture: a single module-level tail that serializes the
  COMPLETE two-phase transaction, with unit tests for ordering, the
  durability gate, and queue resilience to failed gestures. The timeline
  lane-drag's inverse (move-then-z-sync) shares its phases' await ordering
  already; cross-gesture serialization for that path is noted as follow-up.
- LayersPanel's pure sort helpers moved to layersPanelSort.ts (600-line cap).

* fix(studio): multi-clip GSAP batch mutations roll back on late failure

Review finding 6. finishGroupTimingGsapFallback mutates files sequentially
per clip; a late per-clip failure left the earlier rewrites on disk with no
aggregate history entry — unreachable by undo. foldGsapMutationIntoHistory
already snapshots every touched path before mutating; on a mutation failure
it now restores each path whose disk content changed (all-or-nothing batch),
reports restore errors without masking the original failure, and rethrows.
Regression test drives a two-clip batch whose second rewrite fails and
asserts the first clip's write is restored byte-identically.

* fix(studio): scope mirror inserts to their lane zone

* fix(studio): unify source-scoped clip identity

* fix(studio): isolate track insert topology

* fix(studio): harden timeline paint synchronization

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-07-14 14:31:58 -04:00
Ular Kimsanov 403ecb56d9 Merge pull request #2347 from heygen-com/fix/studio-restore-golden-behaviors
fix(studio): restore golden-branch timeline behaviors dropped by the stack rebuild
2026-07-13 17:13:37 -07:00
ukimsanov 512560b4c3 test(studio): delete-path duration rollback coverage, shared dismiss predicate on preview open 2026-07-13 16:48:52 -07:00
ukimsanov a33b3f35e1 fix(studio): address PR #2347 review findings (rounds 1-2)
Review 1 (restore commit):
- asset reveal now clears any open preview overlay (stuck-overlay repro:
  preview on A, click already-added B — A stayed open over the reveal)
- duration readout rolls back on failed persist: captureDurationRollback
  snapshots store + live root data-duration before the optimistic sync and
  restores both in every move/resize/delete/group catch (golden's
  previousDuration pattern)
- asset preview opened during running playback dismisses immediately (the
  RAF loop bypasses the store, so the subscription alone never fired)
- persistTimelineBatchEdit resolves the target (findTagByTarget) before
  treating identical output as a no-op — a mistargeted member now throws
  like the single-element path instead of being silently dropped
- a post-mutation history-fold failure no longer suppresses the preview
  sync: fold errors are surfaced separately and the rewritten script still
  syncs (previously the preview kept stale GSAP positions with no recovery)
- timelineRevealScroll guards degenerate viewports (windowSize <= 0)
- CodeQL: encodeURIComponent(projectId) at all timelineTimingSync fetches

Review 2 (single-source-of-truth pass):
- createTimelineElementFromManifestClip — the one manifest->element
  boundary — now carries authoredTrack and stackingContextId; expanded
  sub-comp children preserve both (authoredTrack in their OWN file's space)
- authoredTrackForLane scopes occupants to the dragged clip's sourceFile
  (a foreign file's authored values are a different coordinate space);
  nearest-same-file-lane offset fallback
- optimistic store updates mirror the persisted track into authoredTrack
  (and roll it back on failure), so consecutive drags before a reload
  resolve from fresh data
- spill sub-lanes: documented decision — dropping onto a spill lane is a
  legitimate same-track join (occupants share the authored track by
  construction); false 'never a lane-move target' docstring rewritten
- single-element fallback persists vertical-only moves (early return now
  requires neither start nor track changed; live DOM patch includes
  data-track-index)
- canonical contextKey helper for stacking-context normalization
- new pipeline test crosses the REAL factory boundary (sparse authored
  tracks -> factory -> expansion -> normalize -> drag commit -> persisted
  attribute), no injected fields
2026-07-13 16:48:52 -07:00
ukimsanov 760b88a6f3 fix(studio): flashless z-order commits and visible-overlap stepping
Two legibility fixes for the canvas z-order menu, from user feel-testing:

- z-only commits no longer remount the preview iframe. The commit hook
  already applies the inline z (+ injected position) to the live elements and
  updates the store synchronously; the post-commit reloadPreview() was a
  redundant full remount that read as a canvas 'blink' on every action.
  commitDomEditPatchBatches gains skipReload, engaged only when provably
  safe: every op is an inline-style patch AND the server reports every patch
  matched — anything else falls back to the reload so the preview reconverges
  with disk. The file-watcher's own reload stays suppressed by the existing
  domEditSaveTimestampRef window, so the skip is real.
- Bring Forward / Send Backward step over the next VISIBLY overlapping
  sibling. The nearest z-neighbor in a composition is often invisible at the
  current frame (runtime hides time-inactive clips with inline
  visibility/display; GSAP parks elements at opacity 0), so the step crossed
  something the user couldn't see — 'enabled but nothing happens'. The
  forward/backward set now filters on element-level computed visibility
  (display/visibility/opacity, injectable for tests); enable/disable shares
  the resolver so the menu is honest: actions disable when no visible
  neighbor exists. Front/back keep the full painting family.
- The neighbor that was stepped over gets a 600ms accent flash, drawn in the
  studio overlay layer (never in the iframe DOM), so the action shows its
  work.
2026-07-13 16:48:52 -07:00
ukimsanov 84963ea8ba fix(studio): persist canvas z-order actions correctly for static elements
An adversarial review of the canvas context-menu z-order pipeline (Bring to
Front / Forward / Backward / Send to Back) found the resolver math sound but
the glue between the menu and the commit hook broken:

- The menu optimistically wrote style.zIndex AND position: relative to the
  live elements BEFORE the commit hook ran. The hook decides whether to
  persist position by checking getComputedStyle(el).position === 'static' —
  always false after the pre-apply — so the position patch was never
  persisted on the menu path and the reorder silently reverted at the
  post-commit reload for any nested/static element (root clips survive only
  because the runtime forces position:absolute). The same pre-apply made the
  failure rollback capture the already-mutated values, restoring the broken
  state on persist errors. The menu no longer pre-applies; the hook owns the
  live writes (it already applied both synchronously) and now sees true
  priors. Siblings without a persistable identity still get their z applied
  live-only so a renumber stays visually coherent.
- The commit hook's entry.key store-sync plumbing had zero production
  callers; the store zIndex went stale until full reload. All three callers
  (canvas menu via PreviewOverlays, timeline lane z-sync, LayersPanel) now
  derive and pass the timeline store key (new deriveTimelineStoreKey helper).
- patchElementBatch discarded the server's per-patch matched[]; unresolvable
  siblings persisted partially and silently. Unmatched targets now warn and
  report save-failure telemetry (z-reorder-unmatched) without rolling back
  the matched subset.
- template/noscript elements counted as painting siblings, so renumber
  fallbacks wrote z-index/position into <template> tags in the source file.
  Excluded from the sibling family.
- The default undo coalesce key merged DISTINCT z actions within 300ms into
  one undo entry; the action kind is now part of the key (LayersPanel drags
  keep coalescing within a drag; explicit lane-move gesture keys untouched).
- rectsIntersect comment claimed touching rects intersect; the strict
  inequalities say otherwise — comment fixed.
2026-07-13 16:48:52 -07:00
ukimsanov 19139b91ed fix(studio): make vertical lane moves persist correctly and harden the z/lane pipeline
Vertical clip moves committed in the store but never survived: two persist
bugs plus a runtime renumber all fought the stable-track-lanes model.

- timelineMoveAdapter deliberately stripped the track from lane-reorder
  persists ('z-only reorder path' — the old z-driven lane model). Lane =
  authored data-track-index now: lane-reorder and track-insert both persist
  the track; plain timing moves omit it to stay SDK-fast-path eligible.
- Display lanes and file tracks are different coordinate spaces:
  normalizeToZones packs sparse authored tracks (1,2,... or gaps, or DOM-index
  fallbacks) onto contiguous display lanes, and lane edits persisted the LANE
  number — silently re-targeting the wrong row in any non-0-contiguous file.
  Elements now record their authoredTrack when remapped; a lane change
  persists the target lane's authored track (store stays in lane space).
- The runtime split same-track clips of different kinds (video vs caption
  div) onto separate renumbered tracks at discovery, so authored indices
  never round-tripped ('drop onto an existing track' bounced back). Removed:
  data-track-index is honored verbatim (render never reads it); kind-based
  row presentation belongs in the display layer if ever wanted.

Adversarial review fixes on the same pipeline:
- runtime: parseInt(attr) || fallback dropped authored track 0 for GSAP and
  overlay clips (parseAuthoredTrack helper honors 0)
- single-clip move fallback persisted only data-start — lane changes snapped
  back on reload (now passes the track to the patch builder)
- lane-change z-sync candidate ignored a multi-selection's time shift, so
  patches were computed against stale overlap sets
- track insert around a locked clip persisted a colliding renumber (the next
  normalize merged lanes); the insert is now refused with a warning
- computeStackingPatches compared leaf z across CSS stacking contexts, where
  ancestor z decides paint order; the sync now partitions by
  stackingContextId and never patches across contexts

Timeline geometry (user-reported):
- fit zoom leaves 20% trailing headroom (FIT_ZOOM_HEADROOM in
  timelineLayout.ts; single fit-pps source, so ruler/lanes/playhead/drag all
  inherit it)
- playhead line center now sits exactly on GUTTER + t*pps at every zoom
  (wrapper had shrink-wrapped to the 9px diamond, off-centering the line);
  ruler ticks center on their timestamp
- ruler: frame-mode steps snap to whole frames (no duplicate labels), hour
  steps added for far zoom-out, tick positions computed as exact multiples
  (no float drift)
2026-07-13 16:48:52 -07:00
ukimsanov 54f41b41b6 fix(studio): restore golden-branch timeline behaviors dropped by the stack rebuild
The Studio stack rebuild (#2291) landed the remaining NLE layers but dropped
or regressed several final-wave behaviors from the reviewed studio-dnd stack,
and never repaired the stale timelineZones.ts that #2279 introduced. Restores:

- TimelineRuler: sticky under vertical scroll, full-height gridlines removed
  (beat lines only), frame-number tick labels via a persisted timeDisplayMode
  store preference (PlayerControls toggle now store-backed)
- timelineZones: stable track lanes — lane = authored data-track-index
  ascending; z is paint order only (replaces the stale z-driven lane pack,
  which broke track insert-band commits that contractually depend on it)
- persistTimelineBatchEdit: a batch member whose patch is a no-op (attributes
  already at target values, e.g. in a track-insert renumber) is skipped
  instead of aborting and rolling back the whole batch — this alone made
  new-track creation (incl. the top insert band) fail silently
- useTimelineStackingSync: unresolvable clips read as NaN again so
  timelineStackingSync's Number.isFinite exclusion contract holds (z=0
  fabrications skewed stacking boundaries)
- timelineAssetDrop: drops land on the drop track (no overlap bump to
  max-track+1), data-hf-id stamped, audio gets data-volume
- timing edits: soft-reload the server's rewritten GSAP script instead of a
  full iframe remount (no all-clips flash on move/resize); full reload only
  when no scriptText or the soft path can't apply, and one full reload when a
  group edit touches non-active files (new hooks/timelineTimingSync.ts)
- duration: content-driven grow-AND-shrink on move/resize/delete, synced
  optimistically to the store and the live root data-duration at release
  (was a grow-only ratchet; shrink never updated the readout)

New UX: sidebar asset click opens a compact non-modal preview over the canvas
(dismiss on outside click, Escape, playback, or seek), and clicking an
already-added asset reveals its clip in the timeline (smooth minimal scroll
to its time and lane; vertical-only in fit zoom).

Verified by pointer-driving a real project: sticky ruler + gridline removal,
no iframe remount on move/resize (marker survives, GSAP tween positions
rewritten in place), duration readout 40->37->40 on shrink/stretch, and
top-insert-band track creation renumbering lanes correctly on disk.
2026-07-13 16:48:51 -07:00
Ular Kimsanov 7f21d5dc49 feat(studio): timeline collision and placement model (#2195)
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
2026-07-11 22:38:16 -04:00
Ular Kimsanov 58a36d7078 Merge pull request #2268 from heygen-com/studio-dnd/pr03-studio-server-files-route
feat(studio-server): files route extensions
2026-07-11 18:21:32 -07:00
Ular Kimsanov 3930460c51 feat(studio): seek-restore contract for player reloads (#2193)
* feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs

What: extends three leaf modules to their final NLE-stack form, tests in the
same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack),
timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and
studioUiPreferences (persisted editor prefs).

Why: leaf dependencies of the NLE timeline stack; landing them first keeps
the later glue PRs to wiring.

How: additive from the consumer side — every export main already uses is
unchanged (typecheck against main's consumers passes untouched); every new
export is exercised by a test in this PR.

Test plan: bunx vitest run on the three test files; tsc --noEmit in
packages/studio; fallow audit --base origin/main clean.

* feat(studio): seek-restore contract for player reloads

What: useTimelineSyncCallbacks gains resolveReloadSeekTime and revealIframe —
the pure contract for where the playhead lands after a preview reload
(pending seek > deep-link seek > store playhead, clamped) and for undoing
refreshPlayer's iframe hide. Test suite included.

Why: the NLE editor reloads the preview on every committed edit; this
contract is the difference between "playhead restores" and "jumps to 0".

How: additive exports on an existing hook file; main's consumers unchanged.

Test plan: bunx vitest run useTimelineSyncCallbacks.test.ts; tsc --noEmit in
packages/studio; fallow audit clean.
2026-07-11 20:33:07 -04:00
Ular Kimsanov 920dad35eb feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs (#2192)
What: extends three leaf modules to their final NLE-stack form, tests in the
same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack),
timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and
studioUiPreferences (persisted editor prefs).

Why: leaf dependencies of the NLE timeline stack; landing them first keeps
the later glue PRs to wiring.

How: additive from the consumer side — every export main already uses is
unchanged (typecheck against main's consumers passes untouched); every new
export is exercised by a test in this PR.

Test plan: bunx vitest run on the three test files; tsc --noEmit in
packages/studio; fallow audit --base origin/main clean.
2026-07-11 20:20:29 -04:00
Ular Kimsanov a1abd57a60 Merge pull request #2010 from heygen-com/fix/studio-selection-outline-only
fix(studio): remove selection overlay fill
2026-07-06 21:21:57 -07:00
Ular Kimsanov 972bedf062 Merge pull request #1988 from heygen-com/feat/color-grading-docs
docs: document Studio Color Grading
2026-07-06 15:40:46 -07:00
Ular Kimsanov c1aa35d069 Merge pull request #1987 from heygen-com/feat/studio-color-grading-inspector
feat(studio): add color grading inspector
2026-07-06 15:24:50 -07:00
Ular Kimsanov a4989dbe75 Merge pull request #1986 from heygen-com/feat/studio-media-processing-routes
feat(studio-server): add media processing routes
2026-07-06 13:39:01 -07:00
Ular Kimsanov 2757d32092 Merge pull request #1985 from heygen-com/feat/studio-color-grading-runtime
feat(runtime): render media color grading shaders
2026-07-06 13:18:06 -07:00
Ular Kimsanov e01ec16194 Merge pull request #1984 from heygen-com/feat/studio-color-grading-core
feat(core): add media color grading contract
2026-07-06 11:30:31 -07:00
Ular Kimsanov 94996d8da9 Merge pull request #1976 from heygen-com/docs-audit-fixes
docs: fix 53 inaccuracies across all documentation
2026-07-05 21:34:05 -07:00
Ular Kimsanov dd049b8985 Merge pull request #1618 from heygen-com/fix/page-side-paint-record
fix(shader-transitions): page-side render dropped shader transitions and final-scene content
2026-06-21 04:03:31 -07:00
Ular Kimsanov c4c978f5a8 Merge pull request #1527 from heygen-com/fix/capture-default-output
fix(cli): default `capture` output to ./capture (auto-suffix capture-2, capture-3 on re-run)
2026-06-16 23:13:57 -07:00
Ular Kimsanov badb6a040c Merge pull request #1515 from heygen-com/feat/color-grading
feat(studio): add color grading inspector controls
2026-06-16 15:32:37 -07:00
Ular Kimsanov f6a817a4e4 Merge pull request #1514 from heygen-com/feat/color-grading-runtime
feat(runtime): apply color grading in preview and render
2026-06-16 15:22:46 -07:00
Ular Kimsanov d4e64f9a28 Merge pull request #1513 from heygen-com/feat/color-grading-core
feat(core): add color grading schema and lut parsing
2026-06-16 15:10:11 -07:00
Ular Kimsanov 16a3d24fa9 Merge pull request #1475 from heygen-com/fix/capture-video-flag
fix(cli): restore `hyperframes capture <url>`; move video download to `--video` flag
2026-06-15 16:15:14 -07:00
Ular Kimsanov 3bfb25efcf Merge pull request #1447 from heygen-com/feat/cli-capture-video
feat(cli): capture-video on-demand fetcher + capture pipeline robustness
2026-06-15 01:54:01 -07:00
Ular Kimsanov e9c515dedd Merge pull request #1026 from heygen-com/fix/w2h-skill-audit
fix(skill): w2h audit + enforcement — close 19 shirking patterns from two agent debriefs
2026-05-22 14:20:01 -07:00
Ular Kimsanov 74b86f6cfa Merge pull request #1006 from heygen-com/feat/skill-hyperframes-core-v2
feat(skill): hyperframes core — remove prescriptive tables, reference text-effects (not vendor)
2026-05-21 13:09:13 -07:00
Ular Kimsanov b9e4ca169e Merge pull request #1005 from heygen-com/feat/skill-website-to-hyperframes-v2
feat(skill): website-to-hyperframes — concept-first authoring + per-beat read protocol
2026-05-21 12:43:40 -07:00
Ular Kimsanov eb3c9d7e7f Merge pull request #1003 from heygen-com/feat/capture-pipeline-improvements-v2
feat(capture): pipeline improvements — contact sheets, design styles, snapshot
2026-05-21 11:28:51 -07:00
Ular Kimsanov 12808fd38f Merge pull request #987 from heygen-com/feat/capture-font-extractor
feat(capture): identify hashed fonts via OpenType name table
2026-05-21 10:09:25 -07:00
Ular Kimsanov 16d2966cd9 Merge pull request #886 from heygen-com/feat/shader-optional-css-mix
feat(shader-transitions): optional shader field — CSS crossfade mixing in HyperShader
2026-05-19 19:17:53 -07:00
Ular Kimsanov bdc1012b9a Merge pull request #496 from heygen-com/fix/docs-claude-design-links
fix(docs): use claude.ai/design link, remove raw download option
2026-04-25 17:09:21 -04:00
Ular Kimsanov 9322ff9c74 refactor: move Claude Design instructions from skills/ to docs/ (#495) 2026-04-25 22:50:16 +02:00
Ular Kimsanov 309c74e864 Merge pull request #364 from ibrews/skill-load-bearing-gsap-rules
docs(website-to-hyperframes): add load-bearing GSAP authoring rules
2026-04-24 17:21:28 -04:00
Ular Kimsanov 35bb21f2b8 Merge pull request #469 from heygen-com/fix/shader-transitions-dynamic-resolution
feat(shader-transitions): support any aspect ratio (vertical, square)
2026-04-23 21:46:19 -04:00
Ular Kimsanov 60d1494279 Merge pull request #468 from heygen-com/fix/claude-design-docs-download-ux
docs: fix SKILL.md download UX across all references
2026-04-23 21:09:20 -04:00
Ular Kimsanov 95cc264fe7 Merge pull request #467 from heygen-com/feat/claude-design-skill-template-first
refactor(claude-design-skill): template-first rewrite with bug fixes
2026-04-23 20:59:24 -04:00
Ular Kimsanov 078a8b349b Merge pull request #456 from heygen-com/fix/shader-transitions-capture-robustness
fix(shader-transitions): harden capture against visibility, scrub, Safari taint
2026-04-23 20:46:43 -04:00
Ular Kimsanov b7a644e2c7 Merge pull request #348 from heygen-com/fix/snapshot-video-frame-injection
fix(cli): inject real video frames in snapshot to match render
2026-04-20 11:00:54 -04:00
Ular KimsanovandClaude Opus 4.7 a78ec5ec76 fix(skill): all file paths use capture/ subfolder prefix (#345)
* fix(skill): all file paths use capture/ subfolder prefix

Step-1 was updated to capture into <project-dir>/capture/ but all
file-read instructions across steps 1-6 still used bare paths
(extracted/tokens.json, assets/hero.png). Agents following the skill
looked for files in wrong locations, producing broken compositions
with 404'd images and fonts.

- step-1: all 12+ file-read paths prefixed with capture/
- step-2: tokens.json and asset-descriptions.md paths fixed
- step-3: visible-text.txt path fixed
- step-4: asset-descriptions.md, asset examples, directory tree expanded
  to show capture/ children (screenshots/, assets/, extracted/)
- step-5: minor cleanup
- step-6: asset-descriptions.md path fixed, composition examples already
  correct (../capture/assets/)
- techniques.md: Lottie, video, font paths all prefixed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(skill): address Copilot review — font comment path, add videos/ to tree

- techniques.md: comment says ../capture/assets/fonts/ (matches composition context)
- step-4 file tree: add videos/ under capture/assets/

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 05:01:25 +02:00
Ular Kimsanov 6a587606e4 Merge pull request #339 from heygen-com/feat/capture-improvements-v2
feat(capture): improve capture quality, clean CLAUDE.md, skill refinements
2026-04-19 18:31:50 -04:00
Ular Kimsanov a95539a0fd Merge pull request #299 from heygen-com/feat/capture-improvements-v2
fix: double-audio scaffold, lint rules, docs guide, Gemini 3.1
2026-04-17 14:49:05 -04:00