mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
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
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
/**
|
|
* Pure scroll-target math for revealing a timeline clip inside the timeline's
|
|
* scroll container (the overflow div in Timeline.tsx).
|
|
*
|
|
* Coordinates are content-space: a clip edge measured from the scroll
|
|
* container's content origin (rect delta + current scroll offset). The visible
|
|
* window on each axis is reduced by the sticky chrome that occludes it — the
|
|
* track gutter on the left (GUTTER) and the ruler on top (RULER_H) — so a clip
|
|
* "hidden" under the sticky gutter still counts as off-screen.
|
|
*
|
|
* Scrolls minimally: an axis already fully visible returns null for that axis;
|
|
* otherwise the nearest edge is brought just inside the window (plus padding).
|
|
* A clip larger than the window aligns its start edge. Pure — unit-tested.
|
|
*/
|
|
|
|
export interface RevealScrollInput {
|
|
scrollLeft: number;
|
|
scrollTop: number;
|
|
/** Scroll container clientWidth / clientHeight. */
|
|
viewportWidth: number;
|
|
viewportHeight: number;
|
|
/** Clip bounds in content-space (relative to the scroll content origin). */
|
|
clipLeft: number;
|
|
clipRight: number;
|
|
clipTop: number;
|
|
clipBottom: number;
|
|
/** Width of the sticky left gutter occluding the viewport's left edge. */
|
|
stickyLeft: number;
|
|
/** Height of the sticky ruler occluding the viewport's top edge. */
|
|
stickyTop: number;
|
|
/** False in "fit" zoom mode, where horizontal scrolling is disabled. */
|
|
allowHorizontal: boolean;
|
|
}
|
|
|
|
export interface RevealScrollTarget {
|
|
/** Target scrollLeft, or null when the horizontal axis needs no scroll. */
|
|
left: number | null;
|
|
/** Target scrollTop, or null when the vertical axis needs no scroll. */
|
|
top: number | null;
|
|
}
|
|
|
|
/** Breathing room between the revealed clip edge and the window edge. */
|
|
export const REVEAL_SCROLL_PADDING_PX = 12;
|
|
|
|
/**
|
|
* Minimal scroll on one axis to bring [start, end] inside the visible window
|
|
* [scroll + stickyStart, scroll + viewport], with padding. Returns null when
|
|
* the range is already fully visible.
|
|
*/
|
|
function revealAxis(
|
|
scroll: number,
|
|
viewport: number,
|
|
stickyStart: number,
|
|
start: number,
|
|
end: number,
|
|
): number | null {
|
|
const windowStart = scroll + stickyStart + REVEAL_SCROLL_PADDING_PX;
|
|
const windowEnd = scroll + viewport - REVEAL_SCROLL_PADDING_PX;
|
|
const windowSize = windowEnd - windowStart;
|
|
// Degenerate viewport (container smaller than the sticky chrome + padding):
|
|
// there is no visible window to reveal into, so never scroll on this axis.
|
|
if (windowSize <= 0) return null;
|
|
if (start >= windowStart && end <= windowEnd) return null;
|
|
// Oversized range (or start hidden): align the start edge to the window start.
|
|
if (end - start > windowSize || start < windowStart) {
|
|
return Math.max(0, start - stickyStart - REVEAL_SCROLL_PADDING_PX);
|
|
}
|
|
// Only the end is clipped: pull it just inside the window's far edge.
|
|
return Math.max(0, end - viewport + REVEAL_SCROLL_PADDING_PX);
|
|
}
|
|
|
|
export function computeRevealScroll(input: RevealScrollInput): RevealScrollTarget {
|
|
return {
|
|
left: input.allowHorizontal
|
|
? revealAxis(
|
|
input.scrollLeft,
|
|
input.viewportWidth,
|
|
input.stickyLeft,
|
|
input.clipLeft,
|
|
input.clipRight,
|
|
)
|
|
: null,
|
|
top: revealAxis(
|
|
input.scrollTop,
|
|
input.viewportHeight,
|
|
input.stickyTop,
|
|
input.clipTop,
|
|
input.clipBottom,
|
|
),
|
|
};
|
|
}
|