0.13.0: unify all track kinds — same height, same drag, same grouping

- Scripts bumped from 26px to 30px so the five track kinds read as one
  visual list; clip / script / audio tracks lose their kind-specific
  gradient backgrounds now that they're no longer fixed-position
  chrome.
- Audio tracks pick up the standard drag attributes + handlers; they
  now participate in the same trackOrder flow as layer / clip / script
  tracks and can be dropped into groups.
- normalizeScenePage accepts scripts + audio as group children (used
  to silently strip them); the per-kind ordered appending is replaced
  with a single tail loop so no kind is hardcoded to the bottom.
- moveTrackIntoGroup color cascade now covers all four groupable kinds
  (scripts + audio inherit group colour like layers + clip tracks).
- Group expansion in Timeline emits scripts + audio as first-class
  sub-track children, with the shared in-group / sub-track classes
  and group-aware drag handlers.
- Tests: move-into-group + color cascade for scripts + audio, legacy
  childIds roundtrip preserves both kinds, audio track is draggable,
  script track is 30px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-24 08:00:02 +02:00
1 parent 354dd8a4f0
commit 86ec7aa835
5 files changed
+227 -63

No files matched your search

+32 -18
View File
@@ -314,11 +314,16 @@ function normalizeScenePage(page: ScenePage): ScenePage {
for (const st of page.scriptTracks) known.add(st.id)
for (const at of page.audioTracks) known.add(at.id)
const layerAndClipIds = new Set<string>()
for (const l of page.layers) layerAndClipIds.add(l.id)
for (const t of page.clipTracks) layerAndClipIds.add(t.id)
// Any non-group track may live inside a group — layers, clip tracks,
// script tracks and audio tracks are all first-class citizens. Groups
// themselves stay at the top level (no nested groups).
const groupableIds = new Set<string>()
for (const l of page.layers) groupableIds.add(l.id)
for (const t of page.clipTracks) groupableIds.add(t.id)
for (const st of page.scriptTracks) groupableIds.add(st.id)
for (const at of page.audioTracks) groupableIds.add(at.id)
for (const g of page.groups) {
g.childIds = g.childIds.filter((id) => layerAndClipIds.has(id))
g.childIds = g.childIds.filter((id) => groupableIds.has(id))
}
const inGroup = new Set<string>()
for (const g of page.groups) {
@@ -330,25 +335,29 @@ function normalizeScenePage(page: ScenePage): ScenePage {
}
page.trackOrder = page.trackOrder.filter((id) => known.has(id))
// Fallback for legacy / corrupted scenes where some ids are known but
// absent from trackOrder. Layers prepend (historically stage content
// sits on top); everything else appends in DOM order with no
// per-kind segregation — users can reorder freely afterwards.
const inOrder = new Set(page.trackOrder)
for (const l of page.layers) {
if (!inGroup.has(l.id) && !page.trackOrder.includes(l.id)) {
if (!inGroup.has(l.id) && !inOrder.has(l.id)) {
page.trackOrder.unshift(l.id)
inOrder.add(l.id)
}
}
for (const t of page.clipTracks) {
if (!inGroup.has(t.id) && !page.trackOrder.includes(t.id)) {
page.trackOrder.push(t.id)
const tail: Array<{ id: string }> = [
...page.clipTracks,
...page.scriptTracks,
...page.audioTracks,
...page.groups,
]
for (const item of tail) {
if (!inGroup.has(item.id) && !inOrder.has(item.id)) {
page.trackOrder.push(item.id)
inOrder.add(item.id)
}
}
for (const g of page.groups) {
if (!page.trackOrder.includes(g.id)) page.trackOrder.push(g.id)
}
for (const st of page.scriptTracks) {
if (!page.trackOrder.includes(st.id)) page.trackOrder.push(st.id)
}
for (const at of page.audioTracks) {
if (!page.trackOrder.includes(at.id)) page.trackOrder.push(at.id)
}
// Orphan audio clips (pointing at a missing track or missing asset) would
// never render anyway — drop them so serialised projects stay clean.
const audioTrackIds = new Set(page.audioTracks.map((t) => t.id))
@@ -2746,12 +2755,17 @@ export const useSceneStore = defineStore('scene', {
// Inherit the target group's color when the moved track hasn't
// been explicitly recoloured by the user. Matches the cascade
// behaviour of setTrackColor so a track joining a coloured group
// visually lines up with its siblings.
// visually lines up with its siblings — applies to all four
// groupable kinds, not just layers + clip tracks.
if (target.color != null) {
const l = this.scene.layers.find((x) => x.id === trackId)
if (l && !l.colorOverridden) l.color = target.color
const ct = this.scene.clipTracks?.find((x) => x.id === trackId)
if (ct && !ct.colorOverridden) ct.color = target.color
const st = this.scene.scriptTracks?.find((x) => x.id === trackId)
if (st && !st.colorOverridden) st.color = target.color
const at = this.scene.audioTracks?.find((x) => x.id === trackId)
if (at && !at.colorOverridden) at.color = target.color
}
},
moveTrackOutOfGroup(trackId: string, toTopLevelIndex?: number) {