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.
This commit is contained in:
Ular Kimsanov
2026-07-11 20:20:29 -04:00
committed by GitHub
parent ae4946e624
commit 920dad35eb
6 changed files with 336 additions and 1 deletions
@@ -16,6 +16,18 @@ export interface StudioUiPreferences {
gridVisible?: boolean;
gridSpacing?: number;
snapToGrid?: boolean;
/** Timeline magnet: snap clip drags/trims/drops to playhead, clip edges, and beats. */
timelineSnapEnabled?: boolean;
/** Transport + ruler readout mode: timecode or frame number. */
timeDisplayMode?: "time" | "frame";
/**
* Timeline zoom mode. Persisted so a zoom PINNED on the first edit survives the
* post-edit iframe reload — otherwise the store reset to "fit" and the duration
* change rescaled every clip (the blink-fix's rescale symptom).
*/
timelineZoomMode?: "fit" | "manual";
/** Manual timeline zoom percent, paired with `timelineZoomMode: "manual"`. */
timelineManualZoomPercent?: number;
}
const STUDIO_UI_PREFERENCES_KEY = "hf-studio-ui-preferences";
@@ -88,6 +100,21 @@ function readStorage(storage: Storage | null): StudioUiPreferences {
if (typeof parsed.snapToGrid === "boolean") {
preferences.snapToGrid = parsed.snapToGrid;
}
if (typeof parsed.timelineSnapEnabled === "boolean") {
preferences.timelineSnapEnabled = parsed.timelineSnapEnabled;
}
if (parsed.timeDisplayMode === "time" || parsed.timeDisplayMode === "frame") {
preferences.timeDisplayMode = parsed.timeDisplayMode;
}
if (parsed.timelineZoomMode === "fit" || parsed.timelineZoomMode === "manual") {
preferences.timelineZoomMode = parsed.timelineZoomMode;
}
if (
typeof parsed.timelineManualZoomPercent === "number" &&
Number.isFinite(parsed.timelineManualZoomPercent)
) {
preferences.timelineManualZoomPercent = parsed.timelineManualZoomPercent;
}
return preferences;
} catch {
return {};