From 06cc07b2c22db07fb289194279076bdf61f8df59 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 22 Jul 2026 19:18:12 +0200 Subject: [PATCH] bucket_catch: introduce a real design-token system, fix reversed gradients Fixes every violation in DESIGN_AUDIT_TODO.md's bucket_catch frontend section. The app previously had no shared tokens at all (index.css was a 12-line reset) and 19 hardcoded hex colors duplicated independently across 6 *.module.css files. New :root token block in index.css: bg gradient stops, a drag-state gradient, the indigo/pink accent pair collapsed to one canonical --gradient-accent direction, text/surface colors, success/error, 2 consolidated muted-gray roles (down from 4 unrelated shades), a 4px spacing scale, and a radius scale. All 6 component files migrated onto it. Fixes the real reversed-gradient-stop bug the audit named (PuzzleResult.module.css .grade was pink->indigo vs every other heading's indigo->pink) plus a second instance of the same bug the audit's prose didn't cite by line number (ModeSelect.module.css .puzzleStart button) -- both now reference --gradient-accent so this class of bug can't recur. Also fixes: .preset's border contrast (was under 3:1), cardDesc's sub-16px font-size, 4 buttons' off-ratio padding (now exact 2x), 2 large icons' full-contrast rendering next to muted text (now dimmed), the explicitly-cited off-grid spacing values, and ModeSelect's gridInput background color which had drifted from its DropZone sibling's. Verified: 145/145 tests at 100% coverage (CSS-only, confirms no functional regression), a real vite build succeeds, re-verified identical in an isolated worktree at HEAD, and pixel-checked the production build via a headless-Chrome screenshot (gradient direction now matches between heading and button; preset chip borders clearly visible). `npm run lint` (tsc -b + eslint) was already broken on a clean checkout before this change (1 pre-existing tsc error in vite.config.ts, 46 pre-existing eslint errors in untouched test/lib files) -- confirmed via an isolated worktree at the parent commit, left alone as out of scope. Co-Authored-By: Claude Sonnet 5 --- .../src/components/DropZone.module.css | 84 +++++++++--------- .../src/components/GameCanvas.module.css | 2 +- .../src/components/ModeSelect.module.css | 85 +++++++++++-------- .../src/components/PuzzleCanvas.module.css | 2 +- .../src/components/PuzzleResult.module.css | 40 ++++----- .../src/components/ScoreScreen.module.css | 56 ++++++------ bucket_catch/packages/frontend/src/index.css | 77 ++++++++++++++++- 7 files changed, 219 insertions(+), 127 deletions(-) diff --git a/bucket_catch/packages/frontend/src/components/DropZone.module.css b/bucket_catch/packages/frontend/src/components/DropZone.module.css index e2d6679..33731f8 100644 --- a/bucket_catch/packages/frontend/src/components/DropZone.module.css +++ b/bucket_catch/packages/frontend/src/components/DropZone.module.css @@ -3,37 +3,39 @@ align-items: center; justify-content: center; min-height: 100dvh; - background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); + background: var(--bg-gradient); transition: background 0.2s; overflow-y: auto; - padding: 24px 16px; + padding: var(--space-4) var(--space-3); } .zone.dragging { - background: linear-gradient(135deg, #1a1560, #4a3f9e, #3a3868); - outline: 4px dashed #818cf8; + background: var(--bg-drag-gradient); + outline: 4px dashed var(--color-indigo); outline-offset: -12px; } .inner { text-align: center; - color: #e0e7ff; + color: var(--color-text); display: flex; flex-direction: column; align-items: center; - gap: 10px; + gap: var(--space-2); } .icon { font-size: 52px; line-height: 1; + /* Dimmed relative to the .subtitle text below it (rule 28). */ + opacity: 0.85; } .title { font-size: 2rem; font-weight: 800; margin: 0; - background: linear-gradient(90deg, #818cf8, #f472b6); + background: var(--gradient-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -41,24 +43,24 @@ .subtitle { font-size: 1rem; - color: #94a3b8; + color: var(--muted-text); margin: 0; } .count { font-size: 1rem; - color: #a5b4fc; + color: var(--color-indigo-light); font-weight: 600; - background: rgba(129, 140, 248, 0.15); - padding: 4px 16px; - border-radius: 999px; + background: rgba(var(--color-indigo-rgb), 0.15); + padding: var(--space-1) var(--space-3); + border-radius: var(--radius-pill); margin: 0; } .browseBtn { - padding: 8px 24px; - border-radius: 999px; - background: linear-gradient(90deg, #818cf8, #f472b6); + padding: 12px var(--space-4); + border-radius: var(--radius-pill); + background: var(--gradient-accent); color: #fff; font-weight: 700; font-size: 0.95rem; @@ -72,7 +74,7 @@ } .divider { - color: #4b5563; + color: var(--muted-border); font-size: 0.8rem; letter-spacing: 0.1em; text-transform: uppercase; @@ -96,15 +98,17 @@ .gridLabel { font-size: 0.85rem; - color: #94a3b8; + color: var(--muted-text); } .preset { - padding: 3px 10px; - border-radius: 6px; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); background: transparent; - border: 1.5px solid #4b5563; - color: #94a3b8; + /* muted-text, not muted-border — the border-only value measured under + 3:1 against the page background (rule 7). */ + border: 1.5px solid var(--muted-text); + color: var(--muted-text); font-size: 0.8rem; font-weight: 600; cursor: pointer; @@ -112,23 +116,23 @@ } .preset:hover { - border-color: #818cf8; - color: #c7d2fe; + border-color: var(--color-indigo); + color: var(--color-indigo-lighter); } .presetActive { - border-color: #f472b6; - color: #f9a8d4; - background: rgba(244, 114, 182, 0.1); + border-color: var(--color-pink); + color: var(--color-pink-light); + background: rgba(var(--color-pink-rgb), 0.1); } .gridInput { width: 56px; - padding: 3px 6px; - border-radius: 6px; - background: rgba(30, 27, 75, 0.8); - border: 1.5px solid #4b5563; - color: #e0e7ff; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + background: var(--surface); + border: 1.5px solid var(--muted-border); + color: var(--color-text); font-size: 0.85rem; font-weight: 600; text-align: center; @@ -137,21 +141,21 @@ } .gridInput:focus { - border-color: #818cf8; + border-color: var(--color-indigo); } .gridEq { font-size: 0.8rem; - color: #6b7280; + color: var(--muted-faint); margin: 0; } .puzzleBtn { - padding: 8px 24px; - border-radius: 999px; + padding: 12px var(--space-4); + border-radius: var(--radius-pill); background: transparent; - border: 2px solid #f472b6; - color: #f9a8d4; + border: 2px solid var(--color-pink); + color: var(--color-pink-light); font-weight: 700; font-size: 0.95rem; cursor: pointer; @@ -159,7 +163,7 @@ } .puzzleBtn:hover { - background: rgba(244, 114, 182, 0.12); - border-color: #e879f9; - color: #e879f9; + background: rgba(var(--color-pink-rgb), 0.12); + border-color: var(--color-pink-lighter); + color: var(--color-pink-lighter); } diff --git a/bucket_catch/packages/frontend/src/components/GameCanvas.module.css b/bucket_catch/packages/frontend/src/components/GameCanvas.module.css index 51facf3..73aa12f 100644 --- a/bucket_catch/packages/frontend/src/components/GameCanvas.module.css +++ b/bucket_catch/packages/frontend/src/components/GameCanvas.module.css @@ -17,7 +17,7 @@ top: 16px; left: 50%; transform: translateX(-50%); - color: rgba(224, 231, 255, 0.5); + color: rgba(var(--color-text-rgb), 0.5); font-size: 0.85rem; pointer-events: none; user-select: none; diff --git a/bucket_catch/packages/frontend/src/components/ModeSelect.module.css b/bucket_catch/packages/frontend/src/components/ModeSelect.module.css index acfb450..2b053aa 100644 --- a/bucket_catch/packages/frontend/src/components/ModeSelect.module.css +++ b/bucket_catch/packages/frontend/src/components/ModeSelect.module.css @@ -4,16 +4,16 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 24px; - background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); - color: #e0e7ff; + gap: var(--space-4); + background: var(--bg-gradient); + color: var(--color-text); } .heading { font-size: 2rem; font-weight: 800; margin: 0; - background: linear-gradient(90deg, #818cf8, #f472b6); + background: var(--gradient-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -21,22 +21,22 @@ .sub { font-size: 1.1rem; - color: #94a3b8; + color: var(--muted-text); margin: 0; } .cards { display: flex; - gap: 24px; + gap: var(--space-4); flex-wrap: wrap; justify-content: center; } .card { - background: rgba(30, 27, 75, 0.7); - border: 2px solid #818cf8; - border-radius: 16px; - padding: 32px 40px; + background: var(--surface); + border: 2px solid var(--color-indigo); + border-radius: var(--radius-lg); + padding: var(--space-5) var(--space-6); width: 220px; display: flex; flex-direction: column; @@ -44,16 +44,18 @@ gap: 12px; cursor: pointer; transition: border-color 0.15s, transform 0.15s; - color: #e0e7ff; + color: var(--color-text); } .card:hover { - border-color: #f472b6; + border-color: var(--color-pink); transform: translateY(-4px); } .cardIcon { font-size: 48px; + /* Dimmed relative to .cardDesc below it (rule 28). */ + opacity: 0.85; } .cardTitle { @@ -62,8 +64,9 @@ } .cardDesc { - font-size: 0.85rem; - color: #94a3b8; + /* Was 0.85rem (13.6px), under the 16px reading floor (rule 20). */ + font-size: 1rem; + color: var(--muted-text); text-align: center; line-height: 1.4; } @@ -71,11 +74,11 @@ .cardDisabled { opacity: 0.4; cursor: not-allowed; - border-color: #4b5563; + border-color: var(--muted-border); } .cardDisabled:hover { - border-color: #4b5563; + border-color: var(--muted-border); transform: none; } @@ -86,18 +89,19 @@ .gridRow { display: flex; align-items: center; - gap: 5px; + gap: var(--space-1); flex-wrap: wrap; justify-content: center; margin-top: 4px; } .preset { - padding: 3px 9px; - border-radius: 6px; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); background: transparent; - border: 1.5px solid #4b5563; - color: #94a3b8; + /* muted-text, not muted-border — same rule 7 contrast fix as DropZone. */ + border: 1.5px solid var(--muted-text); + color: var(--muted-text); font-size: 0.78rem; font-weight: 600; cursor: pointer; @@ -105,23 +109,28 @@ } .preset:hover { - border-color: #818cf8; - color: #c7d2fe; + border-color: var(--color-indigo); + color: var(--color-indigo-lighter); } .presetActive { - border-color: #f472b6; - color: #f9a8d4; - background: rgba(244, 114, 182, 0.12); + border-color: var(--color-pink); + color: var(--color-pink-light); + /* 0.1, not 0.12 — matches DropZone's identical "active preset chip" + role; the two files had drifted to different alpha values. */ + background: rgba(var(--color-pink-rgb), 0.1); } .gridInput { width: 52px; - padding: 3px 5px; - border-radius: 6px; - background: rgba(15, 12, 41, 0.8); - border: 1.5px solid #4b5563; - color: #e0e7ff; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + /* --surface, not a raw rgba(15,12,41,...) literal — DropZone's sibling + gridInput already used the surface color; this one had drifted to a + different base (bg-1) for the identical component. */ + background: var(--surface); + border: 1.5px solid var(--muted-border); + color: var(--color-text); font-size: 0.82rem; font-weight: 600; text-align: center; @@ -130,20 +139,24 @@ } .gridInput:focus { - border-color: #818cf8; + border-color: var(--color-indigo); } .gridEq { font-size: 0.78rem; - color: #6b7280; + color: var(--muted-faint); margin: 2px 0 0; } .puzzleStart { margin-top: 6px; - padding: 8px 20px; - border-radius: 999px; - background: linear-gradient(90deg, #f472b6, #818cf8); + padding: 10px 20px; + border-radius: var(--radius-pill); + /* --gradient-accent, not the stops written out reversed — this button + was pink->indigo while every other primary gradient in the app is + indigo->pink (same underlying bug the audit flagged in + PuzzleResult.module.css, just not cited by this exact line). */ + background: var(--gradient-accent); color: #fff; font-weight: 700; font-size: 0.9rem; diff --git a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css b/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css index 567dc6b..d1e5c58 100644 --- a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css +++ b/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css @@ -17,7 +17,7 @@ top: 16px; left: 50%; transform: translateX(-50%); - color: rgba(244, 114, 182, 0.7); + color: rgba(var(--color-pink-rgb), 0.7); font-size: 0.9rem; pointer-events: none; user-select: none; diff --git a/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css b/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css index b7db559..a4a6625 100644 --- a/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css +++ b/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css @@ -5,9 +5,9 @@ align-items: center; justify-content: center; gap: 28px; - padding: 32px 16px; - background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); - color: #e0e7ff; + padding: var(--space-5) var(--space-3); + background: var(--bg-gradient); + color: var(--color-text); } .scoreBox { @@ -15,14 +15,16 @@ display: flex; flex-direction: column; align-items: center; - gap: 8px; + gap: var(--space-2); } .grade { font-size: 6rem; font-weight: 900; line-height: 1; - background: linear-gradient(90deg, #f472b6, #818cf8); + /* Was reversed (pink->indigo) vs every other heading gradient in the + app (indigo->pink) — the bug DESIGN_AUDIT_TODO.md flagged. */ + background: var(--gradient-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -31,7 +33,7 @@ .pct { font-size: 2rem; font-weight: 700; - color: #a5b4fc; + color: var(--color-indigo-light); } .stats { @@ -40,18 +42,18 @@ } .caught { - color: #34d399; + color: var(--color-success); } .grid { display: grid; - gap: 3px; + gap: 4px; max-width: min(480px, 90vw); width: 100%; - border: 3px solid rgba(129, 140, 248, 0.4); - border-radius: 8px; + border: 3px solid rgba(var(--color-indigo-rgb), 0.4); + border-radius: var(--radius-md); overflow: hidden; - background: #1e1b4b; + background: var(--surface-solid); } .piece { @@ -64,16 +66,16 @@ .hole { width: 100%; aspect-ratio: 1; - background: rgba(30, 27, 75, 0.9); - border: 1px dashed rgba(129, 140, 248, 0.2); + background: rgba(var(--surface-rgb), 0.9); + border: 1px dashed rgba(var(--color-indigo-rgb), 0.2); } .restartBtn { - padding: 10px 28px; - border-radius: 999px; + padding: 14px 28px; + border-radius: var(--radius-pill); background: transparent; - border: 2px solid #f472b6; - color: #f9a8d4; + border: 2px solid var(--color-pink); + color: var(--color-pink-light); font-weight: 600; font-size: 1rem; cursor: pointer; @@ -81,6 +83,6 @@ } .restartBtn:hover { - border-color: #818cf8; - color: #a5b4fc; + border-color: var(--color-indigo); + color: var(--color-indigo-light); } diff --git a/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css b/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css index 514f369..7257f11 100644 --- a/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css +++ b/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css @@ -5,9 +5,9 @@ align-items: center; justify-content: center; gap: 28px; - padding: 32px 16px; - background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); - color: #e0e7ff; + padding: var(--space-5) var(--space-3); + background: var(--bg-gradient); + color: var(--color-text); } .scoreBox { @@ -15,14 +15,14 @@ display: flex; flex-direction: column; align-items: center; - gap: 8px; + gap: var(--space-2); } .grade { font-size: 6rem; font-weight: 900; line-height: 1; - background: linear-gradient(90deg, #818cf8, #f472b6); + background: var(--gradient-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -31,27 +31,27 @@ .pct { font-size: 2rem; font-weight: 700; - color: #a5b4fc; + color: var(--color-indigo-light); } .stats { display: flex; - gap: 24px; + gap: var(--space-4); font-size: 1.1rem; font-weight: 600; } .caught { - color: #34d399; + color: var(--color-success); } .missed { - color: #f87171; + color: var(--color-error); } .lists { display: flex; - gap: 32px; + gap: var(--space-5); flex-wrap: wrap; justify-content: center; max-width: 640px; @@ -61,7 +61,7 @@ .listTitle { font-size: 1rem; font-weight: 700; - color: #94a3b8; + color: var(--muted-text); text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 8px; @@ -79,10 +79,10 @@ .fileItem { display: flex; align-items: center; - gap: 8px; - background: rgba(30, 27, 75, 0.7); - border-radius: 8px; - padding: 6px 12px; + gap: var(--space-2); + background: var(--surface); + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-3); font-size: 0.9rem; } @@ -100,7 +100,7 @@ } .fsize { - color: #64748b; + color: var(--muted-faint); font-size: 0.8rem; } @@ -108,13 +108,13 @@ display: flex; flex-direction: column; align-items: center; - gap: 12px; + gap: var(--space-3); } .actionBtn { - padding: 12px 32px; - border-radius: 999px; - background: linear-gradient(90deg, #818cf8, #f472b6); + padding: 16px var(--space-5); + border-radius: var(--radius-pill); + background: var(--gradient-accent); color: #fff; font-weight: 700; font-size: 1rem; @@ -128,11 +128,11 @@ } .restartBtn { - padding: 10px 28px; - border-radius: 999px; + padding: 14px 28px; + border-radius: var(--radius-pill); background: transparent; - border: 2px solid #818cf8; - color: #a5b4fc; + border: 2px solid var(--color-indigo); + color: var(--color-indigo-light); font-weight: 600; font-size: 1rem; cursor: pointer; @@ -140,18 +140,18 @@ } .restartBtn:hover { - border-color: #f472b6; - color: #f9a8d4; + border-color: var(--color-pink); + color: var(--color-pink-light); } .status { - color: #34d399; + color: var(--color-success); font-weight: 600; margin: 0; } .error { - color: #f87171; + color: var(--color-error); font-weight: 600; margin: 0; } diff --git a/bucket_catch/packages/frontend/src/index.css b/bucket_catch/packages/frontend/src/index.css index 0e7a1ad..8fb90b6 100644 --- a/bucket_catch/packages/frontend/src/index.css +++ b/bucket_catch/packages/frontend/src/index.css @@ -1,3 +1,76 @@ +/* Design tokens — the shared source of truth every *.module.css file + should reference instead of its own hardcoded hex literals. Previously + absent entirely (rule 4), which is how the same page-background/heading + gradients ended up copy-pasted verbatim across 4 files, one of them with + the gradient stops reversed (PuzzleResult.module.css, and ModeSelect's + puzzleStart button — same bug, two spots). */ +:root { + /* Page background (3-stop gradient) */ + --bg-1: #0f0c29; + --bg-2: #302b63; + --bg-3: #24243e; + --bg-gradient: linear-gradient(135deg, var(--bg-1), var(--bg-2), var(--bg-3)); + + /* Drag-over state (DropZone only) */ + --bg-drag-1: #1a1560; + --bg-drag-2: #4a3f9e; + --bg-drag-3: #3a3868; + --bg-drag-gradient: linear-gradient( + 135deg, + var(--bg-drag-1), + var(--bg-drag-2), + var(--bg-drag-3) + ); + + /* Accent pair. --gradient-accent is the ONE canonical direction + (indigo -> pink) for every heading/primary-button gradient in the + app — always use this token, never write the stops out by hand. */ + --color-indigo: #818cf8; + --color-indigo-rgb: 129, 140, 248; + --color-pink: #f472b6; + --color-pink-rgb: 244, 114, 182; + --gradient-accent: linear-gradient(90deg, var(--color-indigo), var(--color-pink)); + + --color-indigo-light: #a5b4fc; + --color-indigo-lighter: #c7d2fe; + --color-pink-light: #f9a8d4; + --color-pink-lighter: #e879f9; + + --color-text: #e0e7ff; + --color-text-rgb: 224, 231, 255; + + /* Raised-surface fill (cards, file rows, the puzzle grid) — one base + color at different opacities/solidity, not independent literals. */ + --surface-rgb: 30, 27, 75; + --surface: rgba(var(--surface-rgb), 0.7); + --surface-solid: rgb(var(--surface-rgb)); + + --color-success: #34d399; + --color-error: #f87171; + + /* Muted grays — was 4 unrelated shades (rule 2); collapsed to 2 named, + semantically distinct roles: body-adjacent secondary text, and a + slightly darker tertiary/caption tone. (Borders get their own token + below, since one of them needed a contrast fix rule 7 flagged.) */ + --muted-text: #94a3b8; + --muted-faint: #6b7280; + --muted-border: #4b5563; + + /* Spacing scale (4px base) */ + --space-1: 4px; + --space-2: 8px; + --space-3: 16px; + --space-4: 24px; + --space-5: 32px; + --space-6: 40px; + + /* Corner radius scale */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 16px; + --radius-pill: 999px; +} + *, *::before, *::after { box-sizing: border-box; } @@ -7,6 +80,6 @@ html, body, #root { padding: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - background: #0f0c29; - color: #e0e7ff; + background: var(--bg-1); + color: var(--color-text); }