feat(frontend): add dark mode option (#34)
All checks were successful
CI / Maven unit tests (push) Successful in 13s
CI / Build and push images (push) Successful in 48s
CI / Deploy to server (push) Successful in 19s

Resolves #32

Reviewed-on: #34
This commit was merged in pull request #34.
This commit is contained in:
2026-08-30 17:48:59 +01:00
parent ec9a982fe9
commit 7f58bad245
10 changed files with 239 additions and 30 deletions

View File

@@ -1,5 +1,10 @@
@import "tailwindcss";
/* Drive the `dark:` variant off the same data-theme attribute the toggle sets,
instead of Tailwind's default prefers-color-scheme. Without this a stray
`dark:` utility would disagree with an explicit light/dark choice. */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
@theme {
/* ── Fonts ───────────────────────────────────────────────────────────── */
--font-body: "IBM Plex Sans", system-ui, sans-serif;
@@ -12,6 +17,10 @@
--color-fields: #F4F0E8;
--color-dropzone: #F9F6EF;
/* Letterbox behind video. Dark in both themes — it is a cinema surface,
not an ink colour — so `text-on-dark` always reads on top of it. */
--color-media-backdrop: #2B2620;
/* ── Ink ─────────────────────────────────────────────────────────────── */
--color-text-primary: #2B2620;
--color-text-strong: #35302A;
@@ -44,6 +53,55 @@
--color-inactive-strong: #C4BAA9;
}
/* Dark mode. Only token values change here: every component already styles
itself with the semantic utilities above, so the whole UI follows from this
one block. `--color-on-dark` is deliberately absent — it is the ink on the
media backdrop, which stays dark in both themes. */
:root[data-theme="dark"] {
color-scheme: dark;
/* ── Surfaces ── elevation inverts: the canvas is darkest, cards rise ─── */
--color-background: #1B1815;
--color-sidebar: #151210;
--color-card: #26211C;
--color-fields: #211D19;
--color-dropzone: #201C18;
--color-media-backdrop: #121010;
/* ── Ink ── warm off-whites, mirroring the light ramp in reverse ──────── */
--color-text-primary: #F3EDE3;
--color-text-strong: #E6DFD3;
--color-text-body: #CFC6B8;
--color-text-secondary: #ABA192;
--color-muted: #948A7C;
--color-faint-mono: #8A8073;
/* ── Accents ── brightened to carry on a dark canvas, so hover now lifts
rather than deepens ─────────────────────────────────────────────────── */
--color-terracotta: #D08055;
--color-terracotta-hover: #E09A6F;
--color-terracotta-light: #E3A87F;
--color-terracotta-disabled: #8A6A55;
--color-olive: #9DAC77;
/* ── Status ──────────────────────────────────────────────────────────── */
--color-live: #E0705A;
--color-error: #E0705A;
/* ── Ink on accent surfaces ── the accents are light here, so the ink that
sits on them flips dark ─────────────────────────────────────────────── */
--color-on-accent: #241B14;
/* ── Lines, chips, placeholders ──────────────────────────────────────── */
--color-hairline: #2E2822;
--color-hover: #322B24;
--color-selected: #2E2119;
--color-ring: #3E362C;
--color-inactive: #3B342B;
--color-inactive-strong: #6B6153;
}
:root {
font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
@@ -52,6 +110,7 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color-scheme: light;
}
body {
@@ -66,6 +125,16 @@ body {
color: var(--color-text-primary);
}
/* Google Identity Services renders its sign-in button in a cross-origin iframe
whose document is light-scheme with a transparent background. Left to inherit
our `color-scheme: dark`, Chrome cannot honour dark there and paints an opaque
white canvas behind it instead — framing the pill in a white box on every dark
page. Pinning the iframe back to light keeps it transparent; the button's own
colours come from the `theme` prop in Topbar.tsx. */
iframe[src*="accounts.google.com/gsi/"] {
color-scheme: light;
}
/* Clip trim slider (react-range-slider-input). Its own stylesheet ships blue
defaults; the #id beats those class selectors regardless of import order. */
#range-slider {
@@ -87,4 +156,4 @@ body {
#range-slider .range-slider__thumb:focus-visible {
box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-terracotta) 35%, transparent);
}
}