diff --git a/package.json b/package.json index e35fe45..4d593c2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "0.13.0", + "version": "0.13.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index d007ca1..6226321 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -1592,7 +1592,10 @@ function toggleGroupCollapsed(groupId: string) { } function addGroup() { - store.addTrackGroup() + // Drop the group directly above whatever the user is currently + // working on — layer selection is the only active-track signal + // today. Falls back to the top of the stack when nothing's active. + store.addTrackGroup(undefined, store.selectedLayerId ?? null) } async function renameGroupPrompt(groupId: string, current: string) { diff --git a/src/devtools/tests.ts b/src/devtools/tests.ts index aa1df5c..4be670d 100644 --- a/src/devtools/tests.ts +++ b/src/devtools/tests.ts @@ -2666,6 +2666,41 @@ export function registerAllTests(): void { expect(s.scene.trackOrder![0]).toBe(g.id) }) + test('addTrackGroup with an anchor inserts directly above that track', () => { + const s = makeFreshStore() + const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' }) + const l1 = s.addLayer(a.id)! // trackOrder ends up [l2, l1] via unshift + const l2 = s.addLayer(a.id)! + // Anchor on the second layer → group lands at its index, shoving it down. + const g = s.addTrackGroup('G', l1.id) + const order = s.scene.trackOrder ?? [] + const gIdx = order.indexOf(g.id) + const l1Idx = order.indexOf(l1.id) + expect(gIdx).toBe(l1Idx - 1) + void l2 + }) + + test('addTrackGroup with an anchor inside an existing group lands above the parent group', () => { + const s = makeFreshStore() + const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' }) + const l = s.addLayer(a.id)! + const parent = s.addTrackGroup('Parent') + s.moveTrackIntoGroup(l.id, parent.id) + // Anchor is nested inside `parent` — new group must sit above `parent`, + // not attempt to nest itself inside another group. + const fresh = s.addTrackGroup('Fresh', l.id) + const order = s.scene.trackOrder ?? [] + expect(order.indexOf(fresh.id)).toBe(order.indexOf(parent.id) - 1) + }) + + test('addTrackGroup with an unknown anchor falls back to the top', () => { + const s = makeFreshStore() + const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' }) + s.addLayer(a.id)! + const g = s.addTrackGroup('G', 'ghost-id') + expect(s.scene.trackOrder![0]).toBe(g.id) + }) + test('moveTrackIntoGroup moves track from top-level to group childIds', () => { const s = makeFreshStore() const a = s.addAsset({ kind: 'rect', name: 'G2', width: 10, height: 10, fill: '#222' }) @@ -5027,6 +5062,51 @@ export function registerAllTests(): void { }) }) + suite('ui — Timeline new-group button', () => { + test('clicking the new-group button with a selected layer inserts the group above it', async () => { + const s = makeFreshStore() + const a = s.addAsset({ kind: 'rect', name: 'Anchor', width: 10, height: 10, fill: '#111' }) + const top = s.addLayer(a.id)! + const anchor = s.addLayer(a.id)! + s.selectLayer(anchor.id) + const handle = mountComponent(Timeline, {}) + await nextTick() + const btn = handle.host.querySelector( + `button[aria-label="${i18nT('timeline.addGroupTooltip')}"]`, + ) as HTMLButtonElement + expect(btn).toBeTruthy() + btn.click() + await nextTick() + const order = s.scene.trackOrder ?? [] + const groupId = (s.scene.groups ?? [])[0]?.id + expect(groupId).toBeTruthy() + const gIdx = order.indexOf(groupId!) + const anchorIdx = order.indexOf(anchor.id) + // Group sits right above the anchor layer, pushing it down by one slot. + expect(gIdx).toBe(anchorIdx - 1) + // Unrelated `top` layer keeps its own slot — not dragged along. + expect(order.indexOf(top.id) >= 0).toBe(true) + handle.unmount() + }) + + test('clicking the new-group button with no selection unshifts to the top (regression)', async () => { + const s = makeFreshStore() + const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' }) + s.addLayer(a.id) + s.selectLayer(null) + const handle = mountComponent(Timeline, {}) + await nextTick() + const btn = handle.host.querySelector( + `button[aria-label="${i18nT('timeline.addGroupTooltip')}"]`, + ) as HTMLButtonElement + btn.click() + await nextTick() + const groupId = (s.scene.groups ?? [])[0]?.id + expect(s.scene.trackOrder?.[0]).toBe(groupId) + handle.unmount() + }) + }) + suite('ui — Timeline audio track', () => { test('audio track renders as an .audio-track lane with one block per clip', async () => { const s = makeFreshStore() diff --git a/src/stores/scene.ts b/src/stores/scene.ts index 4325c05..2cd6fea 100644 --- a/src/stores/scene.ts +++ b/src/stores/scene.ts @@ -2660,14 +2660,39 @@ export const useSceneStore = defineStore('scene', { else inst.rotation = r } }, - addTrackGroup(name?: string): TrackGroup { + /** Create an empty group. When `anchorTrackId` is given the group + * lands directly above that track in trackOrder — resolving to + * the parent group's position if the anchor sits inside one. Falls + * back to the top of the stack when the anchor is missing or the + * scene has no trackOrder yet. */ + addTrackGroup(name?: string, anchorTrackId?: string | null): TrackGroup { const group: TrackGroup = { id: uid(), name: name?.trim() || 'Group', childIds: [], } ;(this.scene.groups ||= []).push(group) - ;(this.scene.trackOrder ||= []).unshift(group.id) + const order = (this.scene.trackOrder ||= []) + + let insertAt = 0 + if (anchorTrackId) { + const direct = order.indexOf(anchorTrackId) + if (direct >= 0) { + insertAt = direct + } else { + // Anchor is inside a group — put the new group above the + // group container instead of trying to wedge in between + // group children (nested groups are not supported). + const parent = this.scene.groups?.find((g) => + g.childIds.includes(anchorTrackId), + ) + if (parent) { + const pi = order.indexOf(parent.id) + if (pi >= 0) insertAt = pi + } + } + } + order.splice(insertAt, 0, group.id) return group }, /** Create a new group and move every currently-selected layer diff --git a/src/version.ts b/src/version.ts index 429044c..e80b2a7 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const APP_VERSION = '0.13.0' +export const APP_VERSION = '0.13.1' export const APP_NAME = 'RUWIO'