mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): group rows in the timeline, and a split disclosure
A group renders as its own row with member rows beneath it, and disclosure
splits into two independent controls: caret shows/hides a group's member
rows (structural), `∿` shows/hides any row's automation-lane rows. Plain
tracks lose their caret (nothing to disclose structurally) and keep only
`∿`. `expandedClipIds` keeps its existing keyframe-lane-state job;
`expandedGroupIds`/`expandedLaneOwnerIds` are new, independent sets.
Groups get a real position in the row/geometry pipeline rather than a
visual-only overlay: `useTimelineTrackDerivations` re-emits a group's member
tracks contiguously under a synthetic fractional anchor key
(firstMember - 0.5, the same fractional-key convention sub-composition
expansion already uses), so `rowGeometry`/keyboard-nav/virtualization treat
a group row as a first-class row without widening their key type away from
number. `TimelineLogicalRow.level` widens `1 | 2` to `1 | 2 | 3` (group /
member-under-group / lane), lanes always `owner.level + 1`.
All of it — grouped row emission, the header, the new expansion state — is
gated behind `isCanaryEnabled("audio-groups")`; disabled, `groups` resolves
empty and every new code path no-ops. `TimelineElement.audioGroup` (+
`audioGroupLabel`, resolved once per document via `resolveAudioGroups` from
B1) is parsed unconditionally, mirroring how `hidden`/`fxChain` already
flow DOM → manifest → TimelineElement — inert without the canary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e42185582b
commit
dd3212d11b
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
||||
import type { ClipManifestClip } from "./playbackTypes";
|
||||
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
||||
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
||||
import { resolveAudioGroups } from "@hyperframes/core/audio-groups";
|
||||
import {
|
||||
resolveMediaElement,
|
||||
applyMediaMetadataFromElement,
|
||||
@@ -67,6 +68,20 @@ function resolveClipTag(clip: ClipManifestClip): string {
|
||||
return clip.tagName || clip.kind || "div";
|
||||
}
|
||||
|
||||
// One `<hf-audio-group>` scan per document, not per clip — resolveAudioGroups
|
||||
// walks the whole tree, and a parse touches every clip in it.
|
||||
const groupLabelCache = new WeakMap<Document, Map<string, string>>();
|
||||
|
||||
function groupLabelFor(doc: Document | null | undefined, groupId: string): string {
|
||||
if (!doc) return groupId;
|
||||
let labels = groupLabelCache.get(doc);
|
||||
if (!labels) {
|
||||
labels = new Map(resolveAudioGroups(doc).map((group) => [group.id, group.label]));
|
||||
groupLabelCache.set(doc, labels);
|
||||
}
|
||||
return labels.get(groupId) ?? groupId;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function createTimelineElementFromManifestClip(params: {
|
||||
clip: ClipManifestClip;
|
||||
@@ -138,6 +153,11 @@ export function createTimelineElementFromManifestClip(params: {
|
||||
if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
|
||||
const timelineRole = hostEl.getAttribute("data-timeline-role");
|
||||
if (timelineRole) entry.timelineRole = timelineRole;
|
||||
const audioGroup = hostEl.getAttribute("data-audio-group");
|
||||
if (audioGroup) {
|
||||
entry.audioGroup = audioGroup;
|
||||
entry.audioGroupLabel = groupLabelFor(doc ?? hostEl.ownerDocument, audioGroup);
|
||||
}
|
||||
const fxChain = hostEl.getAttribute("data-fx-chain");
|
||||
if (fxChain) entry.fxChain = fxChain;
|
||||
const automation = hostEl.getAttribute("data-automation");
|
||||
@@ -356,6 +376,12 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
||||
const timelineRole = el.getAttribute("data-timeline-role");
|
||||
if (timelineRole) entry.timelineRole = timelineRole;
|
||||
|
||||
const domAudioGroup = el.getAttribute("data-audio-group");
|
||||
if (domAudioGroup) {
|
||||
entry.audioGroup = domAudioGroup;
|
||||
entry.audioGroupLabel = groupLabelFor(doc, domAudioGroup);
|
||||
}
|
||||
|
||||
// Sub-compositions
|
||||
const compSrc =
|
||||
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
|
||||
|
||||
Reference in New Issue
Block a user