Files

3.9 KiB

UI Theming — switchable themes + "cockpit" dark skin

Context

The current UI is functional but flat and generic. The user wants to try a different look (reference: a dark "cockpit" dashboard mockup — deep navy surfaces, cyan/amber accents, glow on the live reading, photo banner) without losing the ability to switch back.

Today there is no theming layer at all: 0 CSS custom properties, ~430 hardcoded colours across 16 CSS files. So the switch has to be built before any new skin can exist.

Key finding that reduces scope: Main.jsx already has the card structure the mockup shows — .weighing-section containing .weight-card elements for Tare / Gross / Scale / Net, each with header, value and meta. No restructuring of the detail panel is needed; this is a styling job.

Approach

Token layer + data-theme attribute on <html>. Two themes:

  • classic — reproduces today's look exactly (the safe default)
  • cockpit — the new dark skin

1. Token layer

New frontend/src/styles/tokens.css, imported once in index.css:

:root, :root[data-theme="classic"] {
  --bg, --surface, --surface-raised, --border,
  --text, --text-muted, --accent, --accent-contrast,
  --success, --warning, --danger,
  --radius, --shadow-card, --glow-accent
}
:root[data-theme="cockpit"] { /* same names, dark values */ }

Token NAMES are semantic (--surface), never literal (--navy), so a third theme is one block.

2. Convert existing CSS to var()

Mechanical replacement of hardcoded colours. classic values are lifted from the current CSS so the default theme is visually unchanged — that is the acceptance test.

3. Theme switch

  • ThemeContext (mirrors the existing AuthContext/NomenclatureContext pattern in frontend/src/contexts/)
  • Sets data-theme on document.documentElement
  • Persists to localStorage, wrapped in try/catch (private mode can throw)
  • Toggle control in Header.jsx
  • Default: classic — nothing changes for operators unless they opt in

4. Cockpit theme + banner

Dark palette, raised card surfaces, cyan accent, glow on the live reading, monospace weight readouts. Banner strip behind the vehicle title in .vehicle-detail — image asset TBD (CSS gradient placeholder until the user supplies one). Banner is cockpit-only and must collapse on short viewports; this may run on a weigh-station monitor.

Staging

Deliberately two stages so a wrong palette is caught after 3 files, not 16.

Stage 1 — tokens, ThemeContext, header toggle, and convert:

  • frontend/src/components/Main.css (68 colours)
  • frontend/src/components/Header.css (17)
  • frontend/src/components/ScaleMonitor/ScaleMonitor.css (47)
  • frontend/src/index.css, frontend/src/App.css

→ user reviews both themes on the main weighing screen.

Stage 2 — remaining 13 files, once the direction is approved: NomenclatureManager (79), NomenclatureManagementOverlay (54), LicenseManager (45), ChangePasswordOverlay (22), ExtraCombinationsTable (20), DataDisplay (19), Login (15), NomenclatureEntryForm (14), NomenclatureDropdown (14), FormOverlay, ReportPreviewModal.

Critical files

  • frontend/src/styles/tokens.css (new)
  • frontend/src/contexts/ThemeContext.jsx (new)
  • frontend/src/components/Header.jsx — toggle control
  • frontend/src/App.jsx — wrap in ThemeProvider
  • frontend/src/components/Main.css — the main surface

Report editor CSS (components/ReportEditor/) is out of scope: the A4 designer renders a paper page that must stay white regardless of theme.

Verification

  1. npx vitest run — 201 tests still pass
  2. Classic theme vs git stash screenshot: visually identical
  3. Toggle switches instantly, no reload; survives refresh (localStorage)
  4. Cockpit: check contrast on the live reading, balanced/unbalanced states, and that the banner collapses on a short viewport
  5. Real browser check of both themes on the main weighing screen