feat(stats): add library entry deletion and app-wide delete progress (#174)

This commit is contained in:
2026-07-29 02:00:16 -07:00
committed by GitHub
parent 0d7084c8aa
commit 6d2a72e13b
40 changed files with 2013 additions and 125 deletions
+36 -2
View File
@@ -80,7 +80,7 @@ body.overlay-mode #root {
}
/* Tab content entrance animation */
@keyframes fadeSlideIn {
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(6px);
@@ -92,5 +92,39 @@ body.overlay-mode #root {
}
.animate-fade-in {
animation: fadeSlideIn 0.25s ease-out;
animation: fade-slide-in 0.25s ease-out;
}
/* Indeterminate progress sweep for the global delete indicator */
@keyframes indeterminate-sweep {
from {
transform: translateX(-100%);
}
to {
transform: translateX(400%);
}
}
.animate-indeterminate {
animation: indeterminate-sweep 1.1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-in {
animation-duration: 1ms;
}
/* Looping motion can't just be shortened the way a one-shot entrance can —
that speeds it up rather than removing it. The delete indicator holds a
static busy state instead: the bar fills its track and the spinner stops,
leaving the toast text to carry the meaning. */
.animate-indeterminate {
animation: none;
width: 100%;
transform: none;
}
.animate-spin {
animation: none;
}
}
+22
View File
@@ -14,3 +14,25 @@ test('stats overlay mode paints an opaque full-viewport background', () => {
/body\.overlay-mode #root\s*\{[^}]*background-color:\s*var\(--color-ctp-base\);/s,
);
});
test('reduced motion stops the looping delete indicator rather than speeding it up', () => {
const reducedMotion = /@media \(prefers-reduced-motion: reduce\)\s*\{(?<body>.*)\n\}/s.exec(css)
?.groups?.body;
assert.ok(reducedMotion, 'expected a prefers-reduced-motion block');
// Shortening an infinite animation makes it run faster, so the looping rules
// have to switch it off outright and hold a static state instead.
assert.match(reducedMotion, /\.animate-indeterminate\s*\{[^}]*animation:\s*none;/s);
assert.match(reducedMotion, /\.animate-indeterminate\s*\{[^}]*width:\s*100%;/s);
assert.match(reducedMotion, /\.animate-spin\s*\{[^}]*animation:\s*none;/s);
assert.doesNotMatch(reducedMotion, /\.animate-indeterminate\s*\{[^}]*animation-duration:/s);
assert.doesNotMatch(reducedMotion, /\.animate-spin\s*\{[^}]*animation-duration:/s);
});
test('the looping delete indicator keeps its animation for everyone else', () => {
const beforeMediaQuery = css.slice(0, css.indexOf('@media (prefers-reduced-motion'));
assert.match(
beforeMediaQuery,
/\.animate-indeterminate\s*\{\s*animation:\s*indeterminate-sweep/s,
);
});