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

+85
View File
@@ -2950,6 +2950,63 @@ export function registerAllTests(): void {
const inG2 = g2.childIds.filter((id) => id === l.id).length
expect(inG1 + inG2).toBe(1)
})
test('moveTrackIntoGroup accepts script tracks as children', () => {
const s = makeFreshStore()
const st = s.addScriptTrack('Logic')
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(st.id, g.id)
expect(g.childIds).toContain(st.id)
expect((s.scene.trackOrder ?? []).includes(st.id)).toBe(false)
})
test('moveTrackIntoGroup accepts audio tracks as children', () => {
const s = makeFreshStore()
const at = s.addAudioTrack('FX')
const g = s.addTrackGroup('G')
s.moveTrackIntoGroup(at.id, g.id)
expect(g.childIds).toContain(at.id)
expect((s.scene.trackOrder ?? []).includes(at.id)).toBe(false)
})
test('group color cascades to script + audio children alongside layers', () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'Mix', width: 10, height: 10, fill: '#000' })
const l = s.addLayer(a.id)!
const st = s.addScriptTrack('Script')
const at = s.addAudioTrack('Audio')
const g = s.addTrackGroup('G')
s.setTrackColor(g.id, '#ff8800')
s.moveTrackIntoGroup(l.id, g.id)
s.moveTrackIntoGroup(st.id, g.id)
s.moveTrackIntoGroup(at.id, g.id)
expect(l.color).toBe('#ff8800')
expect(st.color).toBe('#ff8800')
expect(at.color).toBe('#ff8800')
// None of them were explicitly recoloured — all inherit, none pin.
expect(l.colorOverridden).toBe(undefined)
expect(st.colorOverridden).toBe(undefined)
expect(at.colorOverridden).toBe(undefined)
})
test('normalizeScene preserves script + audio tracks as group children', () => {
// Legacy loads used to strip anything other than layers / clip
// tracks from childIds. All four non-group kinds must survive
// the roundtrip now.
const s = makeFreshStore()
const st = s.addScriptTrack('S')
const at = s.addAudioTrack('A')
const g = s.addTrackGroup('G')
const snap = JSON.parse(JSON.stringify(s.scene))
snap.groups = [{ id: g.id, name: 'G', childIds: [st.id, at.id] }]
snap.trackOrder = [g.id]
s.loadScene(snap)
const loaded = s.scene.groups?.find((x) => x.id === g.id)!
expect(loaded.childIds).toContain(st.id)
expect(loaded.childIds).toContain(at.id)
expect((s.scene.trackOrder ?? []).includes(st.id)).toBe(false)
expect((s.scene.trackOrder ?? []).includes(at.id)).toBe(false)
})
})
suite('scene store — duplicateTrack', () => {
@@ -5022,6 +5079,34 @@ export function registerAllTests(): void {
handle.unmount()
})
test('audio track name carries the draggable attribute so it joins drag-reorder', async () => {
// Audio tracks used to render with no @dragstart, which left them
// outside the stack-reorder flow. They now share the same hooks
// as layer / clip / script tracks.
const s = makeFreshStore()
s.addAudioTrack('Draggable')
const handle = mountComponent(Timeline, {})
await nextTick()
const name = handle.host.querySelector('.audio-track .track-name') as HTMLElement
expect(name).toBeTruthy()
expect(name.getAttribute('draggable')).toBe('true')
handle.unmount()
})
test('script track matches the default 30px track height', async () => {
// Regression: scripts used to be 26px, now unified with the other
// kinds so the timeline stack reads as a single visual list.
const s = makeFreshStore()
s.addScriptTrack('S')
const handle = mountComponent(Timeline, {})
await nextTick()
const st = handle.host.querySelector('.script-track') as HTMLElement
expect(st).toBeTruthy()
const h = getComputedStyle(st).height
expect(h).toBe('30px')
handle.unmount()
})
test('audio track co-exists with other track kinds in the tracks container', async () => {
const s = makeFreshStore()
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#111' })