Merge pull request #18 from o3LL/fix/duplicate-keyframes

fix(css): collapse duplicate @keyframes names to the definition that wins
This commit is contained in:
Alexandre Teixeira
2026-09-30 19:18:13 +01:00
committed by GitHub
3 changed files with 52 additions and 22 deletions
@@ -28,10 +28,6 @@
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.7; }
}
@keyframes research-pulse {
0%, 100% { background: color-mix(in srgb, var(--red) 12%, transparent); }
50% { background: color-mix(in srgb, var(--red) 22%, transparent); }
}
#cookbook-gguf-delete-overlay {
background:rgba(0,0,0,0.5);
backdrop-filter:blur(4px);
-18
View File
@@ -1480,7 +1480,6 @@ body.bg-pattern-ascii-fireflies {
0% { opacity: 1; transform: translateY(0) translateX(0) scale(1); }
100% { opacity: 0; transform: translateY(6px) translateX(-3px) scale(0.94); }
}
@keyframes spin { to { transform: rotate(360deg); } }
.row { display:flex; gap:6px; align-items:center; }
.list-item:hover,
.models-row:hover {
@@ -6119,9 +6118,6 @@ body.bg-pattern-ascii-fireflies {
#mobile-backdrop, #mobile-menu-btn { display:none !important; }
#sidebar-backdrop { display:none !important; }
/* ----- Loading spinner ----- */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
@@ -6240,14 +6236,6 @@ body.bg-pattern-ascii-fireflies {
.session-skeleton-bubble.is-user { width: 72%; }
.session-skeleton-line { height: 9px; margin-top: 7px; }
}
@keyframes loading-bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* Modal styling */
.modal {
position:fixed;
@@ -7555,11 +7543,6 @@ body.bg-pattern-ascii-fireflies {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
#recording-indicator {
position: fixed;
@@ -8720,7 +8703,6 @@ pre { background: var(--code-bg, var(--hl-bg, #282c34)) !important; }
}
.compare-probe-start-anyway svg { color: currentColor; }
.compare-probe-start-anyway span { font-weight: 700; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes pane-shake {
0%, 100% { transform: translateX(0); }
15% { transform: translateX(-3px) rotate(-0.5deg); }
@@ -0,0 +1,52 @@
"""No two @keyframes may share a name across the app's stylesheets.
Duplicate names resolve last-wins across the whole cascade, so an earlier
definition is dead code that still looks live at its call site. Two of them
existed and differed from the definition that actually won, which also blocked
moving either animation during the stylesheet decomposition: relocating one
changes which is last and therefore changes rendered behaviour.
"""
import collections
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STATIC = ROOT / "static"
def _stylesheets() -> list[Path]:
"""App-owned stylesheets, in no particular order. Vendored CSS is excluded."""
sheets = [STATIC / "style.css"]
sheets.extend(sorted((STATIC / "css").glob("*.css")))
return [s for s in sheets if s.exists()]
def _keyframe_names(text: str) -> list[str]:
without_comments = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
return re.findall(
r"@(?:-webkit-)?keyframes\s+([A-Za-z0-9_-]+)\s*\{", without_comments
)
def test_app_stylesheets_exist() -> None:
"""Guard the guard: a rename must not turn this file into a no-op."""
assert _stylesheets(), "no app stylesheets found to check"
def test_no_keyframes_name_is_declared_twice() -> None:
counts: collections.Counter[str] = collections.Counter()
where: dict[str, list[str]] = collections.defaultdict(list)
for sheet in _stylesheets():
for name in _keyframe_names(sheet.read_text(encoding="utf-8")):
counts[name] += 1
where[name].append(sheet.relative_to(ROOT).as_posix())
duplicates = {name: where[name] for name, n in counts.items() if n > 1}
assert duplicates == {}, (
"duplicate @keyframes names resolve last-wins, so every definition but "
f"the last is dead: {duplicates}"
)