mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
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.
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
/**
|
|
* Consumes playerStore.clipRevealRequest: when another surface (the sidebar
|
|
* asset card / audio row) asks for a clip to be revealed, smooth-scroll the
|
|
* timeline's scroll container so that clip is visible — horizontally to its
|
|
* time and vertically to its lane.
|
|
*
|
|
* The request is consumed (cleared) whether or not the clip node is found, so
|
|
* a stale request can never replay a scroll later. Respects zoom mode: in
|
|
* "fit" the timeline disables horizontal scrolling (overflow-x-hidden), so
|
|
* only the vertical axis is scrolled there.
|
|
*/
|
|
import { useEffect } from "react";
|
|
import { usePlayerStore } from "../store/playerStore";
|
|
import { GUTTER, RULER_H } from "./timelineLayout";
|
|
import { computeRevealScroll } from "./timelineRevealScroll";
|
|
|
|
export function useTimelineRevealClip(scrollRef: React.RefObject<HTMLDivElement | null>): void {
|
|
const revealRequest = usePlayerStore((s) => s.clipRevealRequest);
|
|
|
|
useEffect(() => {
|
|
if (!revealRequest) return;
|
|
// Consume the request first — reveal is one-shot, even when the clip node
|
|
// isn't currently rendered (e.g. drilled into a different composition).
|
|
usePlayerStore.getState().clearClipRevealRequest();
|
|
|
|
const container = scrollRef.current;
|
|
if (!container) return;
|
|
const clip = container.querySelector(`[data-el-id="${CSS.escape(revealRequest.elementId)}"]`);
|
|
if (!(clip instanceof HTMLElement)) return;
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
const clipRect = clip.getBoundingClientRect();
|
|
const clipLeft = clipRect.left - containerRect.left + container.scrollLeft;
|
|
const clipTop = clipRect.top - containerRect.top + container.scrollTop;
|
|
|
|
const target = computeRevealScroll({
|
|
scrollLeft: container.scrollLeft,
|
|
scrollTop: container.scrollTop,
|
|
viewportWidth: container.clientWidth,
|
|
viewportHeight: container.clientHeight,
|
|
clipLeft,
|
|
clipRight: clipLeft + clipRect.width,
|
|
clipTop,
|
|
clipBottom: clipTop + clipRect.height,
|
|
stickyLeft: GUTTER,
|
|
stickyTop: RULER_H,
|
|
allowHorizontal: usePlayerStore.getState().zoomMode === "manual",
|
|
});
|
|
if (target.left === null && target.top === null) return;
|
|
container.scrollTo({
|
|
left: target.left ?? container.scrollLeft,
|
|
top: target.top ?? container.scrollTop,
|
|
behavior: "smooth",
|
|
});
|
|
}, [revealRequest, scrollRef]);
|
|
}
|