feat: animate queue detail expansion and collapse
Windows CI / verify (push) Canceled after 0s

This commit is contained in:
Sucukdeluxe
2026-09-06 12:44:12 +02:00
parent 7347f2fe43
commit 98c3c01fd3
7 changed files with 69 additions and 10 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Animate queue details expanding and collapsing with a matching chevron rotation; keep collapsed file actions out of keyboard navigation.
- Keep the waiting queue status unboxed with a transparent background and its yellow status dot. - Keep the waiting queue status unboxed with a transparent background and its yellow status dot.
- Rework queue cards with two-line titles, clearer status badges, larger dates below the progress bar on the right, and 32-pixel remove and retry buttons. Keep paused progress visible and wrap long download errors. - Rework queue cards with two-line titles, clearer status badges, larger dates below the progress bar on the right, and 32-pixel remove and retry buttons. Keep paused progress visible and wrap long download errors.
- Expand or collapse queue details by double-clicking the card surface or using the dedicated keyboard-accessible toggle. Add isolated queue-card checks for both languages and themes. - Expand or collapse queue details by double-clicking the card surface or using the dedicated keyboard-accessible toggle. Add isolated queue-card checks for both languages and themes.
+1
View File
@@ -18,6 +18,7 @@ Am 6. September 2026 nach einem Festplatten-Reset aus den vorhandenen Remote-Rep
## Letzte Änderungen ## Letzte Änderungen
- Queue-Details fahren seit dem 6. September 2026 beim Auf-/Zuklappen animiert ein und aus: Grid-Höhe und Deckkraft über 220/180 ms, Pfeilrotation über 220 ms. Bei reduzierten Windows-Animationen bleibt eine kurze 160-ms-Transition wie bei den bestehenden Workspace-Steuerelementen erhalten. Eingeklappte Details sind `inert`, damit unsichtbare Dateiaktionen nicht per Tastatur erreichbar sind. Build, 19 gezielte Tests und isolierter Queue-UI-Test einschließlich gemessener Zwischenhöhen in beiden Bewegungsmodi erfolgreich.
- Queue-Card-Nachbesserung am 6. September 2026: Wartestatus ohne sichtbare Umrandung und ohne gelblichen Hintergrund; gelber Statuspunkt und bisherige Abstände bleiben erhalten. In Hell/Dunkel geprüft, Queue-UI- und Stylesheet-Tests erfolgreich. - Queue-Card-Nachbesserung am 6. September 2026: Wartestatus ohne sichtbare Umrandung und ohne gelblichen Hintergrund; gelber Statuspunkt und bisherige Abstände bleiben erhalten. In Hell/Dunkel geprüft, Queue-UI- und Stylesheet-Tests erfolgreich.
- 6. September 2026: Queue-Cards überarbeitet. Titel mit bis zu zwei Zeilen in 13 px; Datum von 10 auf 12 px vergrößert und rechts unter den Fortschrittsbalken gesetzt. Status als beschriftetes Badge, Entfernen/Wiederholen mit 32 × 32 px Klickfläche, Details und Fortschritt in 12 px, keine Kartenschrift unter 10 px. Wartende Einträge ohne doppelte Statuszeile, pausierte Downloads mit erhaltenem Prozentstand, längere Fehler umbrechend. - 6. September 2026: Queue-Cards überarbeitet. Titel mit bis zu zwei Zeilen in 13 px; Datum von 10 auf 12 px vergrößert und rechts unter den Fortschrittsbalken gesetzt. Status als beschriftetes Badge, Entfernen/Wiederholen mit 32 × 32 px Klickfläche, Details und Fortschritt in 12 px, keine Kartenschrift unter 10 px. Wartende Einträge ohne doppelte Statuszeile, pausierte Downloads mit erhaltenem Prozentstand, längere Fehler umbrechend.
- Details lassen sich per Doppelklick auf die freie Kartenfläche einschließlich Titel, Datum und Balken umschalten. Ein eigener Pfeil unterstützt Einzelklick, Enter und Leertaste; Schaltflächen lösen keine zusätzliche Kartenaktion aus. Aufklappen aktualisiert die vorhandenen Elemente und erhält den Tastaturfokus. - Details lassen sich per Doppelklick auf die freie Kartenfläche einschließlich Titel, Datum und Balken umschalten. Ein eigener Pfeil unterstützt Einzelklick, Enter und Leertaste; Schaltflächen lösen keine zusätzliche Kartenaktion aus. Aufklappen aktualisiert die vorhandenen Elemente und erhält den Tastaturfokus.
+30
View File
@@ -64,6 +64,36 @@ async function main() {
} }
const card = win.locator('.queue-item[data-id="pending"]'); const card = win.locator('.queue-item[data-id="pending"]');
const toggle = card.locator('[data-queue-action="details"]'); const toggle = card.locator('[data-queue-action="details"]');
for (const reducedMotion of ['no-preference', 'reduce']) {
await win.emulateMedia({ reducedMotion });
const motion = await card.evaluate(async (item) => {
const details = item.querySelector('.queue-details');
const frame = () => new Promise((resolve) => requestAnimationFrame(resolve));
const sample = async () => {
await frame();
await frame();
const animations = details.getAnimations();
const slide = animations.find((animation) => animation.transitionProperty === 'grid-template-rows');
if (!slide) throw new Error('Queue details have no height transition');
for (const animation of animations) animation.currentTime = Number(animation.effect.getTiming().duration) / 2;
const middle = details.getBoundingClientRect().height;
for (const animation of animations) animation.finish();
await frame();
return { middle, end: details.getBoundingClientRect().height };
};
const collapsed = details.getBoundingClientRect().height;
toggleQueueDetails(item.dataset.id);
const opening = await sample();
toggleQueueDetails(item.dataset.id);
const closing = await sample();
return { collapsed, opening, closing, inert: details.inert };
});
assert.equal(motion.collapsed, 0);
assert(motion.opening.middle > 0 && motion.opening.middle < motion.opening.end, 'Details slide open');
assert(motion.closing.middle > 0 && motion.closing.middle < motion.opening.end, 'Details slide closed');
assert.equal(motion.closing.end, 0);
assert(motion.inert, 'Collapsed details cannot receive keyboard input');
}
for (const selector of ['.title', '.queue-date', '.queue-progress-wrap', '.queue-status-label']) { for (const selector of ['.title', '.queue-date', '.queue-progress-wrap', '.queue-status-label']) {
await card.locator(selector).dblclick(); await card.locator(selector).dblclick();
assert.equal(await toggle.getAttribute('aria-expanded'), 'true', `${selector} expands`); assert.equal(await toggle.getAttribute('aria-expanded'), 'true', `${selector} expands`);
+8 -2
View File
@@ -575,7 +575,11 @@ function toggleQueueDetails(id: string): void {
} }
const item = Array.from(byId<HTMLElement>('queueList').querySelectorAll<HTMLElement>('.queue-item')) const item = Array.from(byId<HTMLElement>('queueList').querySelectorAll<HTMLElement>('.queue-item'))
.find((candidate) => candidate.dataset.id === id); .find((candidate) => candidate.dataset.id === id);
item?.querySelector('.queue-details')?.classList.toggle('expanded', expandedQueueIds.has(id)); const details = item?.querySelector<HTMLElement>('.queue-details');
if (details) {
details.classList.toggle('expanded', expandedQueueIds.has(id));
details.inert = !expandedQueueIds.has(id);
}
item?.querySelector('[data-queue-action="details"]')?.setAttribute('aria-expanded', String(expandedQueueIds.has(id))); item?.querySelector('[data-queue-action="details"]')?.setAttribute('aria-expanded', String(expandedQueueIds.has(id)));
} }
@@ -723,12 +727,14 @@ function renderQueue(): void {
<span class="queue-progress-status${progressStatusClass}${item.status === 'paused' ? ' is-hidden' : ''}">${safeProgressStatus}</span> <span class="queue-progress-status${progressStatusClass}${item.status === 'paused' ? ' is-hidden' : ''}">${safeProgressStatus}</span>
<span class="queue-progress-metrics">${safeProgressMetrics}</span> <span class="queue-progress-metrics">${safeProgressMetrics}</span>
</div> </div>
<div class="queue-details${expandedQueueIds.has(item.id) ? ' expanded' : ''}" id="${detailsId}"> <div class="queue-details${expandedQueueIds.has(item.id) ? ' expanded' : ''}" id="${detailsId}"${expandedQueueIds.has(item.id) ? '' : ' inert'}>
<div class="queue-details-clip"><div class="queue-details-content">
<div><span class="queue-detail-label">URL:</span> ${escapeHtml(item.url)}</div> <div><span class="queue-detail-label">URL:</span> ${escapeHtml(item.url)}</div>
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailStreamer)}</span> ${escapeHtml(item.streamer)}</div> <div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailStreamer)}</span> ${escapeHtml(item.streamer)}</div>
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDuration)}</span> ${escapeHtml(item.duration_str)}</div> <div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDuration)}</span> ${escapeHtml(item.duration_str)}</div>
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDate)}</span> ${escapeHtml(formatUiDateTime(item.date))}</div> <div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDate)}</span> ${escapeHtml(formatUiDateTime(item.date))}</div>
${renderQueueItemFileActions(item)} ${renderQueueItemFileActions(item)}
</div></div>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -20,7 +20,7 @@ describe('production style modules', () => {
.replace(/\r\n/g, '\n')); .replace(/\r\n/g, '\n'));
const digest = createHash('sha256').update(content).digest('hex'); const digest = createHash('sha256').update(content).digest('hex');
expect(digest).toBe('2121672b9cf4534a7e15be2cf88ab1919fe54fe8f3ca314e6658ed6633436019'); expect(digest).toBe('b937d229d00f4a35098f288399331025580a1448f7c7aab7908f12ec826628e9');
}); });
test('derives the Windows hot-development executable version from package metadata', () => { test('derives the Windows hot-development executable version from package metadata', () => {
+23 -7
View File
@@ -341,6 +341,10 @@
background: color-mix(in srgb, var(--text-secondary) 12%, transparent); background: color-mix(in srgb, var(--text-secondary) 12%, transparent);
} }
.queue-details-toggle svg {
transition: transform 220ms ease;
}
.queue-details-toggle[aria-expanded="true"] svg { .queue-details-toggle[aria-expanded="true"] svg {
transform: rotate(180deg); transform: rotate(180deg);
} }
@@ -363,21 +367,33 @@
} }
.queue-details { .queue-details {
display: none; display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition: grid-template-rows 220ms ease, opacity 180ms ease;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: var(--text-secondary); color: var(--text-secondary);
margin-top: 8px;
padding: 8px 0 2px;
border-top: 1px solid var(--border-soft);
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.queue-details.expanded { .queue-details-clip {
display: block; min-height: 0;
overflow: hidden;
} }
.queue-details div { .queue-details-content {
margin-top: 8px;
padding: 8px 0 2px;
border-top: 1px solid var(--border-soft);
}
.queue-details.expanded {
grid-template-rows: 1fr;
opacity: 1;
}
.queue-details-content > div {
margin-bottom: 2px; margin-bottom: 2px;
} }
+5
View File
@@ -2662,6 +2662,11 @@ input[type="checkbox"].vod-select-checkbox:focus-visible {
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.queue-details,
.queue-details-toggle svg {
transition-duration: 160ms !important;
}
.vod-bulk-bar { .vod-bulk-bar {
transition: transform 280ms cubic-bezier(0.22, 0.76, 0.22, 1), opacity 220ms ease, visibility 0s linear 280ms !important; transition: transform 280ms cubic-bezier(0.22, 0.76, 0.22, 1), opacity 220ms ease, visibility 0s linear 280ms !important;
} }