From 95f15ac834f374ae2ec2e7e9cb3fb5e2c99bf8ea Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:31:48 +0200 Subject: [PATCH] fix: keep queue titles stable when details expand --- CHANGELOG.md | 1 + PROJECT_MEMORY.md | 1 + scripts/smoke-test-queue-cards.js | 48 ++++++++++++++++++++++- src/style-modules.production-path.test.ts | 2 +- src/styles-workflows.css | 1 + 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ed9485..98eb817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Keep queue-card titles and content aligned when expanding or collapsing details by reserving scrollbar space. - Move the queue-card remove button slightly toward the top-right corner while retaining its 32-pixel click target. - Show a yellow left border on waiting queue cards, matching their status dot. - Place the queue-card date on the left beside the details arrow and the download status on the right below the progress bar. diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 79181b9..a573cc7 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -20,6 +20,7 @@ Aktueller veröffentlichter Stand: **1.0.20**, Tag `v1.0.20`, veröffentlicht am ## Letzte Änderungen +- Titelsprung beim Auf-/Zuklappen der Queue-Cards behoben: `scrollbar-gutter: stable` reserviert den 8-px-Platz der Scrollleiste, damit sich Kartenbreite und Titelumbruch nicht ändern. In Tab- und Split-Ansicht nachgemessen. Neuer Queue-UI-Regressionscheck überschreitet beim Öffnen gezielt die Scrollgrenze und prüft die Titelgeometrie während beider Animationen in Hell/Dunkel und Deutsch/Englisch; Hintergrund-Synchronisierung wird nach dem Fixture-Abgleich für diese deterministische Messung angehalten. Vor dem Fix fehlgeschlagen, danach erfolgreich. Build, 19 gezielte Tests und Lint erfolgreich (15 bestehende Warnungen). Noch nicht veröffentlicht. - Löschen-Button in Queue-Cards um jeweils 3 px nach oben und rechts verschoben; Klickfläche bleibt 32 × 32 px. Build, fünf Stylesheet-Tests und isolierter Queue-UI-Test erfolgreich; Hell/Dunkel visuell geprüft. Noch nicht veröffentlicht. - Wartende Queue-Cards haben jetzt eine gelbe linke Statuslinie passend zum Wartepunkt. Der Wartet-Text bleibt ohne Umrandung oder gelblichen Hintergrund. Build, fünf Stylesheet-Tests und isolierter Queue-UI-Test erfolgreich; Linie in Hell/Dunkel visuell geprüft. Noch nicht veröffentlicht. - Nach Release 1.0.20: Datum und Status in der Queue-Card getauscht. Datum steht jetzt links neben dem Detailpfeil, Status rechts unter dem Fortschrittsbalken. Schriftgrößen, gelber Wartepunkt und Aufklappanimation bleiben erhalten. Build, 19 gezielte Tests, Queue-UI-Test in Deutsch/Englisch und Hell/Dunkel sowie vollständiger Workspace-UI-Test erfolgreich; Lint ohne Fehler bei 15 bestehenden Warnungen. Änderung zunächst nur auf `public-v1`/Hot-Dev, noch nicht veröffentlicht. diff --git a/scripts/smoke-test-queue-cards.js b/scripts/smoke-test-queue-cards.js index ea41496..49acdda 100644 --- a/scripts/smoke-test-queue-cards.js +++ b/scripts/smoke-test-queue-cards.js @@ -38,7 +38,11 @@ async function main() { ipcMain.removeHandler('get-queue'); ipcMain.handle('get-queue', async () => items); }, fixtures); - await win.evaluate(() => syncQueueAndDownloadState()); + await win.evaluate(async () => { + await syncQueueAndDownloadState(); + clearTimeout(queueSyncTimer); + queueSyncTimer = null; + }); assert.equal(await win.locator('#queueList .queue-item').count(), fixtures.length, 'Queue fixtures survive background synchronization'); for (const theme of ['twitch', 'light']) { for (const language of ['de', 'en']) { @@ -71,6 +75,48 @@ async function main() { assert(card.overflow <= 1, `Card overflow: ${JSON.stringify(card)}`); } await win.locator('.queue-section').screenshot({ path: path.join(artifacts, `queue-${theme}-${language}.png`) }); + const expansionLayout = await win.locator('#queueList').evaluate(async (list) => { + const items = [...list.querySelectorAll('.queue-item')]; + const originalStyles = [list, ...items].map((element) => element.getAttribute('style')); + const item = items[0]; + const details = item.querySelector('.queue-details'); + const frame = () => new Promise((resolve) => requestAnimationFrame(resolve)); + const measure = () => { + const title = item.querySelector('.title').getBoundingClientRect(); + return { left: title.left, width: title.width, height: title.height, overflowing: list.scrollHeight > list.clientHeight }; + }; + try { + for (const other of items.slice(2)) other.style.display = 'none'; + list.style.flex = '0 0 auto'; + list.style.height = `${items.slice(0, 2).reduce((height, card) => height + card.getBoundingClientRect().height + parseFloat(getComputedStyle(card).marginBottom), 0) + 20}px`; + const before = measure(); + const samples = []; + const animate = async () => { + toggleQueueDetails(item.dataset.id); + await frame(); + await frame(); + do { + samples.push(measure()); + await frame(); + } while (details.getAnimations().length); + return measure(); + }; + const expanded = await animate(); + const collapsed = await animate(); + return { before, expanded, collapsed, samples }; + } finally { + [list, ...items].forEach((element, index) => { + if (originalStyles[index] === null) element.removeAttribute('style'); + else element.setAttribute('style', originalStyles[index]); + }); + } + }); + assert(!expansionLayout.before.overflowing && expansionLayout.expanded.overflowing && !expansionLayout.collapsed.overflowing, `Expansion fixture crosses the scrollbar threshold (${theme}/${language}): ${JSON.stringify({ before: expansionLayout.before, expanded: expansionLayout.expanded, collapsed: expansionLayout.collapsed })}`); + for (const sample of [...expansionLayout.samples, expansionLayout.expanded, expansionLayout.collapsed]) { + for (const dimension of ['left', 'width', 'height']) { + assert(Math.abs(sample[dimension] - expansionLayout.before[dimension]) <= 0.5, `Title ${dimension} shifts during expansion (${theme}/${language})`); + } + } } } const card = win.locator('.queue-item[data-id="pending"]'); diff --git a/src/style-modules.production-path.test.ts b/src/style-modules.production-path.test.ts index 927b45f..6ebacbb 100644 --- a/src/style-modules.production-path.test.ts +++ b/src/style-modules.production-path.test.ts @@ -20,7 +20,7 @@ describe('production style modules', () => { .replace(/\r\n/g, '\n')); const digest = createHash('sha256').update(content).digest('hex'); - expect(digest).toBe('db798e79ce538bd99fe6a21fa9de545150a2d0efd4a925e6f662a664bdb6ff03'); + expect(digest).toBe('a2626a94632641b7e6039d5a50165e34224a75a1d286db90ae9e22bdae72064e'); }); test('derives the Windows hot-development executable version from package metadata', () => { diff --git a/src/styles-workflows.css b/src/styles-workflows.css index 3915fe8..bd59ef2 100644 --- a/src/styles-workflows.css +++ b/src/styles-workflows.css @@ -33,6 +33,7 @@ .queue-list { flex: 1; overflow-y: auto; + scrollbar-gutter: stable; min-height: 60px; }